@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/index.js
CHANGED
|
@@ -1511,6 +1511,399 @@ var init_tools = __esm({
|
|
|
1511
1511
|
}
|
|
1512
1512
|
});
|
|
1513
1513
|
|
|
1514
|
+
// src/tools.jobs.ts
|
|
1515
|
+
function formatJobNotice(n) {
|
|
1516
|
+
return `[job ${n.id} ${n.status}${n.reason ? `: ${n.reason}` : ""}] ${n.kind}${n.label ? ` \`${n.label}\`` : ""}
|
|
1517
|
+
` + (n.tail ? `${n.tail.replace(/\n+$/, "")}
|
|
1518
|
+
` : "(no output)\n") + `Read the full output with JobOutput({id:"${n.id}"}).`;
|
|
1519
|
+
}
|
|
1520
|
+
function makeJobTools(registry, opts = {}) {
|
|
1521
|
+
const maxWaitMs = opts.maxWaitMs ?? 12e4;
|
|
1522
|
+
return [
|
|
1523
|
+
{
|
|
1524
|
+
name: "JobOutput",
|
|
1525
|
+
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.",
|
|
1526
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" }, offset: { type: "number", description: "return output after this position (from a previous read's `next`)" } } },
|
|
1527
|
+
async run({ id, offset }) {
|
|
1528
|
+
const r = registry.read(String(id), Number(offset) || 0);
|
|
1529
|
+
if (!r) return NO_JOB(String(id));
|
|
1530
|
+
const st = registry.status(String(id));
|
|
1531
|
+
return `[${st.status}${st.reason ? `: ${st.reason}` : ""}] next=${r.next}${r.truncated ? " (older output dropped)" : ""}
|
|
1532
|
+
${r.text.replace(/\n+$/, "") || "(no new output)"}`;
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
name: "JobStatus",
|
|
1537
|
+
description: "Status of a job (running/done/error/killed, age, last activity). Omit `id` to list all jobs.",
|
|
1538
|
+
parameters: { type: "object", properties: { id: { type: "string" } } },
|
|
1539
|
+
async run({ id }) {
|
|
1540
|
+
const now3 = registry.options.now();
|
|
1541
|
+
if (!id) {
|
|
1542
|
+
const all = registry.list();
|
|
1543
|
+
return all.length ? all.map((j2) => describe(j2, now3)).join("\n") : "(no background jobs)";
|
|
1544
|
+
}
|
|
1545
|
+
const j = registry.get(String(id));
|
|
1546
|
+
return j ? describe(j, now3) : NO_JOB(String(id));
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
name: "JobKill",
|
|
1551
|
+
description: "Stop a running job by id (sandbox jobs are cancelled without committing; processes get SIGTERM; MCP calls are cancelled).",
|
|
1552
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
1553
|
+
async run({ id }) {
|
|
1554
|
+
return registry.kill(String(id), "killed by JobKill") ? `Killed job ${id}.` : NO_JOB(String(id));
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
name: "JobWait",
|
|
1559
|
+
description: `Block until a job finishes or \`timeoutMs\` passes (max ${maxWaitMs}ms), then return its status and output tail. Prefer this over sleeping.`,
|
|
1560
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" }, timeoutMs: { type: "number" } } },
|
|
1561
|
+
async run({ id, timeoutMs }) {
|
|
1562
|
+
const j = await registry.wait(String(id), Math.min(Number(timeoutMs) || maxWaitMs, maxWaitMs));
|
|
1563
|
+
if (!j) return NO_JOB(String(id));
|
|
1564
|
+
const tail = (registry.output(j.id) ?? "").slice(-4e3).replace(/\n+$/, "");
|
|
1565
|
+
return `${describe(j, registry.options.now())}
|
|
1566
|
+
${tail || "(no output)"}`;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
];
|
|
1570
|
+
}
|
|
1571
|
+
var log5, JobRegistryOptions, secs, JobRegistry, SandboxJobRegistry, NO_JOB, describe;
|
|
1572
|
+
var init_tools_jobs = __esm({
|
|
1573
|
+
"src/tools.jobs.ts"() {
|
|
1574
|
+
"use strict";
|
|
1575
|
+
init_logging();
|
|
1576
|
+
log5 = forComponent("jobs");
|
|
1577
|
+
JobRegistryOptions = class {
|
|
1578
|
+
/** Tail buffer cap per job (chars). */
|
|
1579
|
+
maxBuffer = 256 * 1024;
|
|
1580
|
+
/** Idle window per kind: no output/progress/CPU for this long ⇒ stuck. Kinds without an activity channel
|
|
1581
|
+
* (host, native) are absent on purpose — silence is all they can ever show, so it proves nothing; their
|
|
1582
|
+
* bound is the hard cap. Explicit `background:true` shell jobs opt out per job (a quiet server is healthy). */
|
|
1583
|
+
idleMs = { shell: 10 * 6e4, mcp: 10 * 6e4 };
|
|
1584
|
+
/** Absolute ceiling per kind (0/absent = none). */
|
|
1585
|
+
hardCapMs = { mcp: 30 * 6e4 };
|
|
1586
|
+
/** Soft foreground timeout per kind: past it the caller stops waiting and gets a job id instead. */
|
|
1587
|
+
foregroundMs = { mcp: 3e4 };
|
|
1588
|
+
/** How often the reaper runs while reapable jobs exist. */
|
|
1589
|
+
sweepMs = 15e3;
|
|
1590
|
+
/** Grace between the kill signal and a forced kill (producers that escalate read this). */
|
|
1591
|
+
killGraceMs = 5e3;
|
|
1592
|
+
/** Finished jobs kept for inspection; older finished ones are pruned (every delegated tool call is a job). */
|
|
1593
|
+
keepFinished = 200;
|
|
1594
|
+
/** Clock (injectable for tests). Late-bound so a patched Date.now is honoured. */
|
|
1595
|
+
now = () => Date.now();
|
|
1596
|
+
/** Terminal-state notification for jobs with `notify` (the Agent wires this to `inject`). */
|
|
1597
|
+
onExit;
|
|
1598
|
+
};
|
|
1599
|
+
secs = (ms) => `${Math.round(ms / 1e3)}s`;
|
|
1600
|
+
JobRegistry = class {
|
|
1601
|
+
options;
|
|
1602
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1603
|
+
seq = 0;
|
|
1604
|
+
timer;
|
|
1605
|
+
constructor(options) {
|
|
1606
|
+
this.options = { ...new JobRegistryOptions(), ...options };
|
|
1607
|
+
}
|
|
1608
|
+
/** Register externally-driven work (a process, a request, a native tool) and get a handle to feed it. */
|
|
1609
|
+
track(opts = {}) {
|
|
1610
|
+
const o = this.options;
|
|
1611
|
+
const kind = opts.kind ?? "task";
|
|
1612
|
+
const now3 = o.now();
|
|
1613
|
+
let settle;
|
|
1614
|
+
const done = new Promise((r) => {
|
|
1615
|
+
settle = r;
|
|
1616
|
+
});
|
|
1617
|
+
const job = {
|
|
1618
|
+
id: `job-${++this.seq}`,
|
|
1619
|
+
kind,
|
|
1620
|
+
label: opts.label ?? "",
|
|
1621
|
+
status: "running",
|
|
1622
|
+
startedAt: now3,
|
|
1623
|
+
lastActivityAt: now3,
|
|
1624
|
+
bytes: 0,
|
|
1625
|
+
holds: !!opts.holds,
|
|
1626
|
+
buf: "",
|
|
1627
|
+
dropped: 0,
|
|
1628
|
+
idleMs: opts.idleMs ?? o.idleMs[kind] ?? 0,
|
|
1629
|
+
hardCapMs: opts.hardCapMs ?? o.hardCapMs[kind] ?? 0,
|
|
1630
|
+
drain: !!opts.drain,
|
|
1631
|
+
notify: !!opts.notify,
|
|
1632
|
+
idleAfterActivity: !!opts.idleAfterActivity,
|
|
1633
|
+
active: false,
|
|
1634
|
+
cpuTime: opts.cpuTime,
|
|
1635
|
+
onKill: opts.onKill,
|
|
1636
|
+
onExit: opts.onExit,
|
|
1637
|
+
done,
|
|
1638
|
+
settle
|
|
1639
|
+
};
|
|
1640
|
+
this.jobs.set(job.id, job);
|
|
1641
|
+
if (opts.seed) this.append(job, opts.seed);
|
|
1642
|
+
this.arm();
|
|
1643
|
+
return {
|
|
1644
|
+
id: job.id,
|
|
1645
|
+
chunk: (s) => {
|
|
1646
|
+
if (s) {
|
|
1647
|
+
this.append(job, s);
|
|
1648
|
+
job.lastActivityAt = o.now();
|
|
1649
|
+
job.active = true;
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
touch: () => {
|
|
1653
|
+
job.lastActivityAt = o.now();
|
|
1654
|
+
job.active = true;
|
|
1655
|
+
},
|
|
1656
|
+
finish: (status, result, reason) => this.end(job, status, result, reason),
|
|
1657
|
+
detach: () => {
|
|
1658
|
+
job.holds = false;
|
|
1659
|
+
job.notify = true;
|
|
1660
|
+
job.drain = true;
|
|
1661
|
+
},
|
|
1662
|
+
get status() {
|
|
1663
|
+
return job.status;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
/** Start `fn` in the background and return a job id immediately (sandbox/subagent semantics: drained at turn end). */
|
|
1668
|
+
start(fn, opts = {}) {
|
|
1669
|
+
return this.run(fn, { drain: true, ...opts }).handle.id;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Run `fn` in the FOREGROUND with a soft timeout: resolves `{done:true,value}` if it settles in time, else
|
|
1673
|
+
* `{done:false,id}` — the work keeps going as a detached job whose completion is reported via `onExit`.
|
|
1674
|
+
* A rejection before the timeout is rethrown (the caller's normal error path). `softTimeoutMs` ≤ 0 waits forever.
|
|
1675
|
+
*/
|
|
1676
|
+
async runForeground(fn, opts = {}) {
|
|
1677
|
+
const { handle, promise } = this.run(fn, opts);
|
|
1678
|
+
const soft = opts.softTimeoutMs ?? this.options.foregroundMs[opts.kind ?? "task"] ?? 0;
|
|
1679
|
+
let t;
|
|
1680
|
+
const late = soft > 0 ? new Promise((r) => {
|
|
1681
|
+
t = setTimeout(() => r("late"), soft);
|
|
1682
|
+
}) : new Promise(() => {
|
|
1683
|
+
});
|
|
1684
|
+
try {
|
|
1685
|
+
const r = await Promise.race([promise.then((v) => ({ v }), (e) => ({ e })), late]);
|
|
1686
|
+
if (r !== "late" && handle.status !== "running") this.jobs.delete(handle.id);
|
|
1687
|
+
if (r === "late") {
|
|
1688
|
+
if (handle.status === "running") {
|
|
1689
|
+
handle.detach();
|
|
1690
|
+
return { done: false, id: handle.id };
|
|
1691
|
+
}
|
|
1692
|
+
const settled = await promise.then((v) => ({ v }), (e) => ({ e }));
|
|
1693
|
+
if ("e" in settled) throw settled.e;
|
|
1694
|
+
return { done: true, value: settled.v };
|
|
1695
|
+
}
|
|
1696
|
+
if ("e" in r) throw r.e;
|
|
1697
|
+
return { done: true, value: r.v };
|
|
1698
|
+
} finally {
|
|
1699
|
+
if (t) clearTimeout(t);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
run(fn, opts) {
|
|
1703
|
+
const ctl = new AbortController();
|
|
1704
|
+
const handle = this.track({ ...opts, onKill: (reason) => {
|
|
1705
|
+
ctl.abort(Object.assign(new Error(reason), { name: "AbortError", code: "job_killed" }));
|
|
1706
|
+
opts.onKill?.(reason);
|
|
1707
|
+
} });
|
|
1708
|
+
let promise;
|
|
1709
|
+
try {
|
|
1710
|
+
promise = fn({ signal: ctl.signal, onChunk: handle.chunk, touch: handle.touch });
|
|
1711
|
+
} catch (e) {
|
|
1712
|
+
promise = Promise.reject(e);
|
|
1713
|
+
}
|
|
1714
|
+
promise.then(
|
|
1715
|
+
(res) => {
|
|
1716
|
+
if (handle.status === "running" && typeof res === "string" && res) handle.chunk(res);
|
|
1717
|
+
handle.finish("done", res);
|
|
1718
|
+
},
|
|
1719
|
+
(e) => {
|
|
1720
|
+
if (handle.status === "running") handle.chunk(`
|
|
1721
|
+
[error] ${e?.message ?? e}`);
|
|
1722
|
+
handle.finish("error", void 0, e?.message ?? String(e));
|
|
1723
|
+
}
|
|
1724
|
+
);
|
|
1725
|
+
return { handle, promise };
|
|
1726
|
+
}
|
|
1727
|
+
append(job, s) {
|
|
1728
|
+
job.buf += s;
|
|
1729
|
+
const over = job.buf.length - this.options.maxBuffer;
|
|
1730
|
+
if (over > 0) {
|
|
1731
|
+
job.buf = job.buf.slice(over);
|
|
1732
|
+
job.dropped += over;
|
|
1733
|
+
}
|
|
1734
|
+
job.bytes = job.buf.length;
|
|
1735
|
+
}
|
|
1736
|
+
end(job, status, result, reason) {
|
|
1737
|
+
if (job.status !== "running") return;
|
|
1738
|
+
job.status = status;
|
|
1739
|
+
job.endedAt = this.options.now();
|
|
1740
|
+
job.result = result;
|
|
1741
|
+
if (reason && status !== "done") job.reason = reason;
|
|
1742
|
+
job.holds = false;
|
|
1743
|
+
job.settle();
|
|
1744
|
+
this.prune();
|
|
1745
|
+
const onExit = job.onExit ?? this.options.onExit;
|
|
1746
|
+
if (job.notify && onExit) {
|
|
1747
|
+
try {
|
|
1748
|
+
onExit({ id: job.id, kind: job.kind, label: job.label, status, reason: job.reason, tail: job.buf.slice(-4e3), result });
|
|
1749
|
+
} catch (e) {
|
|
1750
|
+
log5.warn(`job onExit handler threw for ${job.id}: ${e}`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
prune() {
|
|
1755
|
+
const finished = [...this.jobs.values()].filter((j) => j.status !== "running");
|
|
1756
|
+
for (const j of finished.slice(0, Math.max(0, finished.length - this.options.keepFinished))) this.jobs.delete(j.id);
|
|
1757
|
+
}
|
|
1758
|
+
/** Current tail output (null = no such job). */
|
|
1759
|
+
output(id) {
|
|
1760
|
+
return this.jobs.get(id)?.buf ?? null;
|
|
1761
|
+
}
|
|
1762
|
+
/** Incremental read: output from absolute char `offset` (clamped to what the ring still holds). */
|
|
1763
|
+
read(id, offset = 0) {
|
|
1764
|
+
const j = this.jobs.get(id);
|
|
1765
|
+
if (!j) return null;
|
|
1766
|
+
const start = Math.max(0, offset - j.dropped);
|
|
1767
|
+
return { text: j.buf.slice(start), next: j.dropped + j.buf.length, truncated: offset < j.dropped };
|
|
1768
|
+
}
|
|
1769
|
+
get(id) {
|
|
1770
|
+
const j = this.jobs.get(id);
|
|
1771
|
+
return j ? this.info(j) : null;
|
|
1772
|
+
}
|
|
1773
|
+
status(id) {
|
|
1774
|
+
const j = this.jobs.get(id);
|
|
1775
|
+
return j ? { status: j.status, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} } : null;
|
|
1776
|
+
}
|
|
1777
|
+
list() {
|
|
1778
|
+
return [...this.jobs.values()].map((j) => this.info(j));
|
|
1779
|
+
}
|
|
1780
|
+
info(j) {
|
|
1781
|
+
const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result } = j;
|
|
1782
|
+
return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result };
|
|
1783
|
+
}
|
|
1784
|
+
/** Wait (bounded) for a job to settle. Resolves the job's info either way; null for an unknown id. */
|
|
1785
|
+
async wait(id, timeoutMs) {
|
|
1786
|
+
const j = this.jobs.get(id);
|
|
1787
|
+
if (!j) return null;
|
|
1788
|
+
let t;
|
|
1789
|
+
await Promise.race([j.done, new Promise((r) => {
|
|
1790
|
+
t = setTimeout(r, Math.max(0, timeoutMs));
|
|
1791
|
+
})]);
|
|
1792
|
+
if (t) clearTimeout(t);
|
|
1793
|
+
return this.info(j);
|
|
1794
|
+
}
|
|
1795
|
+
/** Stop a job: runs its `onKill` (abort/SIGTERM) and marks it killed with `reason`. */
|
|
1796
|
+
kill(id, reason = "killed by request") {
|
|
1797
|
+
const j = this.jobs.get(id);
|
|
1798
|
+
if (!j) return false;
|
|
1799
|
+
if (j.status === "running") {
|
|
1800
|
+
this.end(j, "killed", void 0, reason);
|
|
1801
|
+
try {
|
|
1802
|
+
j.onKill?.(reason);
|
|
1803
|
+
} catch (e) {
|
|
1804
|
+
log5.warn(`onKill for ${id} threw: ${e}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
/** Kill every running job — or, with `onlyDrainable`, only turn-scoped ones (sandbox/sub-agent/handed-off MCP),
|
|
1810
|
+
* leaving jobs meant to outlive a run (a background dev server) alone. */
|
|
1811
|
+
killAll(reason = "run ended", onlyDrainable = false) {
|
|
1812
|
+
for (const j of this.jobs.values()) if (!onlyDrainable || j.drain) this.kill(j.id, reason);
|
|
1813
|
+
}
|
|
1814
|
+
/** Jobs something is synchronously waiting on (see `JobTrackOptions.holds`) that are still running. */
|
|
1815
|
+
holding() {
|
|
1816
|
+
return [...this.jobs.values()].filter((j) => j.status === "running" && j.holds).map((j) => this.info(j));
|
|
1817
|
+
}
|
|
1818
|
+
/** Await every drainable running job (turn end). Returns the ids that were still running when drain began. */
|
|
1819
|
+
async drain(signal) {
|
|
1820
|
+
const pending = [...this.jobs.values()].filter((j) => j.status === "running" && j.drain);
|
|
1821
|
+
const all = Promise.all(pending.map((j) => j.done));
|
|
1822
|
+
await (signal ? Promise.race([all, new Promise((r) => signal.aborted ? r() : signal.addEventListener("abort", () => r(), { once: true }))]) : all);
|
|
1823
|
+
return pending.map((j) => j.id);
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* The stuck-job decision, synchronous part: kill every running job past its hard cap, or past its idle
|
|
1827
|
+
* window when it has no CPU probe to argue otherwise. Cheap — the stall watchdog calls it on every tick.
|
|
1828
|
+
* Returns the killed ids. Jobs WITH a probe are left to `sweep()`.
|
|
1829
|
+
*/
|
|
1830
|
+
reap() {
|
|
1831
|
+
const now3 = this.options.now();
|
|
1832
|
+
const killed = [];
|
|
1833
|
+
for (const j of this.jobs.values()) {
|
|
1834
|
+
if (j.status !== "running") continue;
|
|
1835
|
+
const verdict = this.verdict(j, now3);
|
|
1836
|
+
if (!verdict || verdict.idle && j.cpuTime) continue;
|
|
1837
|
+
this.stuck(j, verdict.reason, now3);
|
|
1838
|
+
killed.push(j.id);
|
|
1839
|
+
}
|
|
1840
|
+
return killed;
|
|
1841
|
+
}
|
|
1842
|
+
/** Full sweep: sample CPU probes (advancing CPU = activity), then `reap()` including probed jobs. */
|
|
1843
|
+
async sweep() {
|
|
1844
|
+
const now3 = this.options.now();
|
|
1845
|
+
for (const j of [...this.jobs.values()]) {
|
|
1846
|
+
if (j.status !== "running" || !j.cpuTime || !j.idleMs) continue;
|
|
1847
|
+
let cpu;
|
|
1848
|
+
try {
|
|
1849
|
+
cpu = await j.cpuTime();
|
|
1850
|
+
} catch (e) {
|
|
1851
|
+
log5.debug(`cpu probe for ${j.id} failed: ${e}`);
|
|
1852
|
+
}
|
|
1853
|
+
if (cpu != null && j.lastCpu != null && cpu > j.lastCpu) j.lastActivityAt = this.options.now();
|
|
1854
|
+
const prev = j.lastCpu;
|
|
1855
|
+
if (cpu != null) j.lastCpu = cpu;
|
|
1856
|
+
const verdict = this.verdict(j, now3);
|
|
1857
|
+
if (!verdict?.idle || j.status !== "running") continue;
|
|
1858
|
+
if (cpu == null) {
|
|
1859
|
+
log5.debug(`not reaping ${j.id}: ${verdict.reason} but CPU unknown`);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
if (prev == null) continue;
|
|
1863
|
+
this.stuck(j, `${verdict.reason}, no CPU progress (${prev}s \u2192 ${cpu}s)`, now3);
|
|
1864
|
+
}
|
|
1865
|
+
return this.reap();
|
|
1866
|
+
}
|
|
1867
|
+
verdict(j, now3) {
|
|
1868
|
+
if (j.hardCapMs && now3 - j.startedAt >= j.hardCapMs) return { reason: `stuck: exceeded hard cap ${secs(j.hardCapMs)}`, idle: false };
|
|
1869
|
+
if (j.idleMs && (!j.idleAfterActivity || j.active) && now3 - j.lastActivityAt >= j.idleMs) return { reason: `stuck: idle ${secs(now3 - j.lastActivityAt)}`, idle: true };
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
stuck(j, reason, now3) {
|
|
1873
|
+
log5.warn(`reaping ${j.id} (${j.kind} "${j.label}"): ${reason} \u2014 age ${secs(now3 - j.startedAt)}, last activity ${secs(now3 - j.lastActivityAt)} ago, idleMs=${j.idleMs}, hardCapMs=${j.hardCapMs}, ${j.bytes} byte(s) output`);
|
|
1874
|
+
if (j.onKill) j.notify = true;
|
|
1875
|
+
this.kill(j.id, reason);
|
|
1876
|
+
}
|
|
1877
|
+
/** Keep a reaper interval alive only while a reapable job is running (unref'd: never holds the process open). */
|
|
1878
|
+
arm() {
|
|
1879
|
+
if (this.timer || !this.options.sweepMs) return;
|
|
1880
|
+
this.timer = setInterval(() => {
|
|
1881
|
+
const live = [...this.jobs.values()].some((j) => j.status === "running" && (j.idleMs || j.hardCapMs));
|
|
1882
|
+
if (!live) {
|
|
1883
|
+
clearInterval(this.timer);
|
|
1884
|
+
this.timer = void 0;
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
void this.sweep().catch((e) => log5.warn(`job sweep failed: ${e}`));
|
|
1888
|
+
}, this.options.sweepMs);
|
|
1889
|
+
this.timer.unref?.();
|
|
1890
|
+
}
|
|
1891
|
+
/** Stop the reaper timer (tests / teardown). */
|
|
1892
|
+
dispose() {
|
|
1893
|
+
if (this.timer) clearInterval(this.timer);
|
|
1894
|
+
this.timer = void 0;
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
SandboxJobRegistry = class extends JobRegistry {
|
|
1898
|
+
constructor(maxBuffer) {
|
|
1899
|
+
super(typeof maxBuffer === "number" ? { maxBuffer } : maxBuffer);
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs.`;
|
|
1903
|
+
describe = (j, now3) => `${j.id} ${j.status}${j.reason ? ` (${j.reason})` : ""} ${j.kind}${j.label ? " " + j.label : ""} \xB7 ${secs((j.endedAt ?? now3) - j.startedAt)} old \xB7 last activity ${secs(now3 - j.lastActivityAt)} ago \xB7 ${j.bytes} byte(s)`;
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1514
1907
|
// src/NodeDiskFilesystem.ts
|
|
1515
1908
|
var NodeDiskFilesystem_exports = {};
|
|
1516
1909
|
__export(NodeDiskFilesystem_exports, {
|
|
@@ -1954,7 +2347,10 @@ __export(tools_shell_exports, {
|
|
|
1954
2347
|
ShellJobRegistry: () => ShellJobRegistry,
|
|
1955
2348
|
formatJobExit: () => formatJobExit,
|
|
1956
2349
|
makeRealShellTool: () => makeRealShellTool,
|
|
1957
|
-
makeShellJobTools: () => makeShellJobTools
|
|
2350
|
+
makeShellJobTools: () => makeShellJobTools,
|
|
2351
|
+
parsePsTime: () => parsePsTime,
|
|
2352
|
+
psGroupCpuSeconds: () => psGroupCpuSeconds,
|
|
2353
|
+
sumTreeCpu: () => sumTreeCpu
|
|
1958
2354
|
});
|
|
1959
2355
|
function killGroup(proc, signal) {
|
|
1960
2356
|
if (!proc?.pid) return false;
|
|
@@ -1984,10 +2380,39 @@ async function nodeSpawn() {
|
|
|
1984
2380
|
return _spawn;
|
|
1985
2381
|
}
|
|
1986
2382
|
function formatJobExit(n) {
|
|
1987
|
-
return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
|
|
2383
|
+
return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}${n.reason ? `: ${n.reason}` : ""}] \`${n.command}\`
|
|
1988
2384
|
` + (n.tail ? `${n.tail}
|
|
1989
2385
|
` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
|
|
1990
2386
|
}
|
|
2387
|
+
function parsePsTime(t) {
|
|
2388
|
+
const m = t.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/);
|
|
2389
|
+
if (!m) return void 0;
|
|
2390
|
+
return Number(m[1] ?? 0) * 86400 + Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
|
|
2391
|
+
}
|
|
2392
|
+
async function psGroupCpuSeconds(pgid) {
|
|
2393
|
+
try {
|
|
2394
|
+
const { execFile } = await import("child_process");
|
|
2395
|
+
const out = await new Promise((res, rej) => execFile("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
|
|
2396
|
+
return sumTreeCpu(out, pgid);
|
|
2397
|
+
} catch (e) {
|
|
2398
|
+
log8.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
|
|
2399
|
+
return void 0;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function sumTreeCpu(psOut, root) {
|
|
2403
|
+
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) }));
|
|
2404
|
+
const inJob = new Set(rows.filter((r) => r.pgid === root || r.pid === root).map((r) => r.pid));
|
|
2405
|
+
for (let grew = true; grew; ) {
|
|
2406
|
+
grew = false;
|
|
2407
|
+
for (const r of rows) if (!inJob.has(r.pid) && inJob.has(r.ppid)) {
|
|
2408
|
+
inJob.add(r.pid);
|
|
2409
|
+
grew = true;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
let total;
|
|
2413
|
+
for (const r of rows) if (inJob.has(r.pid) && r.sec != null) total = (total ?? 0) + r.sec;
|
|
2414
|
+
return total;
|
|
2415
|
+
}
|
|
1991
2416
|
function makeRealShellTool(options) {
|
|
1992
2417
|
const defaultTimeoutMs = options.timeoutMs ?? 12e4;
|
|
1993
2418
|
const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
|
|
@@ -2118,7 +2543,7 @@ function makeRealShellTool(options) {
|
|
|
2118
2543
|
proc.stderr?.on("data", collect);
|
|
2119
2544
|
proc.on("error", (err) => {
|
|
2120
2545
|
if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
2121
|
-
|
|
2546
|
+
log8.debug("shell spawn error", err);
|
|
2122
2547
|
finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
|
|
2123
2548
|
});
|
|
2124
2549
|
proc.on("close", (code) => {
|
|
@@ -2138,7 +2563,7 @@ function makeRealShellTool(options) {
|
|
|
2138
2563
|
};
|
|
2139
2564
|
}
|
|
2140
2565
|
function handoffFor(timeoutMs, id, body, notifies) {
|
|
2141
|
-
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.");
|
|
2566
|
+
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.");
|
|
2142
2567
|
return body ? `${head}
|
|
2143
2568
|
Output so far:
|
|
2144
2569
|
${body}` : head;
|
|
@@ -2162,7 +2587,7 @@ function makeShellJobTools(registry) {
|
|
|
2162
2587
|
const out = registry.output(String(id));
|
|
2163
2588
|
if (out == null) return NO_JOB2(String(id));
|
|
2164
2589
|
const st = registry.status(String(id));
|
|
2165
|
-
return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}]
|
|
2590
|
+
return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}${st.reason ? `: ${st.reason}` : ""}]
|
|
2166
2591
|
${clean(out) || "(no output yet)"}`;
|
|
2167
2592
|
}
|
|
2168
2593
|
},
|
|
@@ -2177,13 +2602,13 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
2177
2602
|
return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join("\n") : "(no background jobs)";
|
|
2178
2603
|
}
|
|
2179
2604
|
const st = registry.status(String(id));
|
|
2180
|
-
return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ""} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB2(String(id));
|
|
2605
|
+
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));
|
|
2181
2606
|
}
|
|
2182
2607
|
},
|
|
2183
2608
|
{
|
|
2184
2609
|
...dropOnRebind,
|
|
2185
2610
|
name: "ShellKill",
|
|
2186
|
-
description: "Stop a running background Shell job by id (SIGTERM).",
|
|
2611
|
+
description: "Stop a running background Shell job by id (SIGTERM to its process group, SIGKILL if it ignores that).",
|
|
2187
2612
|
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
2188
2613
|
async run({ id }) {
|
|
2189
2614
|
return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB2(String(id));
|
|
@@ -2191,7 +2616,7 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
2191
2616
|
}
|
|
2192
2617
|
];
|
|
2193
2618
|
}
|
|
2194
|
-
var
|
|
2619
|
+
var log8, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
|
|
2195
2620
|
var init_tools_shell = __esm({
|
|
2196
2621
|
"src/tools.shell.ts"() {
|
|
2197
2622
|
"use strict";
|
|
@@ -2199,62 +2624,94 @@ var init_tools_shell = __esm({
|
|
|
2199
2624
|
init_redact();
|
|
2200
2625
|
init_logging();
|
|
2201
2626
|
init_shell_sandbox();
|
|
2202
|
-
|
|
2627
|
+
init_tools_jobs();
|
|
2628
|
+
log8 = forComponent("shell");
|
|
2203
2629
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
2204
2630
|
DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
|
|
2205
2631
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
2206
2632
|
ShellJobRegistry = class {
|
|
2207
2633
|
constructor(cfg) {
|
|
2208
2634
|
this.cfg = cfg;
|
|
2635
|
+
this.jobs = cfg.jobs ?? new JobRegistry(cfg.maxBuffer ? { maxBuffer: cfg.maxBuffer } : {});
|
|
2209
2636
|
if (cfg.killOnExit && typeof process !== "undefined") process.once("exit", () => this.killAll());
|
|
2210
2637
|
}
|
|
2211
2638
|
cfg;
|
|
2212
|
-
jobs
|
|
2213
|
-
|
|
2639
|
+
jobs;
|
|
2640
|
+
meta = /* @__PURE__ */ new Map();
|
|
2214
2641
|
async start(command) {
|
|
2215
|
-
const
|
|
2216
|
-
const max = this.cfg.maxBuffer ?? 256 * 1024;
|
|
2217
|
-
const job = { command, buf: "", status: "running" };
|
|
2218
|
-
const append = (chunk) => {
|
|
2219
|
-
const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
|
|
2220
|
-
job.buf = (job.buf + s).slice(-max);
|
|
2221
|
-
};
|
|
2642
|
+
const h = this.track(command, { idleMs: 0 });
|
|
2222
2643
|
try {
|
|
2223
2644
|
const spawn = this.cfg.spawn ?? await nodeSpawn();
|
|
2224
2645
|
const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
|
|
2225
|
-
|
|
2226
|
-
job.proc = proc;
|
|
2227
|
-
proc.stdout?.on("data", append);
|
|
2228
|
-
proc.stderr?.on("data", append);
|
|
2229
|
-
proc.on("error", (err) => {
|
|
2230
|
-
if (job.status === "running") {
|
|
2231
|
-
job.status = "error";
|
|
2232
|
-
append(`
|
|
2233
|
-
[error] ${err?.message ?? err}`);
|
|
2234
|
-
this.notifyExit(id, job);
|
|
2235
|
-
}
|
|
2236
|
-
});
|
|
2237
|
-
proc.on("close", (code) => {
|
|
2238
|
-
if (job.status === "running") {
|
|
2239
|
-
job.status = "exited";
|
|
2240
|
-
job.exitCode = code ?? void 0;
|
|
2241
|
-
this.notifyExit(id, job);
|
|
2242
|
-
}
|
|
2243
|
-
});
|
|
2646
|
+
this.attach(h, spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED }));
|
|
2244
2647
|
} catch (e) {
|
|
2245
|
-
|
|
2246
|
-
|
|
2648
|
+
h.chunk(`failed to spawn: ${e?.message ?? e}`);
|
|
2649
|
+
h.finish("error", void 0, e?.message ?? String(e));
|
|
2650
|
+
}
|
|
2651
|
+
return h.id;
|
|
2652
|
+
}
|
|
2653
|
+
track(command, extra) {
|
|
2654
|
+
const meta = { command };
|
|
2655
|
+
let id = "";
|
|
2656
|
+
const h = this.jobs.track({
|
|
2657
|
+
kind: "shell",
|
|
2658
|
+
label: command,
|
|
2659
|
+
seed: extra.seed,
|
|
2660
|
+
idleMs: extra.idleMs,
|
|
2661
|
+
cpuTime: extra.cpu ? async () => meta.proc?.pid ? (this.cfg.cpuTime ?? psGroupCpuSeconds)(meta.proc.pid) : void 0 : void 0,
|
|
2662
|
+
onKill: () => this.signal(meta),
|
|
2663
|
+
onExit: (n) => this.notifyExit(id, meta, n.status, n.reason),
|
|
2664
|
+
notify: true
|
|
2665
|
+
});
|
|
2666
|
+
id = h.id;
|
|
2667
|
+
this.meta.set(id, meta);
|
|
2668
|
+
return h;
|
|
2669
|
+
}
|
|
2670
|
+
attach(h, proc) {
|
|
2671
|
+
const meta = this.meta.get(h.id);
|
|
2672
|
+
meta.proc = proc;
|
|
2673
|
+
const append = (chunk) => h.chunk(typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "");
|
|
2674
|
+
proc.stdout?.on("data", append);
|
|
2675
|
+
proc.stderr?.on("data", append);
|
|
2676
|
+
proc.on("error", (err) => {
|
|
2677
|
+
if (h.status === "running") {
|
|
2678
|
+
append(`
|
|
2679
|
+
[error] ${err?.message ?? err}`);
|
|
2680
|
+
h.finish("error", void 0, err?.message ?? String(err));
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
proc.on("close", (code) => {
|
|
2684
|
+
if (h.status === "running") {
|
|
2685
|
+
meta.exitCode = code ?? void 0;
|
|
2686
|
+
h.finish("done");
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
/** SIGTERM the whole group (bg children are detached group leaders — /bin/sh alone would orphan a forked
|
|
2691
|
+
* server), then SIGKILL after the grace period if it is still there. Falls back to the pid for fakes. */
|
|
2692
|
+
signal(meta) {
|
|
2693
|
+
const proc = meta.proc;
|
|
2694
|
+
if (!killGroup(proc, "SIGTERM")) {
|
|
2695
|
+
try {
|
|
2696
|
+
proc?.kill("SIGTERM");
|
|
2697
|
+
} catch {
|
|
2698
|
+
}
|
|
2247
2699
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
this.
|
|
2700
|
+
if (!proc?.pid) return;
|
|
2701
|
+
let closed = false;
|
|
2702
|
+
proc.on("close", () => {
|
|
2703
|
+
closed = true;
|
|
2704
|
+
});
|
|
2705
|
+
const t = setTimeout(() => {
|
|
2706
|
+
if (!closed) killGroup(proc, "SIGKILL");
|
|
2707
|
+
}, this.jobs.options.killGraceMs);
|
|
2708
|
+
t.unref?.();
|
|
2709
|
+
}
|
|
2710
|
+
/** 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. */
|
|
2711
|
+
notifyExit(id, meta, status, reason) {
|
|
2712
|
+
if (!this.cfg.onExit || status === "killed" && !reason?.startsWith("stuck")) return;
|
|
2256
2713
|
try {
|
|
2257
|
-
this.cfg.onExit({ id, command:
|
|
2714
|
+
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 } : {} });
|
|
2258
2715
|
} catch {
|
|
2259
2716
|
}
|
|
2260
2717
|
}
|
|
@@ -2273,14 +2730,20 @@ var init_tools_shell = __esm({
|
|
|
2273
2730
|
}
|
|
2274
2731
|
/** Current tail output for a job (null = no such job). */
|
|
2275
2732
|
output(id) {
|
|
2276
|
-
return this.
|
|
2733
|
+
return this.meta.has(id) ? this.jobs.output(id) : null;
|
|
2277
2734
|
}
|
|
2735
|
+
// null too once the shared registry pruned it
|
|
2278
2736
|
status(id) {
|
|
2279
|
-
const
|
|
2280
|
-
|
|
2737
|
+
const m = this.meta.get(id);
|
|
2738
|
+
const j = m && this.jobs.get(id);
|
|
2739
|
+
if (!m || !j) {
|
|
2740
|
+
this.meta.delete(id);
|
|
2741
|
+
return null;
|
|
2742
|
+
}
|
|
2743
|
+
return { status: shellStatus(j.status), exitCode: m.exitCode, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} };
|
|
2281
2744
|
}
|
|
2282
2745
|
list() {
|
|
2283
|
-
return [...this.
|
|
2746
|
+
return [...this.meta].filter(([id]) => this.jobs.get(id)).map(([id, m]) => ({ id, command: m.command, status: shellStatus(this.jobs.get(id).status) }));
|
|
2284
2747
|
}
|
|
2285
2748
|
/**
|
|
2286
2749
|
* Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its
|
|
@@ -2288,53 +2751,22 @@ var init_tools_shell = __esm({
|
|
|
2288
2751
|
* worse, a launcher that spawned its own detached worker leaves that worker running with nothing
|
|
2289
2752
|
* tracking it. Adopting hands the model a handle instead: the command keeps going, its completion
|
|
2290
2753
|
* is reported like any other job, and `seed` carries the output produced before the handoff.
|
|
2754
|
+
* Unlike an explicit background job it IS liveness-checked (idle window + CPU probe): nobody asked for
|
|
2755
|
+
* a long-lived process here, so one that goes silent and CPU-idle is stuck, not serving.
|
|
2291
2756
|
*/
|
|
2292
2757
|
adopt(command, proc, seed = "") {
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
const append = (chunk) => {
|
|
2297
|
-
const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
|
|
2298
|
-
job.buf = (job.buf + s).slice(-max);
|
|
2299
|
-
};
|
|
2300
|
-
proc.stdout?.on("data", append);
|
|
2301
|
-
proc.stderr?.on("data", append);
|
|
2302
|
-
proc.on("error", (err) => {
|
|
2303
|
-
if (job.status === "running") {
|
|
2304
|
-
job.status = "error";
|
|
2305
|
-
append(`
|
|
2306
|
-
[error] ${err?.message ?? err}`);
|
|
2307
|
-
this.notifyExit(id, job);
|
|
2308
|
-
}
|
|
2309
|
-
});
|
|
2310
|
-
proc.on("close", (code) => {
|
|
2311
|
-
if (job.status === "running") {
|
|
2312
|
-
job.status = "exited";
|
|
2313
|
-
job.exitCode = code ?? void 0;
|
|
2314
|
-
this.notifyExit(id, job);
|
|
2315
|
-
}
|
|
2316
|
-
});
|
|
2317
|
-
this.jobs.set(id, job);
|
|
2318
|
-
return id;
|
|
2758
|
+
const h = this.track(command, { seed, cpu: true });
|
|
2759
|
+
this.attach(h, proc);
|
|
2760
|
+
return h.id;
|
|
2319
2761
|
}
|
|
2320
|
-
kill(id) {
|
|
2321
|
-
|
|
2322
|
-
if (!j) return false;
|
|
2323
|
-
if (j.status === "running") {
|
|
2324
|
-
if (!killGroup(j.proc, "SIGTERM")) {
|
|
2325
|
-
try {
|
|
2326
|
-
j.proc?.kill("SIGTERM");
|
|
2327
|
-
} catch {
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
j.status = "killed";
|
|
2331
|
-
}
|
|
2332
|
-
return true;
|
|
2762
|
+
kill(id, reason = "killed by ShellKill") {
|
|
2763
|
+
return this.meta.has(id) && this.jobs.kill(id, reason);
|
|
2333
2764
|
}
|
|
2334
2765
|
killAll() {
|
|
2335
|
-
for (const id of this.
|
|
2766
|
+
for (const id of this.meta.keys()) this.jobs.kill(id, "agent exiting");
|
|
2336
2767
|
}
|
|
2337
2768
|
};
|
|
2769
|
+
shellStatus = (s) => s === "done" ? "exited" : s === "running" ? "running" : s === "killed" ? "killed" : "error";
|
|
2338
2770
|
NO_JOB2 = (id) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;
|
|
2339
2771
|
}
|
|
2340
2772
|
});
|
|
@@ -2342,111 +2774,7 @@ var init_tools_shell = __esm({
|
|
|
2342
2774
|
// src/Agent.ts
|
|
2343
2775
|
init_llm();
|
|
2344
2776
|
init_tools();
|
|
2345
|
-
|
|
2346
|
-
// src/tools.jobs.ts
|
|
2347
|
-
var SandboxJobRegistry = class {
|
|
2348
|
-
constructor(maxBuffer = 256 * 1024) {
|
|
2349
|
-
this.maxBuffer = maxBuffer;
|
|
2350
|
-
}
|
|
2351
|
-
maxBuffer;
|
|
2352
|
-
jobs = /* @__PURE__ */ new Map();
|
|
2353
|
-
seq = 0;
|
|
2354
|
-
/** Start `fn` in the background and return a job id immediately (does not await completion). */
|
|
2355
|
-
start(fn, opts = {}) {
|
|
2356
|
-
const id = `job-${++this.seq}`;
|
|
2357
|
-
const ctl = new AbortController();
|
|
2358
|
-
const onChunk = (s) => {
|
|
2359
|
-
job.buf = (job.buf + s).slice(-this.maxBuffer);
|
|
2360
|
-
};
|
|
2361
|
-
const job = { kind: opts.kind ?? "task", label: opts.label ?? "", status: "running", buf: "", ctl, promise: Promise.resolve() };
|
|
2362
|
-
job.promise = fn({ signal: ctl.signal, onChunk }).then(
|
|
2363
|
-
(res) => {
|
|
2364
|
-
if (job.status === "running") {
|
|
2365
|
-
job.status = "done";
|
|
2366
|
-
if (res) onChunk(res);
|
|
2367
|
-
}
|
|
2368
|
-
},
|
|
2369
|
-
(e) => {
|
|
2370
|
-
if (job.status === "running") {
|
|
2371
|
-
job.status = "error";
|
|
2372
|
-
onChunk(`
|
|
2373
|
-
[error] ${e?.message ?? e}`);
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
);
|
|
2377
|
-
this.jobs.set(id, job);
|
|
2378
|
-
return id;
|
|
2379
|
-
}
|
|
2380
|
-
/** Tail output for a job (null = no such job, '' = job exists but no output yet). */
|
|
2381
|
-
output(id) {
|
|
2382
|
-
return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
|
|
2383
|
-
}
|
|
2384
|
-
status(id) {
|
|
2385
|
-
const j = this.jobs.get(id);
|
|
2386
|
-
return j ? { status: j.status, bytes: j.buf.length } : null;
|
|
2387
|
-
}
|
|
2388
|
-
list() {
|
|
2389
|
-
return [...this.jobs].map(([id, j]) => ({ id, kind: j.kind, label: j.label, status: j.status }));
|
|
2390
|
-
}
|
|
2391
|
-
/** Signal cancel (the producer decides what abort means — e.g. skip the overlay commit). */
|
|
2392
|
-
kill(id) {
|
|
2393
|
-
const j = this.jobs.get(id);
|
|
2394
|
-
if (!j) return false;
|
|
2395
|
-
if (j.status === "running") {
|
|
2396
|
-
j.ctl.abort();
|
|
2397
|
-
j.status = "killed";
|
|
2398
|
-
}
|
|
2399
|
-
return true;
|
|
2400
|
-
}
|
|
2401
|
-
killAll() {
|
|
2402
|
-
for (const id of this.jobs.keys()) this.kill(id);
|
|
2403
|
-
}
|
|
2404
|
-
/** Await every still-running job. Called at turn end so a job's committed writes aren't lost from view
|
|
2405
|
-
* just because the model didn't poll it. Returns the ids that were still running when drain began. */
|
|
2406
|
-
async drain() {
|
|
2407
|
-
const pending = [...this.jobs].filter(([, j]) => j.status === "running").map(([id]) => id);
|
|
2408
|
-
await Promise.all([...this.jobs.values()].map((j) => j.promise));
|
|
2409
|
-
return pending;
|
|
2410
|
-
}
|
|
2411
|
-
};
|
|
2412
|
-
var NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs, or start one with bash({background:true}).`;
|
|
2413
|
-
function makeJobTools(registry) {
|
|
2414
|
-
return [
|
|
2415
|
-
{
|
|
2416
|
-
name: "JobOutput",
|
|
2417
|
-
description: "Read the accumulated output (tail) of a background job by id (from bash({background:true})).",
|
|
2418
|
-
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
2419
|
-
async run({ id }) {
|
|
2420
|
-
const out = registry.output(String(id));
|
|
2421
|
-
if (out == null) return NO_JOB(String(id));
|
|
2422
|
-
const st = registry.status(String(id));
|
|
2423
|
-
return `[${st.status}]
|
|
2424
|
-
${out.replace(/\n+$/, "") || "(no output yet)"}`;
|
|
2425
|
-
}
|
|
2426
|
-
},
|
|
2427
|
-
{
|
|
2428
|
-
name: "JobStatus",
|
|
2429
|
-
description: "Status of a background job (running/done/killed/error). Omit `id` to list all jobs.",
|
|
2430
|
-
parameters: { type: "object", properties: { id: { type: "string" } } },
|
|
2431
|
-
async run({ id }) {
|
|
2432
|
-
if (!id) {
|
|
2433
|
-
const jobs = registry.list();
|
|
2434
|
-
return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.kind}${j.label ? " " + j.label : ""}`).join("\n") : "(no background jobs)";
|
|
2435
|
-
}
|
|
2436
|
-
const st = registry.status(String(id));
|
|
2437
|
-
return st ? `${st.status} \xB7 ${st.bytes} byte(s) buffered` : NO_JOB(String(id));
|
|
2438
|
-
}
|
|
2439
|
-
},
|
|
2440
|
-
{
|
|
2441
|
-
name: "JobKill",
|
|
2442
|
-
description: "Stop a running background job by id (the job is cancelled; its overlay writes are NOT committed).",
|
|
2443
|
-
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
2444
|
-
async run({ id }) {
|
|
2445
|
-
return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
];
|
|
2449
|
-
}
|
|
2777
|
+
init_tools_jobs();
|
|
2450
2778
|
|
|
2451
2779
|
// src/host.ts
|
|
2452
2780
|
var askUserQuestionTool = {
|
|
@@ -3229,7 +3557,7 @@ function exitWorktree() {
|
|
|
3229
3557
|
// src/subagent.ts
|
|
3230
3558
|
init_NodeDiskFilesystem();
|
|
3231
3559
|
init_logging();
|
|
3232
|
-
var
|
|
3560
|
+
var log6 = forComponent("subagent");
|
|
3233
3561
|
async function boundedPool(items, limit, fn) {
|
|
3234
3562
|
const out = new Array(items.length);
|
|
3235
3563
|
let next = 0;
|
|
@@ -3303,7 +3631,7 @@ function traceFileFor(dir, label, depth) {
|
|
|
3303
3631
|
const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
|
|
3304
3632
|
return join3(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
|
|
3305
3633
|
} catch (e) {
|
|
3306
|
-
|
|
3634
|
+
log6.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
|
|
3307
3635
|
return void 0;
|
|
3308
3636
|
}
|
|
3309
3637
|
}
|
|
@@ -3311,7 +3639,7 @@ function flushTrace(file, meta, messages, status) {
|
|
|
3311
3639
|
try {
|
|
3312
3640
|
writeFileSync(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
|
|
3313
3641
|
} catch (e) {
|
|
3314
|
-
|
|
3642
|
+
log6.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
|
|
3315
3643
|
}
|
|
3316
3644
|
}
|
|
3317
3645
|
function installTrace(child, file, meta) {
|
|
@@ -3352,7 +3680,7 @@ async function runChildTraced(args) {
|
|
|
3352
3680
|
if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
|
|
3353
3681
|
let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
|
|
3354
3682
|
if (wc.timedOut()) {
|
|
3355
|
-
|
|
3683
|
+
log6.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
|
|
3356
3684
|
result += deadlineNote(deadlineMs);
|
|
3357
3685
|
}
|
|
3358
3686
|
await opts.hooks?.onSubagentStop?.(result, { label, agentType });
|
|
@@ -3806,7 +4134,7 @@ function reasoningToChatFragment(model, effort) {
|
|
|
3806
4134
|
}
|
|
3807
4135
|
|
|
3808
4136
|
// src/Agent.ts
|
|
3809
|
-
var
|
|
4137
|
+
var log7 = forComponent("Agent");
|
|
3810
4138
|
function envInt(key, fallback) {
|
|
3811
4139
|
const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
|
|
3812
4140
|
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
@@ -3958,6 +4286,12 @@ var AgentOptions = class {
|
|
|
3958
4286
|
/** Enable `bash({background:true})` + JobOutput/JobStatus/JobKill — sandbox background jobs (overlay-isolated,
|
|
3959
4287
|
* committed on completion, drained at turn end). Useful when the VFS backend is slow (remote) or for sub-agents. */
|
|
3960
4288
|
backgroundJobs = false;
|
|
4289
|
+
/** The job registry every long-lived tool call lives in (MCP calls past their soft timeout, delegated host/native
|
|
4290
|
+
* tools, and — when the host shares it with its `ShellJobRegistry` — shell jobs). Its liveness is what the stall
|
|
4291
|
+
* watchdog and the stuck-job reaper decide from. Default: a fresh registry per Agent. Knobs (idle windows, hard
|
|
4292
|
+
* caps, MCP soft timeout) live on `JobRegistryOptions`. Completion notices reach the model via `inject` unless
|
|
4293
|
+
* the host set its own `onExit`. */
|
|
4294
|
+
jobs;
|
|
3961
4295
|
/** Plan mode: block mutating tools until the agent calls `ExitPlanMode` (host-approved). */
|
|
3962
4296
|
planMode = false;
|
|
3963
4297
|
/** Permission policy gating each tool call (allow / ask / deny). */
|
|
@@ -4024,12 +4358,18 @@ var Agent = class _Agent {
|
|
|
4024
4358
|
/** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
|
|
4025
4359
|
* runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
|
|
4026
4360
|
invokedTools = /* @__PURE__ */ new Set();
|
|
4027
|
-
/**
|
|
4028
|
-
*
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
*
|
|
4032
|
-
|
|
4361
|
+
/** Every long-lived tool call is a job here (see AgentOptions.jobs). Delegated-runtime HOST tools (cursor
|
|
4362
|
+
* toolExecutor) and NATIVE tools (cursor's own shell/mcp/task, `running` → `completed`) are tracked as
|
|
4363
|
+
* `holds` jobs: while one runs the provider is not silent — it is waiting on a tool — so the idle-stall
|
|
4364
|
+
* watchdog must not fire. Each is bounded by the transport tool deadline (`hardCapMs = toolHoldMs`), so a
|
|
4365
|
+
* tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection. */
|
|
4366
|
+
jobs;
|
|
4367
|
+
/** Native delegated tool activity id → its job (per step attempt). */
|
|
4368
|
+
nativeJobs = /* @__PURE__ */ new Map();
|
|
4369
|
+
toolHoldMs = 0;
|
|
4370
|
+
/** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
|
|
4371
|
+
* from the pre-step transcript, which would re-run it — so such a stall is not retried. */
|
|
4372
|
+
toolRanThisAttempt = false;
|
|
4033
4373
|
/** Resets the active request's stall timer; a host tool's completion restarts the model's idle window. */
|
|
4034
4374
|
stallPoke;
|
|
4035
4375
|
/** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
|
|
@@ -4074,6 +4414,7 @@ var Agent = class _Agent {
|
|
|
4074
4414
|
addTools(tools) {
|
|
4075
4415
|
this.injectedTools.push(...tools);
|
|
4076
4416
|
this.activeTools.push(...tools);
|
|
4417
|
+
if (this.prepared && tools.some(isMcpTool) && !this.activeTools.some((t) => t.name === "JobWait")) this.activeTools.push(...makeJobTools(this.jobs));
|
|
4077
4418
|
}
|
|
4078
4419
|
/** Remove tools by name from a running agent. Returns the count removed. Also filters
|
|
4079
4420
|
* `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
|
|
@@ -4125,6 +4466,11 @@ var Agent = class _Agent {
|
|
|
4125
4466
|
}
|
|
4126
4467
|
constructor(options) {
|
|
4127
4468
|
this.options = { ...new AgentOptions(), ...options };
|
|
4469
|
+
this.jobs = this.options.jobs ?? new JobRegistry();
|
|
4470
|
+
this.jobs.options.onExit ??= (n) => {
|
|
4471
|
+
const imgs = n.result?.images;
|
|
4472
|
+
this.inject(imgs?.length ? [{ type: "text", text: formatJobNotice(n) }, ...imgs.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : formatJobNotice(n));
|
|
4473
|
+
};
|
|
4128
4474
|
if (this.options.fs) this.buildCtx();
|
|
4129
4475
|
}
|
|
4130
4476
|
/** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
|
|
@@ -4136,7 +4482,8 @@ var Agent = class _Agent {
|
|
|
4136
4482
|
this.ctx.ai = this.options.ai;
|
|
4137
4483
|
this.ctx.model = this.options.model;
|
|
4138
4484
|
this.ctx.parkHuman = (p) => this.park(p);
|
|
4139
|
-
|
|
4485
|
+
this.ctx.toolJobs = this.jobs;
|
|
4486
|
+
if (this.options.backgroundJobs) this.ctx.jobs = this.jobs;
|
|
4140
4487
|
}
|
|
4141
4488
|
/**
|
|
4142
4489
|
* Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
|
|
@@ -4152,7 +4499,7 @@ var Agent = class _Agent {
|
|
|
4152
4499
|
const disk = new NodeDiskFilesystem2(process.cwd());
|
|
4153
4500
|
await disk.init();
|
|
4154
4501
|
this.options.fs = new JailedFilesystem2(disk);
|
|
4155
|
-
|
|
4502
|
+
log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
|
|
4156
4503
|
}
|
|
4157
4504
|
this.buildCtx();
|
|
4158
4505
|
}
|
|
@@ -4222,7 +4569,7 @@ var Agent = class _Agent {
|
|
|
4222
4569
|
tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
|
|
4223
4570
|
}
|
|
4224
4571
|
if (o.checkpoints) tools = [...tools, ...checkpointTools()];
|
|
4225
|
-
if (this.ctx.jobs) tools = [...tools, ...makeJobTools(this.
|
|
4572
|
+
if (this.ctx.jobs || [...tools, ...this.injectedTools].some(isMcpTool)) tools = [...tools, ...makeJobTools(this.jobs)];
|
|
4226
4573
|
const plan = o.planMode ? planMode({ host: o.host }) : void 0;
|
|
4227
4574
|
if (plan) tools = [...tools, plan.tool];
|
|
4228
4575
|
this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
|
|
@@ -4338,8 +4685,8 @@ var Agent = class _Agent {
|
|
|
4338
4685
|
let closureNudged = false;
|
|
4339
4686
|
let lastProgressAt = start;
|
|
4340
4687
|
const kill = (finishReason) => {
|
|
4341
|
-
|
|
4342
|
-
this.
|
|
4688
|
+
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` : ""})`);
|
|
4689
|
+
this.jobs.killAll(`run ended: ${finishReason}`, true);
|
|
4343
4690
|
const claims = this.survivingClaims();
|
|
4344
4691
|
return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
|
|
4345
4692
|
};
|
|
@@ -4360,22 +4707,27 @@ var Agent = class _Agent {
|
|
|
4360
4707
|
const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
|
|
4361
4708
|
const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
|
|
4362
4709
|
const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
|
|
4710
|
+
this.toolHoldMs = effectiveToolTimeoutMs;
|
|
4363
4711
|
if (isCursorWithTools) {
|
|
4364
4712
|
if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
|
|
4365
|
-
|
|
4713
|
+
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.`);
|
|
4366
4714
|
}
|
|
4367
|
-
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs))
|
|
4715
|
+
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
|
|
4368
4716
|
}
|
|
4369
4717
|
const cursorPo = isCursorWithTools ? {
|
|
4370
4718
|
toolTimeoutMs: effectiveToolTimeoutMs,
|
|
4371
4719
|
toolExecutor: async (name, args) => {
|
|
4372
4720
|
const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
|
|
4373
|
-
this.
|
|
4374
|
-
this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
|
|
4721
|
+
const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
|
|
4375
4722
|
try {
|
|
4376
|
-
|
|
4723
|
+
const r = await this.dispatch(tc);
|
|
4724
|
+
job.finish("done");
|
|
4725
|
+
return r;
|
|
4726
|
+
} catch (e) {
|
|
4727
|
+
job.finish("error", void 0, String(e));
|
|
4728
|
+
throw e;
|
|
4377
4729
|
} finally {
|
|
4378
|
-
this.
|
|
4730
|
+
this.toolRanThisAttempt = true;
|
|
4379
4731
|
this.stallPoke?.();
|
|
4380
4732
|
}
|
|
4381
4733
|
}
|
|
@@ -4388,6 +4740,9 @@ var Agent = class _Agent {
|
|
|
4388
4740
|
};
|
|
4389
4741
|
try {
|
|
4390
4742
|
for (let attempt = 0; ; attempt++) {
|
|
4743
|
+
for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
|
|
4744
|
+
this.nativeJobs.clear();
|
|
4745
|
+
this.toolRanThisAttempt = false;
|
|
4391
4746
|
try {
|
|
4392
4747
|
if (useStream) {
|
|
4393
4748
|
const stall = this.armStallWatchdog(o.signal, o.stallMs);
|
|
@@ -4417,10 +4772,12 @@ var Agent = class _Agent {
|
|
|
4417
4772
|
const stalled = err?.code === "stall";
|
|
4418
4773
|
const empty = err?.code === "empty" || err?.code === "cursor_silent";
|
|
4419
4774
|
const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
|
|
4420
|
-
const
|
|
4775
|
+
const replaysTool = stalled && this.toolRanThisAttempt;
|
|
4776
|
+
if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
|
|
4777
|
+
const transient = !o.signal?.aborted && !isAbortError(err) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
|
|
4421
4778
|
if (!transient) throw err;
|
|
4422
4779
|
const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
|
|
4423
|
-
|
|
4780
|
+
log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
|
|
4424
4781
|
o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
|
|
4425
4782
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
4426
4783
|
}
|
|
@@ -4428,12 +4785,12 @@ var Agent = class _Agent {
|
|
|
4428
4785
|
} catch (err) {
|
|
4429
4786
|
if (err?.code === "budget") return kill("budget");
|
|
4430
4787
|
if (err?.code === "stall") {
|
|
4431
|
-
|
|
4788
|
+
log7.warn(`model stalled out after retries: ${err?.message ?? err}`);
|
|
4432
4789
|
return { ...kill("timeout"), error: err };
|
|
4433
4790
|
}
|
|
4434
4791
|
if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
|
|
4435
4792
|
if (err?.code === "empty" || err?.code === "cursor_silent") {
|
|
4436
|
-
|
|
4793
|
+
log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
|
|
4437
4794
|
return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err };
|
|
4438
4795
|
}
|
|
4439
4796
|
const body = err?.body ?? err?.response?.data ?? err?.error;
|
|
@@ -4445,7 +4802,7 @@ var Agent = class _Agent {
|
|
|
4445
4802
|
}
|
|
4446
4803
|
if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
|
|
4447
4804
|
const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
|
|
4448
|
-
|
|
4805
|
+
log7.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
|
|
4449
4806
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
|
|
4450
4807
|
}
|
|
4451
4808
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -4501,21 +4858,21 @@ var Agent = class _Agent {
|
|
|
4501
4858
|
if (emptyTurn && o.failEmptyTurns) {
|
|
4502
4859
|
if (!emptyNudged && canNudge()) {
|
|
4503
4860
|
emptyNudged = true;
|
|
4504
|
-
|
|
4861
|
+
log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
|
|
4505
4862
|
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." });
|
|
4506
4863
|
continue;
|
|
4507
4864
|
}
|
|
4508
|
-
|
|
4865
|
+
log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
|
|
4509
4866
|
const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
|
|
4510
4867
|
e.code = "empty";
|
|
4511
4868
|
return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
|
|
4512
4869
|
}
|
|
4513
4870
|
const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
|
|
4514
4871
|
for (const n of fabricated) this.fabricatedClaims.add(n);
|
|
4515
|
-
if (fabricated.length && !canNudge())
|
|
4872
|
+
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`);
|
|
4516
4873
|
if (fabricated.length && canNudge()) {
|
|
4517
4874
|
closureNudged = true;
|
|
4518
|
-
|
|
4875
|
+
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`);
|
|
4519
4876
|
this.transcript.push({
|
|
4520
4877
|
role: "user",
|
|
4521
4878
|
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.`
|
|
@@ -4524,15 +4881,18 @@ var Agent = class _Agent {
|
|
|
4524
4881
|
}
|
|
4525
4882
|
if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
|
|
4526
4883
|
closureNudged = true;
|
|
4527
|
-
|
|
4884
|
+
log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
|
|
4528
4885
|
this.transcript.push({
|
|
4529
4886
|
role: "user",
|
|
4530
4887
|
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."
|
|
4531
4888
|
});
|
|
4532
4889
|
continue;
|
|
4533
4890
|
}
|
|
4534
|
-
|
|
4535
|
-
await this.
|
|
4891
|
+
log7.verbose(`completed in ${steps} step(s)`);
|
|
4892
|
+
const drained = await this.jobs.drain(o.signal);
|
|
4893
|
+
if (o.signal?.aborted) return kill("aborted");
|
|
4894
|
+
if (drained.length) lastProgressAt = Date.now();
|
|
4895
|
+
if (this.drainInjections()) continue;
|
|
4536
4896
|
this.releaseStaleImages();
|
|
4537
4897
|
await this.activeHooks?.onStop?.(res.content ?? "");
|
|
4538
4898
|
const claims = this.survivingClaims();
|
|
@@ -4558,11 +4918,12 @@ var Agent = class _Agent {
|
|
|
4558
4918
|
}
|
|
4559
4919
|
this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
|
|
4560
4920
|
}
|
|
4921
|
+
lastProgressAt = Date.now();
|
|
4561
4922
|
if (this.ctx.needsInput) {
|
|
4562
4923
|
const question = this.ctx.needsInput;
|
|
4563
4924
|
this.ctx.needsInput = void 0;
|
|
4564
|
-
|
|
4565
|
-
await this.
|
|
4925
|
+
log7.warn(`needs_input: ${question.question} (steps=${steps})`);
|
|
4926
|
+
await this.jobs.drain();
|
|
4566
4927
|
const text = [
|
|
4567
4928
|
`OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
|
|
4568
4929
|
...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
|
|
@@ -4600,7 +4961,9 @@ var Agent = class _Agent {
|
|
|
4600
4961
|
if (!stallMs) return;
|
|
4601
4962
|
if (timer) clearTimeout(timer);
|
|
4602
4963
|
timer = setTimeout(() => {
|
|
4603
|
-
if (this.
|
|
4964
|
+
if (this.toolHoldActive()) return poke();
|
|
4965
|
+
const inFlight = this.jobs.list().filter((j) => j.status === "running");
|
|
4966
|
+
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(", ")})` : ""));
|
|
4604
4967
|
fired = true;
|
|
4605
4968
|
ac.abort(stallError());
|
|
4606
4969
|
}, stallMs);
|
|
@@ -4620,6 +4983,12 @@ var Agent = class _Agent {
|
|
|
4620
4983
|
reclassify: (err) => stalled() ? stallError() : err
|
|
4621
4984
|
};
|
|
4622
4985
|
}
|
|
4986
|
+
/** A host or native delegated tool is running and not stuck — the provider is quiet, not stalled. The reap runs
|
|
4987
|
+
* FIRST, so a holder past its deadline is released (logged with evidence) instead of holding forever. */
|
|
4988
|
+
toolHoldActive() {
|
|
4989
|
+
this.jobs.reap();
|
|
4990
|
+
return this.jobs.holding().length > 0;
|
|
4991
|
+
}
|
|
4623
4992
|
/** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
|
|
4624
4993
|
* non-result the model produced by going silent. Callers retry it rather than report "done". */
|
|
4625
4994
|
isEmptyStop(res) {
|
|
@@ -4658,6 +5027,18 @@ var Agent = class _Agent {
|
|
|
4658
5027
|
if (a.output !== void 0) prev.output = a.output;
|
|
4659
5028
|
prev.status = a.status;
|
|
4660
5029
|
}
|
|
5030
|
+
const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
|
|
5031
|
+
if (a.status === "running") {
|
|
5032
|
+
if (nj) nj.touch();
|
|
5033
|
+
else if (a.id) this.nativeJobs.set(a.id, this.jobs.track({ kind: "native", label: a.name, holds: true, hardCapMs: this.toolHoldMs }));
|
|
5034
|
+
} else {
|
|
5035
|
+
if (nj) {
|
|
5036
|
+
if (typeof a.output === "string") nj.chunk(a.output.slice(-4e3));
|
|
5037
|
+
nj.finish(a.status === "error" ? "error" : "done");
|
|
5038
|
+
this.nativeJobs.delete(a.id);
|
|
5039
|
+
}
|
|
5040
|
+
this.toolRanThisAttempt = true;
|
|
5041
|
+
}
|
|
4661
5042
|
sawTool = true;
|
|
4662
5043
|
this.invokedTools.add(a.name);
|
|
4663
5044
|
trailing = "";
|
|
@@ -4746,13 +5127,13 @@ var Agent = class _Agent {
|
|
|
4746
5127
|
const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
|
|
4747
5128
|
if (decision?.block) {
|
|
4748
5129
|
const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
|
|
4749
|
-
|
|
5130
|
+
log7.debug(`${tc.function.name} -> ${blocked}`);
|
|
4750
5131
|
await hooks?.postToolUse?.(call, blocked, meta);
|
|
4751
5132
|
return blocked;
|
|
4752
5133
|
}
|
|
4753
5134
|
this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
|
|
4754
5135
|
if (earlyError) {
|
|
4755
|
-
|
|
5136
|
+
log7.debug(`${tc.function.name} -> ${earlyError}`);
|
|
4756
5137
|
await hooks?.postToolUse?.(call, earlyError, meta);
|
|
4757
5138
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
|
|
4758
5139
|
return earlyError;
|
|
@@ -4761,12 +5142,12 @@ var Agent = class _Agent {
|
|
|
4761
5142
|
let images;
|
|
4762
5143
|
let threw = false;
|
|
4763
5144
|
try {
|
|
4764
|
-
|
|
5145
|
+
log7.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
4765
5146
|
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
4766
5147
|
try {
|
|
4767
5148
|
hooks.onToolOutput(call, chunk, meta);
|
|
4768
5149
|
} catch (e) {
|
|
4769
|
-
|
|
5150
|
+
log7.debug(`onToolOutput hook error: ${e}`);
|
|
4770
5151
|
}
|
|
4771
5152
|
} : void 0;
|
|
4772
5153
|
const raw = await tool.run(args, this.ctx);
|
|
@@ -4778,7 +5159,7 @@ var Agent = class _Agent {
|
|
|
4778
5159
|
}
|
|
4779
5160
|
} catch (e) {
|
|
4780
5161
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4781
|
-
|
|
5162
|
+
log7.debug(`${tc.function.name} -> error: ${msg}`);
|
|
4782
5163
|
result = `Error: ${msg}`;
|
|
4783
5164
|
threw = true;
|
|
4784
5165
|
} finally {
|
|
@@ -4870,6 +5251,7 @@ function estimateTokens(m, keepRecentImages = 1) {
|
|
|
4870
5251
|
return Math.ceil(sendBytes(m, keepRecentImages) / 4);
|
|
4871
5252
|
}
|
|
4872
5253
|
var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
|
|
5254
|
+
var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
|
|
4873
5255
|
function transportHeadroomMs(t) {
|
|
4874
5256
|
const d = Number(t.maxDurationMs) || 0;
|
|
4875
5257
|
if (!d) return 0;
|
|
@@ -5045,7 +5427,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
|
|
|
5045
5427
|
const ids = callIdSet(messages.slice(from));
|
|
5046
5428
|
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
5047
5429
|
if (total > maxTokens)
|
|
5048
|
-
|
|
5430
|
+
log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
5049
5431
|
return [...head, ...messages.slice(from)];
|
|
5050
5432
|
}
|
|
5051
5433
|
function compact(m, max, focus) {
|
|
@@ -5441,6 +5823,7 @@ var toolCall = (id, name, args) => ({
|
|
|
5441
5823
|
|
|
5442
5824
|
// src/index.ts
|
|
5443
5825
|
init_tools();
|
|
5826
|
+
init_tools_jobs();
|
|
5444
5827
|
|
|
5445
5828
|
// src/scheduler.ts
|
|
5446
5829
|
function parseCronField(field, min, max) {
|
|
@@ -5715,7 +6098,7 @@ async function fullAgentOptions(opts = {}) {
|
|
|
5715
6098
|
const registry = jobs ?? new ShellJobRegistry2({ cwd, killOnExit: true });
|
|
5716
6099
|
if (onJobExit) registry.setOnExit((n) => onJobExit(formatJobExit2(n)));
|
|
5717
6100
|
const shell = [makeRealShellTool2({ cwd, registry }), ...makeShellJobTools2(registry)];
|
|
5718
|
-
return { ...rest, tools: [...tools ?? defaultTools(), ...shell] };
|
|
6101
|
+
return { ...rest, jobs: registry.jobs, tools: [...tools ?? defaultTools(), ...shell] };
|
|
5719
6102
|
}
|
|
5720
6103
|
|
|
5721
6104
|
// src/index.ts
|
|
@@ -5727,7 +6110,7 @@ init_tools_web();
|
|
|
5727
6110
|
init_tools();
|
|
5728
6111
|
init_tools_structured();
|
|
5729
6112
|
init_logging();
|
|
5730
|
-
var
|
|
6113
|
+
var log9 = forComponent("scratch");
|
|
5731
6114
|
var SCRATCH_DIR = "/scratch";
|
|
5732
6115
|
function shortArgs(args) {
|
|
5733
6116
|
try {
|
|
@@ -5767,7 +6150,7 @@ var Scratch = class {
|
|
|
5767
6150
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5768
6151
|
await this.fs.writeFile(path, header + raw);
|
|
5769
6152
|
} catch (e) {
|
|
5770
|
-
|
|
6153
|
+
log9.debug("scratch write failed; returning raw", e);
|
|
5771
6154
|
return raw;
|
|
5772
6155
|
}
|
|
5773
6156
|
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
@@ -5797,7 +6180,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
|
|
|
5797
6180
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5798
6181
|
await this.fs.writeFile(path, header + full);
|
|
5799
6182
|
} catch (e) {
|
|
5800
|
-
|
|
6183
|
+
log9.debug("scratch spill failed; cropping lossy", e);
|
|
5801
6184
|
return full.slice(0, pageBytes) + `
|
|
5802
6185
|
|
|
5803
6186
|
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
@@ -5843,7 +6226,7 @@ Question: ${q}`);
|
|
|
5843
6226
|
const answer = (res.text ?? "").trim();
|
|
5844
6227
|
return answer || "(no answer found in scratch)";
|
|
5845
6228
|
} catch (e) {
|
|
5846
|
-
|
|
6229
|
+
log9.debug("Ask peek failed", e);
|
|
5847
6230
|
return `Error querying scratch: ${e?.message ?? e}`;
|
|
5848
6231
|
}
|
|
5849
6232
|
}
|
|
@@ -5852,7 +6235,7 @@ Question: ${q}`);
|
|
|
5852
6235
|
|
|
5853
6236
|
// src/lessons.ts
|
|
5854
6237
|
init_logging();
|
|
5855
|
-
var
|
|
6238
|
+
var log10 = forComponent("Lessons");
|
|
5856
6239
|
var LessonOptionsDefaults = class {
|
|
5857
6240
|
minRepeats = 2;
|
|
5858
6241
|
};
|
|
@@ -5877,8 +6260,8 @@ function lessonCapture(options) {
|
|
|
5877
6260
|
counts.set(lesson.slug, n);
|
|
5878
6261
|
if (n < o.minRepeats) return;
|
|
5879
6262
|
written.add(lesson.slug);
|
|
5880
|
-
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) =>
|
|
5881
|
-
|
|
6263
|
+
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log10.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
|
|
6264
|
+
log10.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
|
|
5882
6265
|
}
|
|
5883
6266
|
};
|
|
5884
6267
|
}
|
|
@@ -5886,7 +6269,7 @@ function lessonCapture(options) {
|
|
|
5886
6269
|
// src/reflect.ts
|
|
5887
6270
|
init_llm();
|
|
5888
6271
|
init_logging();
|
|
5889
|
-
var
|
|
6272
|
+
var log11 = forComponent("Reflect");
|
|
5890
6273
|
async function reflectOnRun(o) {
|
|
5891
6274
|
const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
|
|
5892
6275
|
if (!digest.trim()) return null;
|
|
@@ -5902,7 +6285,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5902
6285
|
const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
|
|
5903
6286
|
text = r?.content ?? "";
|
|
5904
6287
|
} catch (e) {
|
|
5905
|
-
|
|
6288
|
+
log11.warn(`reflection call failed: ${e?.message ?? e}`);
|
|
5906
6289
|
return null;
|
|
5907
6290
|
}
|
|
5908
6291
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
@@ -5912,10 +6295,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5912
6295
|
try {
|
|
5913
6296
|
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
5914
6297
|
} catch (e) {
|
|
5915
|
-
|
|
6298
|
+
log11.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
5916
6299
|
return null;
|
|
5917
6300
|
}
|
|
5918
|
-
|
|
6301
|
+
log11.debug(`reflection persisted ${slug2}`);
|
|
5919
6302
|
return slug2;
|
|
5920
6303
|
}
|
|
5921
6304
|
function digestRun(messages, maxChars) {
|
|
@@ -5964,7 +6347,7 @@ function forComponent2(name) {
|
|
|
5964
6347
|
}
|
|
5965
6348
|
|
|
5966
6349
|
// node_modules/@bod.ee/voice/src/core/emotion.ts
|
|
5967
|
-
var
|
|
6350
|
+
var log12 = forComponent2("Emotion");
|
|
5968
6351
|
var EMOTIONS = [
|
|
5969
6352
|
// primary (best results)
|
|
5970
6353
|
"neutral",
|
|
@@ -6118,7 +6501,7 @@ var EmotionStream = class {
|
|
|
6118
6501
|
if (tag) {
|
|
6119
6502
|
this.pending = tag;
|
|
6120
6503
|
if (this.show) display += m[0];
|
|
6121
|
-
} else
|
|
6504
|
+
} else log12.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
6122
6505
|
last = m.index + m[0].length;
|
|
6123
6506
|
}
|
|
6124
6507
|
this.emit(body.slice(last), (s, d, p) => {
|
|
@@ -6141,7 +6524,7 @@ var EmotionStream = class {
|
|
|
6141
6524
|
};
|
|
6142
6525
|
|
|
6143
6526
|
// node_modules/@bod.ee/voice/src/core/engine.ts
|
|
6144
|
-
var
|
|
6527
|
+
var log13 = forComponent2("VoiceEngine");
|
|
6145
6528
|
var realClock = {
|
|
6146
6529
|
now: () => performance.now(),
|
|
6147
6530
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -6416,7 +6799,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6416
6799
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
6417
6800
|
this.tts.warmup?.();
|
|
6418
6801
|
this.setState("listening");
|
|
6419
|
-
|
|
6802
|
+
log13.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
6420
6803
|
}
|
|
6421
6804
|
get usingAec() {
|
|
6422
6805
|
return this.stt.usingAec;
|
|
@@ -6438,7 +6821,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6438
6821
|
this.options.onDiag({ t: this.clock.now(), kind, ...fields });
|
|
6439
6822
|
} catch (e) {
|
|
6440
6823
|
this.diagOn = false;
|
|
6441
|
-
|
|
6824
|
+
log13.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
6442
6825
|
}
|
|
6443
6826
|
}
|
|
6444
6827
|
idleWaiters = [];
|
|
@@ -6547,7 +6930,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6547
6930
|
if (prose.trim()) this.repliedSinceDispatch = true;
|
|
6548
6931
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
6549
6932
|
this.tts.speak(forSpeech(speech), true);
|
|
6550
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
6933
|
+
if (!this.spokeDeltas && this.turnStartAt) log13.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
|
|
6551
6934
|
this.spokeDeltas = true;
|
|
6552
6935
|
this.setState("speaking");
|
|
6553
6936
|
if (this.options.revealMode !== "off" && display) {
|
|
@@ -6624,7 +7007,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6624
7007
|
this.drainTimer = null;
|
|
6625
7008
|
this.speaking = false;
|
|
6626
7009
|
this.stopWordReveal(true);
|
|
6627
|
-
if (this.turnStartAt)
|
|
7010
|
+
if (this.turnStartAt) log13.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
|
|
6628
7011
|
this.echoUntil = this.clock.now() + 2500;
|
|
6629
7012
|
if (!this.usingAec) this.stt.reset();
|
|
6630
7013
|
this.setState("listening");
|
|
@@ -6712,7 +7095,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6712
7095
|
this.clearAckTimer("interrupt");
|
|
6713
7096
|
this.adaptiveAckFired = false;
|
|
6714
7097
|
this.ackProbe = null;
|
|
6715
|
-
if (this.uttQueue.length)
|
|
7098
|
+
if (this.uttQueue.length) log13.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
|
|
6716
7099
|
const droppedQueued = this.uttQueue.length;
|
|
6717
7100
|
this.uttQueue = [];
|
|
6718
7101
|
if (!this.speaking && !this.drainTimer) return;
|
|
@@ -6858,7 +7241,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6858
7241
|
if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
|
|
6859
7242
|
this.specSpent = true;
|
|
6860
7243
|
this.specText = this.specPartial;
|
|
6861
|
-
|
|
7244
|
+
log13.debug(`speculate: "${this.specText.slice(0, 60)}"`);
|
|
6862
7245
|
this.diag("speculate", { text: this.specText });
|
|
6863
7246
|
this.options.onSpeculate(this.specText);
|
|
6864
7247
|
}, this.options.speculativeMs);
|
|
@@ -6927,7 +7310,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6927
7310
|
};
|
|
6928
7311
|
this.tts.newContext();
|
|
6929
7312
|
this.tts.speak(phrase, false);
|
|
6930
|
-
|
|
7313
|
+
log13.debug(`backchannel: "${phrase}"`);
|
|
6931
7314
|
this.diag("backchannel", { phrase });
|
|
6932
7315
|
this.options.onBackchannel(phrase);
|
|
6933
7316
|
}
|
|
@@ -6960,7 +7343,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6960
7343
|
});
|
|
6961
7344
|
if (idx < 0) return text;
|
|
6962
7345
|
toks.splice(idx, 1);
|
|
6963
|
-
|
|
7346
|
+
log13.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
|
|
6964
7347
|
return toks.join(" ");
|
|
6965
7348
|
}
|
|
6966
7349
|
/** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
|
|
@@ -7051,7 +7434,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7051
7434
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
7052
7435
|
if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
|
|
7053
7436
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
7054
|
-
|
|
7437
|
+
log13.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
7055
7438
|
this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
|
|
7056
7439
|
this.options.onHold();
|
|
7057
7440
|
if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
|
|
@@ -7086,13 +7469,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7086
7469
|
if (text) {
|
|
7087
7470
|
const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7088
7471
|
if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
|
|
7089
|
-
|
|
7472
|
+
log13.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
|
|
7090
7473
|
this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
|
|
7091
7474
|
return;
|
|
7092
7475
|
}
|
|
7093
7476
|
const refinal = this.refinalizes(text);
|
|
7094
7477
|
if (refinal && this.repliedSinceDispatch) {
|
|
7095
|
-
|
|
7478
|
+
log13.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
|
|
7096
7479
|
this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
|
|
7097
7480
|
this.lastDispatchFlat = flat;
|
|
7098
7481
|
this.lastDispatchWords = text.trim().split(/\s+/);
|
|
@@ -7252,7 +7635,7 @@ var SpokenSplitter = class {
|
|
|
7252
7635
|
};
|
|
7253
7636
|
|
|
7254
7637
|
// node_modules/@bod.ee/voice/src/adapters/soniox.ts
|
|
7255
|
-
var
|
|
7638
|
+
var log14 = forComponent2("SonioxSTT");
|
|
7256
7639
|
var now = () => performance.now();
|
|
7257
7640
|
var SonioxSTTOptions = class {
|
|
7258
7641
|
auth = "";
|
|
@@ -7293,7 +7676,7 @@ var SonioxSTT = class {
|
|
|
7293
7676
|
this.onDiag({ t: now(), kind, ...fields });
|
|
7294
7677
|
} catch (e) {
|
|
7295
7678
|
this.diagOn = false;
|
|
7296
|
-
|
|
7679
|
+
log14.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7297
7680
|
}
|
|
7298
7681
|
}
|
|
7299
7682
|
lastChunkAt = 0;
|
|
@@ -7335,11 +7718,11 @@ var SonioxSTT = class {
|
|
|
7335
7718
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
7336
7719
|
this.ws.onclose = (ev) => {
|
|
7337
7720
|
if (this.stopped) return;
|
|
7338
|
-
|
|
7721
|
+
log14.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
7339
7722
|
this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
|
|
7340
7723
|
this.reset();
|
|
7341
7724
|
this.connectWs().catch((e) => {
|
|
7342
|
-
|
|
7725
|
+
log14.error(`soniox reconnect failed: ${e.message}`);
|
|
7343
7726
|
this.diag("stt_reconnect_failed", { message: e.message });
|
|
7344
7727
|
});
|
|
7345
7728
|
};
|
|
@@ -7351,7 +7734,7 @@ var SonioxSTT = class {
|
|
|
7351
7734
|
this.endpointTimer = setInterval(() => {
|
|
7352
7735
|
const combined = (this.finalText + this.partialText).trim();
|
|
7353
7736
|
if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
7354
|
-
if (this.firstTokenAt)
|
|
7737
|
+
if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
7355
7738
|
this.reset();
|
|
7356
7739
|
this.onUtterance(combined, now());
|
|
7357
7740
|
}, 120);
|
|
@@ -7363,7 +7746,7 @@ var SonioxSTT = class {
|
|
|
7363
7746
|
if (this.stopped) return;
|
|
7364
7747
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
7365
7748
|
if (now() - ref > noAudioMs) {
|
|
7366
|
-
|
|
7749
|
+
log14.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
7367
7750
|
this.diag("stt_watchdog_fatal", { noAudioMs });
|
|
7368
7751
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
7369
7752
|
this.stop();
|
|
@@ -7386,7 +7769,7 @@ var SonioxSTT = class {
|
|
|
7386
7769
|
handle(m) {
|
|
7387
7770
|
if (m.error_message) {
|
|
7388
7771
|
this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
|
|
7389
|
-
return
|
|
7772
|
+
return log14.error(`soniox: ${m.error_message}`);
|
|
7390
7773
|
}
|
|
7391
7774
|
let endpoint = false;
|
|
7392
7775
|
for (const t of m.tokens ?? []) {
|
|
@@ -7403,7 +7786,7 @@ var SonioxSTT = class {
|
|
|
7403
7786
|
this.onPartial(combined);
|
|
7404
7787
|
if (endpoint && this.finalText.trim()) {
|
|
7405
7788
|
const utterance = this.finalText.trim();
|
|
7406
|
-
if (this.firstTokenAt)
|
|
7789
|
+
if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
7407
7790
|
this.reset();
|
|
7408
7791
|
this.onUtterance(utterance, now());
|
|
7409
7792
|
}
|
|
@@ -7425,7 +7808,7 @@ var SonioxSTT = class {
|
|
|
7425
7808
|
};
|
|
7426
7809
|
|
|
7427
7810
|
// node_modules/@bod.ee/voice/src/adapters/cartesia.ts
|
|
7428
|
-
var
|
|
7811
|
+
var log15 = forComponent2("CartesiaTTS");
|
|
7429
7812
|
var now2 = () => performance.now();
|
|
7430
7813
|
var CartesiaTTSOptions = class {
|
|
7431
7814
|
auth = "";
|
|
@@ -7461,7 +7844,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7461
7844
|
this.onDiag({ t: now2(), kind, ...fields });
|
|
7462
7845
|
} catch (e) {
|
|
7463
7846
|
this.diagOn = false;
|
|
7464
|
-
|
|
7847
|
+
log15.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7465
7848
|
}
|
|
7466
7849
|
}
|
|
7467
7850
|
firstAudioAt = 0;
|
|
@@ -7496,11 +7879,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7496
7879
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
7497
7880
|
});
|
|
7498
7881
|
this.ws.onclose = (ev) => {
|
|
7499
|
-
|
|
7882
|
+
log15.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
7500
7883
|
this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
|
|
7501
7884
|
if (!this.closed) {
|
|
7502
7885
|
this.connecting = this.doConnect().catch((e) => {
|
|
7503
|
-
|
|
7886
|
+
log15.error(`cartesia reconnect failed: ${e.message}`);
|
|
7504
7887
|
this.diag("tts_reconnect_failed", { message: e.message });
|
|
7505
7888
|
});
|
|
7506
7889
|
}
|
|
@@ -7528,12 +7911,12 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7528
7911
|
this.down = true;
|
|
7529
7912
|
this.downAt = now2();
|
|
7530
7913
|
this.consecutiveOk = 0;
|
|
7531
|
-
|
|
7914
|
+
log15.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
7532
7915
|
this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
|
|
7533
7916
|
this.onDone();
|
|
7534
7917
|
this.startProbe();
|
|
7535
7918
|
} else if (!this.down) {
|
|
7536
|
-
(/No valid transcripts/i.test(m.message || "") ?
|
|
7919
|
+
(/No valid transcripts/i.test(m.message || "") ? log15.debug : log15.warn)(`cartesia: ${JSON.stringify(m)}`);
|
|
7537
7920
|
}
|
|
7538
7921
|
}
|
|
7539
7922
|
};
|
|
@@ -7548,7 +7931,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7548
7931
|
this.stopProbe();
|
|
7549
7932
|
const downMs = this.downAt ? now2() - this.downAt : 0;
|
|
7550
7933
|
this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
|
|
7551
|
-
(downMs < 2e3 ?
|
|
7934
|
+
(downMs < 2e3 ? log15.debug : log15.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
7552
7935
|
}
|
|
7553
7936
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
7554
7937
|
async ensureConnected() {
|
|
@@ -7639,7 +8022,7 @@ configureLogging((name) => {
|
|
|
7639
8022
|
});
|
|
7640
8023
|
|
|
7641
8024
|
// src/duplex.ts
|
|
7642
|
-
var
|
|
8025
|
+
var log16 = forComponent("DuplexAgent");
|
|
7643
8026
|
function describeCall(call) {
|
|
7644
8027
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
7645
8028
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -7850,7 +8233,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7850
8233
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
7851
8234
|
if (m) {
|
|
7852
8235
|
this.fabricationCut = true;
|
|
7853
|
-
|
|
8236
|
+
log16.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
7854
8237
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
7855
8238
|
if (!safe) return;
|
|
7856
8239
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -7945,7 +8328,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7945
8328
|
return {
|
|
7946
8329
|
preToolUse: (call) => {
|
|
7947
8330
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
7948
|
-
|
|
8331
|
+
log16.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
7949
8332
|
this.abortSpeculation();
|
|
7950
8333
|
return { block: true, reason: "Speculative turn aborted." };
|
|
7951
8334
|
}
|
|
@@ -7983,7 +8366,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7983
8366
|
try {
|
|
7984
8367
|
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.");
|
|
7985
8368
|
} catch (e) {
|
|
7986
|
-
|
|
8369
|
+
log16.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
7987
8370
|
} finally {
|
|
7988
8371
|
this.nudging = false;
|
|
7989
8372
|
}
|
|
@@ -8076,7 +8459,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8076
8459
|
try {
|
|
8077
8460
|
res = await this.voice.send(spec.text);
|
|
8078
8461
|
} catch (e) {
|
|
8079
|
-
|
|
8462
|
+
log16.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
8080
8463
|
} finally {
|
|
8081
8464
|
this.voice.options.signal = prevSignal;
|
|
8082
8465
|
}
|
|
@@ -8090,7 +8473,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8090
8473
|
if (d === "abort") {
|
|
8091
8474
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
8092
8475
|
this.speculativeAbortedCalls++;
|
|
8093
|
-
|
|
8476
|
+
log16.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
8094
8477
|
if (this.spec === spec) this.spec = void 0;
|
|
8095
8478
|
return res ?? empty;
|
|
8096
8479
|
}
|
|
@@ -8355,7 +8738,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8355
8738
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
8356
8739
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
8357
8740
|
if (cres.finishReason !== "stop") {
|
|
8358
|
-
|
|
8741
|
+
log16.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
8359
8742
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
8360
8743
|
}
|
|
8361
8744
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -8491,7 +8874,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8491
8874
|
rec.status = "done";
|
|
8492
8875
|
rec.result = res.text;
|
|
8493
8876
|
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
8494
|
-
|
|
8877
|
+
log16.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
8495
8878
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
8496
8879
|
id,
|
|
8497
8880
|
text: res.text,
|
|
@@ -8533,14 +8916,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8533
8916
|
foldIfSuperseded(rec, note) {
|
|
8534
8917
|
if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
|
|
8535
8918
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
8536
|
-
|
|
8919
|
+
log16.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
8537
8920
|
return true;
|
|
8538
8921
|
}
|
|
8539
8922
|
failTask(rec, msg) {
|
|
8540
8923
|
this.dropAsk(rec.id);
|
|
8541
8924
|
rec.status = "error";
|
|
8542
8925
|
rec.result = msg;
|
|
8543
|
-
|
|
8926
|
+
log16.warn(`task ${rec.id} failed: ${msg}`);
|
|
8544
8927
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
8545
8928
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
8546
8929
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
@@ -8789,17 +9172,42 @@ async function withDeadline(p, ms) {
|
|
|
8789
9172
|
}
|
|
8790
9173
|
}
|
|
8791
9174
|
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.`;
|
|
9175
|
+
async function runMcpCall(name, call, ctx, timeoutMs) {
|
|
9176
|
+
const jobs = ctx?.toolJobs;
|
|
9177
|
+
const shape = (raw) => {
|
|
9178
|
+
const r2 = toResult(raw);
|
|
9179
|
+
return r2.images?.length ? r2 : r2.text;
|
|
9180
|
+
};
|
|
9181
|
+
if (!jobs) {
|
|
9182
|
+
const raw = await withDeadline(call({}), timeoutMs);
|
|
9183
|
+
return raw === TIMED_OUT ? mcpTimedOut(name, timeoutMs) : shape(raw);
|
|
9184
|
+
}
|
|
9185
|
+
const r = await jobs.runForeground(async ({ signal, touch, onChunk }) => {
|
|
9186
|
+
const raw = await call({
|
|
9187
|
+
signal,
|
|
9188
|
+
onProgress: (p) => {
|
|
9189
|
+
touch();
|
|
9190
|
+
if (p?.message) onChunk(`[progress${p.progress != null ? ` ${p.progress}${p.total != null ? `/${p.total}` : ""}` : ""}] ${p.message}
|
|
9191
|
+
`);
|
|
9192
|
+
}
|
|
9193
|
+
});
|
|
9194
|
+
const res = toResult(raw);
|
|
9195
|
+
onChunk(res.text + (res.images?.length ? `
|
|
9196
|
+
[${res.images.length} image(s) in the result]` : ""));
|
|
9197
|
+
return res;
|
|
9198
|
+
}, { kind: "mcp", label: name, idleAfterActivity: true });
|
|
9199
|
+
if (r.done) return r.value.images?.length ? r.value : r.value.text;
|
|
9200
|
+
const soft = jobs.options.foregroundMs.mcp ?? 0;
|
|
9201
|
+
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.`;
|
|
9202
|
+
}
|
|
8792
9203
|
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
|
|
8793
9204
|
return {
|
|
8794
9205
|
name: `${prefix}${spec.name}`,
|
|
8795
9206
|
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
8796
9207
|
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
8797
9208
|
maxDurationMs: timeoutMs,
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
|
|
8801
|
-
const r = toResult(raw);
|
|
8802
|
-
return r.images?.length ? r : r.text;
|
|
9209
|
+
run(args, ctx) {
|
|
9210
|
+
return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
|
|
8803
9211
|
}
|
|
8804
9212
|
};
|
|
8805
9213
|
}
|
|
@@ -8840,13 +9248,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
8840
9248
|
}
|
|
8841
9249
|
},
|
|
8842
9250
|
maxDurationMs: callTimeoutMs,
|
|
8843
|
-
async run({ name, args }) {
|
|
9251
|
+
async run({ name, args }, ctx) {
|
|
8844
9252
|
const n = String(name ?? "");
|
|
8845
9253
|
if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
|
|
8846
|
-
|
|
8847
|
-
if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
|
|
8848
|
-
const r = toResult(raw);
|
|
8849
|
-
return r.images?.length ? r : r.text;
|
|
9254
|
+
return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
|
|
8850
9255
|
}
|
|
8851
9256
|
};
|
|
8852
9257
|
return [searchTool, callMcpTool];
|
|
@@ -8866,17 +9271,17 @@ function buildMcpCatalog(servers) {
|
|
|
8866
9271
|
return { specs, routes };
|
|
8867
9272
|
}
|
|
8868
9273
|
function searchOverCatalog(servers, specs, routes, resolve2, options) {
|
|
8869
|
-
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
9274
|
+
const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
|
|
8870
9275
|
const r = routes.get(name);
|
|
8871
9276
|
if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
|
|
8872
|
-
return resolve2(r.server, r.rawName, args ?? {});
|
|
9277
|
+
return resolve2(r.server, r.rawName, args ?? {}, o);
|
|
8873
9278
|
}, options) : [];
|
|
8874
9279
|
return { tools, serverNames: servers, toolCount: specs.length };
|
|
8875
9280
|
}
|
|
8876
9281
|
function makeMcpToolSearchFromMounted(mounted, options) {
|
|
8877
9282
|
const { specs, routes } = buildMcpCatalog(mounted);
|
|
8878
9283
|
const byName = new Map(mounted.map((m) => [m.name, m]));
|
|
8879
|
-
return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
|
|
9284
|
+
return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args, o) => byName.get(server).client.callTool(rawName, args, o), options);
|
|
8880
9285
|
}
|
|
8881
9286
|
function makeLazyMcpToolSearch(servers, resolve2, options) {
|
|
8882
9287
|
const { specs, routes } = buildMcpCatalog(servers);
|
|
@@ -8961,6 +9366,8 @@ export {
|
|
|
8961
9366
|
IndexedDbFilesystem,
|
|
8962
9367
|
JailOptions,
|
|
8963
9368
|
JailedFilesystem,
|
|
9369
|
+
JobRegistry,
|
|
9370
|
+
JobRegistryOptions,
|
|
8964
9371
|
LessonOptionsDefaults,
|
|
8965
9372
|
MEMORY_PROMPT,
|
|
8966
9373
|
MODEL_ALIASES,
|
|
@@ -9015,6 +9422,7 @@ export {
|
|
|
9015
9422
|
expandTemplate,
|
|
9016
9423
|
findGitRoot,
|
|
9017
9424
|
forComponent,
|
|
9425
|
+
formatJobNotice,
|
|
9018
9426
|
fullAgentOptions,
|
|
9019
9427
|
getOrCreateWorktree,
|
|
9020
9428
|
getWorktreeSession,
|