@livx.cc/agentx 0.99.47 → 0.99.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{Agent-BWbQ4Sqp.d.ts → Agent-BeoEFl2T.d.ts} +17 -12
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +888 -428
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +696 -315
- package/dist/index.js.map +1 -1
- package/dist/{mcp-kgG6rtsd.d.ts → mcp-DjYZzGGW.d.ts} +13 -4
- package/dist/mcp.client.d.ts +33 -7
- package/dist/mcp.client.js +147 -40
- package/dist/mcp.client.js.map +1 -1
- package/dist/{tools-BqL8Lk4J.d.ts → tools-MNdqlJIa.d.ts} +180 -40
- package/dist/tools.shell.d.ts +40 -11
- package/dist/tools.shell.js +452 -91
- package/dist/tools.shell.js.map +1 -1
- package/package.json +2 -2
package/dist/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,16 +4358,14 @@ 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
|
-
|
|
4033
|
-
/**
|
|
4034
|
-
|
|
4035
|
-
* bounded by the same tool deadline (`toolHoldMs`) so a lost `completed` event can't disable it. */
|
|
4036
|
-
nativeToolsInFlight = /* @__PURE__ */ new Map();
|
|
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();
|
|
4037
4369
|
toolHoldMs = 0;
|
|
4038
4370
|
/** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
|
|
4039
4371
|
* from the pre-step transcript, which would re-run it — so such a stall is not retried. */
|
|
@@ -4082,6 +4414,7 @@ var Agent = class _Agent {
|
|
|
4082
4414
|
addTools(tools) {
|
|
4083
4415
|
this.injectedTools.push(...tools);
|
|
4084
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));
|
|
4085
4418
|
}
|
|
4086
4419
|
/** Remove tools by name from a running agent. Returns the count removed. Also filters
|
|
4087
4420
|
* `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
|
|
@@ -4133,6 +4466,11 @@ var Agent = class _Agent {
|
|
|
4133
4466
|
}
|
|
4134
4467
|
constructor(options) {
|
|
4135
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
|
+
};
|
|
4136
4474
|
if (this.options.fs) this.buildCtx();
|
|
4137
4475
|
}
|
|
4138
4476
|
/** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
|
|
@@ -4144,7 +4482,8 @@ var Agent = class _Agent {
|
|
|
4144
4482
|
this.ctx.ai = this.options.ai;
|
|
4145
4483
|
this.ctx.model = this.options.model;
|
|
4146
4484
|
this.ctx.parkHuman = (p) => this.park(p);
|
|
4147
|
-
|
|
4485
|
+
this.ctx.toolJobs = this.jobs;
|
|
4486
|
+
if (this.options.backgroundJobs) this.ctx.jobs = this.jobs;
|
|
4148
4487
|
}
|
|
4149
4488
|
/**
|
|
4150
4489
|
* Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
|
|
@@ -4160,7 +4499,7 @@ var Agent = class _Agent {
|
|
|
4160
4499
|
const disk = new NodeDiskFilesystem2(process.cwd());
|
|
4161
4500
|
await disk.init();
|
|
4162
4501
|
this.options.fs = new JailedFilesystem2(disk);
|
|
4163
|
-
|
|
4502
|
+
log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
|
|
4164
4503
|
}
|
|
4165
4504
|
this.buildCtx();
|
|
4166
4505
|
}
|
|
@@ -4230,7 +4569,7 @@ var Agent = class _Agent {
|
|
|
4230
4569
|
tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
|
|
4231
4570
|
}
|
|
4232
4571
|
if (o.checkpoints) tools = [...tools, ...checkpointTools()];
|
|
4233
|
-
if (this.ctx.jobs) tools = [...tools, ...makeJobTools(this.
|
|
4572
|
+
if (this.ctx.jobs || [...tools, ...this.injectedTools].some(isMcpTool)) tools = [...tools, ...makeJobTools(this.jobs)];
|
|
4234
4573
|
const plan = o.planMode ? planMode({ host: o.host }) : void 0;
|
|
4235
4574
|
if (plan) tools = [...tools, plan.tool];
|
|
4236
4575
|
this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
|
|
@@ -4346,8 +4685,8 @@ var Agent = class _Agent {
|
|
|
4346
4685
|
let closureNudged = false;
|
|
4347
4686
|
let lastProgressAt = start;
|
|
4348
4687
|
const kill = (finishReason) => {
|
|
4349
|
-
|
|
4350
|
-
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);
|
|
4351
4690
|
const claims = this.survivingClaims();
|
|
4352
4691
|
return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
|
|
4353
4692
|
};
|
|
@@ -4371,20 +4710,23 @@ var Agent = class _Agent {
|
|
|
4371
4710
|
this.toolHoldMs = effectiveToolTimeoutMs;
|
|
4372
4711
|
if (isCursorWithTools) {
|
|
4373
4712
|
if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
|
|
4374
|
-
|
|
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.`);
|
|
4375
4714
|
}
|
|
4376
|
-
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs))
|
|
4715
|
+
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
|
|
4377
4716
|
}
|
|
4378
4717
|
const cursorPo = isCursorWithTools ? {
|
|
4379
4718
|
toolTimeoutMs: effectiveToolTimeoutMs,
|
|
4380
4719
|
toolExecutor: async (name, args) => {
|
|
4381
4720
|
const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
|
|
4382
|
-
this.
|
|
4383
|
-
this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
|
|
4721
|
+
const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
|
|
4384
4722
|
try {
|
|
4385
|
-
|
|
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;
|
|
4386
4729
|
} finally {
|
|
4387
|
-
this.hostToolsInFlight--;
|
|
4388
4730
|
this.toolRanThisAttempt = true;
|
|
4389
4731
|
this.stallPoke?.();
|
|
4390
4732
|
}
|
|
@@ -4398,7 +4740,8 @@ var Agent = class _Agent {
|
|
|
4398
4740
|
};
|
|
4399
4741
|
try {
|
|
4400
4742
|
for (let attempt = 0; ; attempt++) {
|
|
4401
|
-
this.
|
|
4743
|
+
for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
|
|
4744
|
+
this.nativeJobs.clear();
|
|
4402
4745
|
this.toolRanThisAttempt = false;
|
|
4403
4746
|
try {
|
|
4404
4747
|
if (useStream) {
|
|
@@ -4430,11 +4773,11 @@ var Agent = class _Agent {
|
|
|
4430
4773
|
const empty = err?.code === "empty" || err?.code === "cursor_silent";
|
|
4431
4774
|
const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
|
|
4432
4775
|
const replaysTool = stalled && this.toolRanThisAttempt;
|
|
4433
|
-
if (replaysTool)
|
|
4776
|
+
if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
|
|
4434
4777
|
const transient = !o.signal?.aborted && !isAbortError(err) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
|
|
4435
4778
|
if (!transient) throw err;
|
|
4436
4779
|
const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
|
|
4437
|
-
|
|
4780
|
+
log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
|
|
4438
4781
|
o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
|
|
4439
4782
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
4440
4783
|
}
|
|
@@ -4442,12 +4785,12 @@ var Agent = class _Agent {
|
|
|
4442
4785
|
} catch (err) {
|
|
4443
4786
|
if (err?.code === "budget") return kill("budget");
|
|
4444
4787
|
if (err?.code === "stall") {
|
|
4445
|
-
|
|
4788
|
+
log7.warn(`model stalled out after retries: ${err?.message ?? err}`);
|
|
4446
4789
|
return { ...kill("timeout"), error: err };
|
|
4447
4790
|
}
|
|
4448
4791
|
if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
|
|
4449
4792
|
if (err?.code === "empty" || err?.code === "cursor_silent") {
|
|
4450
|
-
|
|
4793
|
+
log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
|
|
4451
4794
|
return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err };
|
|
4452
4795
|
}
|
|
4453
4796
|
const body = err?.body ?? err?.response?.data ?? err?.error;
|
|
@@ -4459,7 +4802,7 @@ var Agent = class _Agent {
|
|
|
4459
4802
|
}
|
|
4460
4803
|
if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
|
|
4461
4804
|
const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
|
|
4462
|
-
|
|
4805
|
+
log7.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
|
|
4463
4806
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
|
|
4464
4807
|
}
|
|
4465
4808
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -4515,21 +4858,21 @@ var Agent = class _Agent {
|
|
|
4515
4858
|
if (emptyTurn && o.failEmptyTurns) {
|
|
4516
4859
|
if (!emptyNudged && canNudge()) {
|
|
4517
4860
|
emptyNudged = true;
|
|
4518
|
-
|
|
4861
|
+
log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
|
|
4519
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." });
|
|
4520
4863
|
continue;
|
|
4521
4864
|
}
|
|
4522
|
-
|
|
4865
|
+
log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
|
|
4523
4866
|
const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
|
|
4524
4867
|
e.code = "empty";
|
|
4525
4868
|
return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
|
|
4526
4869
|
}
|
|
4527
4870
|
const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
|
|
4528
4871
|
for (const n of fabricated) this.fabricatedClaims.add(n);
|
|
4529
|
-
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`);
|
|
4530
4873
|
if (fabricated.length && canNudge()) {
|
|
4531
4874
|
closureNudged = true;
|
|
4532
|
-
|
|
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`);
|
|
4533
4876
|
this.transcript.push({
|
|
4534
4877
|
role: "user",
|
|
4535
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.`
|
|
@@ -4538,15 +4881,18 @@ var Agent = class _Agent {
|
|
|
4538
4881
|
}
|
|
4539
4882
|
if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
|
|
4540
4883
|
closureNudged = true;
|
|
4541
|
-
|
|
4884
|
+
log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
|
|
4542
4885
|
this.transcript.push({
|
|
4543
4886
|
role: "user",
|
|
4544
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."
|
|
4545
4888
|
});
|
|
4546
4889
|
continue;
|
|
4547
4890
|
}
|
|
4548
|
-
|
|
4549
|
-
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;
|
|
4550
4896
|
this.releaseStaleImages();
|
|
4551
4897
|
await this.activeHooks?.onStop?.(res.content ?? "");
|
|
4552
4898
|
const claims = this.survivingClaims();
|
|
@@ -4572,11 +4918,12 @@ var Agent = class _Agent {
|
|
|
4572
4918
|
}
|
|
4573
4919
|
this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
|
|
4574
4920
|
}
|
|
4921
|
+
lastProgressAt = Date.now();
|
|
4575
4922
|
if (this.ctx.needsInput) {
|
|
4576
4923
|
const question = this.ctx.needsInput;
|
|
4577
4924
|
this.ctx.needsInput = void 0;
|
|
4578
|
-
|
|
4579
|
-
await this.
|
|
4925
|
+
log7.warn(`needs_input: ${question.question} (steps=${steps})`);
|
|
4926
|
+
await this.jobs.drain();
|
|
4580
4927
|
const text = [
|
|
4581
4928
|
`OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
|
|
4582
4929
|
...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
|
|
@@ -4615,6 +4962,8 @@ var Agent = class _Agent {
|
|
|
4615
4962
|
if (timer) clearTimeout(timer);
|
|
4616
4963
|
timer = setTimeout(() => {
|
|
4617
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(", ")})` : ""));
|
|
4618
4967
|
fired = true;
|
|
4619
4968
|
ac.abort(stallError());
|
|
4620
4969
|
}, stallMs);
|
|
@@ -4634,12 +4983,11 @@ var Agent = class _Agent {
|
|
|
4634
4983
|
reclassify: (err) => stalled() ? stallError() : err
|
|
4635
4984
|
};
|
|
4636
4985
|
}
|
|
4637
|
-
/** A host or native delegated tool is running
|
|
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. */
|
|
4638
4988
|
toolHoldActive() {
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
for (const until of this.nativeToolsInFlight.values()) if (now3 < until) return true;
|
|
4642
|
-
return false;
|
|
4989
|
+
this.jobs.reap();
|
|
4990
|
+
return this.jobs.holding().length > 0;
|
|
4643
4991
|
}
|
|
4644
4992
|
/** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
|
|
4645
4993
|
* non-result the model produced by going silent. Callers retry it rather than report "done". */
|
|
@@ -4679,10 +5027,16 @@ var Agent = class _Agent {
|
|
|
4679
5027
|
if (a.output !== void 0) prev.output = a.output;
|
|
4680
5028
|
prev.status = a.status;
|
|
4681
5029
|
}
|
|
5030
|
+
const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
|
|
4682
5031
|
if (a.status === "running") {
|
|
4683
|
-
if (
|
|
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 }));
|
|
4684
5034
|
} else {
|
|
4685
|
-
if (
|
|
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
|
+
}
|
|
4686
5040
|
this.toolRanThisAttempt = true;
|
|
4687
5041
|
}
|
|
4688
5042
|
sawTool = true;
|
|
@@ -4773,13 +5127,13 @@ var Agent = class _Agent {
|
|
|
4773
5127
|
const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
|
|
4774
5128
|
if (decision?.block) {
|
|
4775
5129
|
const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
|
|
4776
|
-
|
|
5130
|
+
log7.debug(`${tc.function.name} -> ${blocked}`);
|
|
4777
5131
|
await hooks?.postToolUse?.(call, blocked, meta);
|
|
4778
5132
|
return blocked;
|
|
4779
5133
|
}
|
|
4780
5134
|
this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
|
|
4781
5135
|
if (earlyError) {
|
|
4782
|
-
|
|
5136
|
+
log7.debug(`${tc.function.name} -> ${earlyError}`);
|
|
4783
5137
|
await hooks?.postToolUse?.(call, earlyError, meta);
|
|
4784
5138
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
|
|
4785
5139
|
return earlyError;
|
|
@@ -4788,12 +5142,12 @@ var Agent = class _Agent {
|
|
|
4788
5142
|
let images;
|
|
4789
5143
|
let threw = false;
|
|
4790
5144
|
try {
|
|
4791
|
-
|
|
5145
|
+
log7.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
4792
5146
|
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
4793
5147
|
try {
|
|
4794
5148
|
hooks.onToolOutput(call, chunk, meta);
|
|
4795
5149
|
} catch (e) {
|
|
4796
|
-
|
|
5150
|
+
log7.debug(`onToolOutput hook error: ${e}`);
|
|
4797
5151
|
}
|
|
4798
5152
|
} : void 0;
|
|
4799
5153
|
const raw = await tool.run(args, this.ctx);
|
|
@@ -4805,7 +5159,7 @@ var Agent = class _Agent {
|
|
|
4805
5159
|
}
|
|
4806
5160
|
} catch (e) {
|
|
4807
5161
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4808
|
-
|
|
5162
|
+
log7.debug(`${tc.function.name} -> error: ${msg}`);
|
|
4809
5163
|
result = `Error: ${msg}`;
|
|
4810
5164
|
threw = true;
|
|
4811
5165
|
} finally {
|
|
@@ -4897,6 +5251,7 @@ function estimateTokens(m, keepRecentImages = 1) {
|
|
|
4897
5251
|
return Math.ceil(sendBytes(m, keepRecentImages) / 4);
|
|
4898
5252
|
}
|
|
4899
5253
|
var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
|
|
5254
|
+
var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
|
|
4900
5255
|
function transportHeadroomMs(t) {
|
|
4901
5256
|
const d = Number(t.maxDurationMs) || 0;
|
|
4902
5257
|
if (!d) return 0;
|
|
@@ -5072,7 +5427,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
|
|
|
5072
5427
|
const ids = callIdSet(messages.slice(from));
|
|
5073
5428
|
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
5074
5429
|
if (total > maxTokens)
|
|
5075
|
-
|
|
5430
|
+
log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
5076
5431
|
return [...head, ...messages.slice(from)];
|
|
5077
5432
|
}
|
|
5078
5433
|
function compact(m, max, focus) {
|
|
@@ -5468,6 +5823,7 @@ var toolCall = (id, name, args) => ({
|
|
|
5468
5823
|
|
|
5469
5824
|
// src/index.ts
|
|
5470
5825
|
init_tools();
|
|
5826
|
+
init_tools_jobs();
|
|
5471
5827
|
|
|
5472
5828
|
// src/scheduler.ts
|
|
5473
5829
|
function parseCronField(field, min, max) {
|
|
@@ -5742,7 +6098,7 @@ async function fullAgentOptions(opts = {}) {
|
|
|
5742
6098
|
const registry = jobs ?? new ShellJobRegistry2({ cwd, killOnExit: true });
|
|
5743
6099
|
if (onJobExit) registry.setOnExit((n) => onJobExit(formatJobExit2(n)));
|
|
5744
6100
|
const shell = [makeRealShellTool2({ cwd, registry }), ...makeShellJobTools2(registry)];
|
|
5745
|
-
return { ...rest, tools: [...tools ?? defaultTools(), ...shell] };
|
|
6101
|
+
return { ...rest, jobs: registry.jobs, tools: [...tools ?? defaultTools(), ...shell] };
|
|
5746
6102
|
}
|
|
5747
6103
|
|
|
5748
6104
|
// src/index.ts
|
|
@@ -5754,7 +6110,7 @@ init_tools_web();
|
|
|
5754
6110
|
init_tools();
|
|
5755
6111
|
init_tools_structured();
|
|
5756
6112
|
init_logging();
|
|
5757
|
-
var
|
|
6113
|
+
var log9 = forComponent("scratch");
|
|
5758
6114
|
var SCRATCH_DIR = "/scratch";
|
|
5759
6115
|
function shortArgs(args) {
|
|
5760
6116
|
try {
|
|
@@ -5794,7 +6150,7 @@ var Scratch = class {
|
|
|
5794
6150
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5795
6151
|
await this.fs.writeFile(path, header + raw);
|
|
5796
6152
|
} catch (e) {
|
|
5797
|
-
|
|
6153
|
+
log9.debug("scratch write failed; returning raw", e);
|
|
5798
6154
|
return raw;
|
|
5799
6155
|
}
|
|
5800
6156
|
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
@@ -5824,7 +6180,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
|
|
|
5824
6180
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5825
6181
|
await this.fs.writeFile(path, header + full);
|
|
5826
6182
|
} catch (e) {
|
|
5827
|
-
|
|
6183
|
+
log9.debug("scratch spill failed; cropping lossy", e);
|
|
5828
6184
|
return full.slice(0, pageBytes) + `
|
|
5829
6185
|
|
|
5830
6186
|
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
@@ -5870,7 +6226,7 @@ Question: ${q}`);
|
|
|
5870
6226
|
const answer = (res.text ?? "").trim();
|
|
5871
6227
|
return answer || "(no answer found in scratch)";
|
|
5872
6228
|
} catch (e) {
|
|
5873
|
-
|
|
6229
|
+
log9.debug("Ask peek failed", e);
|
|
5874
6230
|
return `Error querying scratch: ${e?.message ?? e}`;
|
|
5875
6231
|
}
|
|
5876
6232
|
}
|
|
@@ -5879,7 +6235,7 @@ Question: ${q}`);
|
|
|
5879
6235
|
|
|
5880
6236
|
// src/lessons.ts
|
|
5881
6237
|
init_logging();
|
|
5882
|
-
var
|
|
6238
|
+
var log10 = forComponent("Lessons");
|
|
5883
6239
|
var LessonOptionsDefaults = class {
|
|
5884
6240
|
minRepeats = 2;
|
|
5885
6241
|
};
|
|
@@ -5904,8 +6260,8 @@ function lessonCapture(options) {
|
|
|
5904
6260
|
counts.set(lesson.slug, n);
|
|
5905
6261
|
if (n < o.minRepeats) return;
|
|
5906
6262
|
written.add(lesson.slug);
|
|
5907
|
-
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) =>
|
|
5908
|
-
|
|
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)`);
|
|
5909
6265
|
}
|
|
5910
6266
|
};
|
|
5911
6267
|
}
|
|
@@ -5913,7 +6269,7 @@ function lessonCapture(options) {
|
|
|
5913
6269
|
// src/reflect.ts
|
|
5914
6270
|
init_llm();
|
|
5915
6271
|
init_logging();
|
|
5916
|
-
var
|
|
6272
|
+
var log11 = forComponent("Reflect");
|
|
5917
6273
|
async function reflectOnRun(o) {
|
|
5918
6274
|
const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
|
|
5919
6275
|
if (!digest.trim()) return null;
|
|
@@ -5929,7 +6285,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5929
6285
|
const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
|
|
5930
6286
|
text = r?.content ?? "";
|
|
5931
6287
|
} catch (e) {
|
|
5932
|
-
|
|
6288
|
+
log11.warn(`reflection call failed: ${e?.message ?? e}`);
|
|
5933
6289
|
return null;
|
|
5934
6290
|
}
|
|
5935
6291
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
@@ -5939,10 +6295,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5939
6295
|
try {
|
|
5940
6296
|
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
5941
6297
|
} catch (e) {
|
|
5942
|
-
|
|
6298
|
+
log11.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
5943
6299
|
return null;
|
|
5944
6300
|
}
|
|
5945
|
-
|
|
6301
|
+
log11.debug(`reflection persisted ${slug2}`);
|
|
5946
6302
|
return slug2;
|
|
5947
6303
|
}
|
|
5948
6304
|
function digestRun(messages, maxChars) {
|
|
@@ -5991,7 +6347,7 @@ function forComponent2(name) {
|
|
|
5991
6347
|
}
|
|
5992
6348
|
|
|
5993
6349
|
// node_modules/@bod.ee/voice/src/core/emotion.ts
|
|
5994
|
-
var
|
|
6350
|
+
var log12 = forComponent2("Emotion");
|
|
5995
6351
|
var EMOTIONS = [
|
|
5996
6352
|
// primary (best results)
|
|
5997
6353
|
"neutral",
|
|
@@ -6145,7 +6501,7 @@ var EmotionStream = class {
|
|
|
6145
6501
|
if (tag) {
|
|
6146
6502
|
this.pending = tag;
|
|
6147
6503
|
if (this.show) display += m[0];
|
|
6148
|
-
} else
|
|
6504
|
+
} else log12.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
6149
6505
|
last = m.index + m[0].length;
|
|
6150
6506
|
}
|
|
6151
6507
|
this.emit(body.slice(last), (s, d, p) => {
|
|
@@ -6168,7 +6524,7 @@ var EmotionStream = class {
|
|
|
6168
6524
|
};
|
|
6169
6525
|
|
|
6170
6526
|
// node_modules/@bod.ee/voice/src/core/engine.ts
|
|
6171
|
-
var
|
|
6527
|
+
var log13 = forComponent2("VoiceEngine");
|
|
6172
6528
|
var realClock = {
|
|
6173
6529
|
now: () => performance.now(),
|
|
6174
6530
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -6443,7 +6799,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6443
6799
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
6444
6800
|
this.tts.warmup?.();
|
|
6445
6801
|
this.setState("listening");
|
|
6446
|
-
|
|
6802
|
+
log13.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
6447
6803
|
}
|
|
6448
6804
|
get usingAec() {
|
|
6449
6805
|
return this.stt.usingAec;
|
|
@@ -6465,7 +6821,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6465
6821
|
this.options.onDiag({ t: this.clock.now(), kind, ...fields });
|
|
6466
6822
|
} catch (e) {
|
|
6467
6823
|
this.diagOn = false;
|
|
6468
|
-
|
|
6824
|
+
log13.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
6469
6825
|
}
|
|
6470
6826
|
}
|
|
6471
6827
|
idleWaiters = [];
|
|
@@ -6574,7 +6930,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6574
6930
|
if (prose.trim()) this.repliedSinceDispatch = true;
|
|
6575
6931
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
6576
6932
|
this.tts.speak(forSpeech(speech), true);
|
|
6577
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
6933
|
+
if (!this.spokeDeltas && this.turnStartAt) log13.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
|
|
6578
6934
|
this.spokeDeltas = true;
|
|
6579
6935
|
this.setState("speaking");
|
|
6580
6936
|
if (this.options.revealMode !== "off" && display) {
|
|
@@ -6651,7 +7007,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6651
7007
|
this.drainTimer = null;
|
|
6652
7008
|
this.speaking = false;
|
|
6653
7009
|
this.stopWordReveal(true);
|
|
6654
|
-
if (this.turnStartAt)
|
|
7010
|
+
if (this.turnStartAt) log13.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
|
|
6655
7011
|
this.echoUntil = this.clock.now() + 2500;
|
|
6656
7012
|
if (!this.usingAec) this.stt.reset();
|
|
6657
7013
|
this.setState("listening");
|
|
@@ -6739,7 +7095,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6739
7095
|
this.clearAckTimer("interrupt");
|
|
6740
7096
|
this.adaptiveAckFired = false;
|
|
6741
7097
|
this.ackProbe = null;
|
|
6742
|
-
if (this.uttQueue.length)
|
|
7098
|
+
if (this.uttQueue.length) log13.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
|
|
6743
7099
|
const droppedQueued = this.uttQueue.length;
|
|
6744
7100
|
this.uttQueue = [];
|
|
6745
7101
|
if (!this.speaking && !this.drainTimer) return;
|
|
@@ -6885,7 +7241,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6885
7241
|
if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
|
|
6886
7242
|
this.specSpent = true;
|
|
6887
7243
|
this.specText = this.specPartial;
|
|
6888
|
-
|
|
7244
|
+
log13.debug(`speculate: "${this.specText.slice(0, 60)}"`);
|
|
6889
7245
|
this.diag("speculate", { text: this.specText });
|
|
6890
7246
|
this.options.onSpeculate(this.specText);
|
|
6891
7247
|
}, this.options.speculativeMs);
|
|
@@ -6954,7 +7310,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6954
7310
|
};
|
|
6955
7311
|
this.tts.newContext();
|
|
6956
7312
|
this.tts.speak(phrase, false);
|
|
6957
|
-
|
|
7313
|
+
log13.debug(`backchannel: "${phrase}"`);
|
|
6958
7314
|
this.diag("backchannel", { phrase });
|
|
6959
7315
|
this.options.onBackchannel(phrase);
|
|
6960
7316
|
}
|
|
@@ -6987,7 +7343,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6987
7343
|
});
|
|
6988
7344
|
if (idx < 0) return text;
|
|
6989
7345
|
toks.splice(idx, 1);
|
|
6990
|
-
|
|
7346
|
+
log13.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
|
|
6991
7347
|
return toks.join(" ");
|
|
6992
7348
|
}
|
|
6993
7349
|
/** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
|
|
@@ -7078,7 +7434,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7078
7434
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
7079
7435
|
if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
|
|
7080
7436
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
7081
|
-
|
|
7437
|
+
log13.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
7082
7438
|
this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
|
|
7083
7439
|
this.options.onHold();
|
|
7084
7440
|
if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
|
|
@@ -7113,13 +7469,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7113
7469
|
if (text) {
|
|
7114
7470
|
const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7115
7471
|
if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
|
|
7116
|
-
|
|
7472
|
+
log13.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
|
|
7117
7473
|
this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
|
|
7118
7474
|
return;
|
|
7119
7475
|
}
|
|
7120
7476
|
const refinal = this.refinalizes(text);
|
|
7121
7477
|
if (refinal && this.repliedSinceDispatch) {
|
|
7122
|
-
|
|
7478
|
+
log13.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
|
|
7123
7479
|
this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
|
|
7124
7480
|
this.lastDispatchFlat = flat;
|
|
7125
7481
|
this.lastDispatchWords = text.trim().split(/\s+/);
|
|
@@ -7279,7 +7635,7 @@ var SpokenSplitter = class {
|
|
|
7279
7635
|
};
|
|
7280
7636
|
|
|
7281
7637
|
// node_modules/@bod.ee/voice/src/adapters/soniox.ts
|
|
7282
|
-
var
|
|
7638
|
+
var log14 = forComponent2("SonioxSTT");
|
|
7283
7639
|
var now = () => performance.now();
|
|
7284
7640
|
var SonioxSTTOptions = class {
|
|
7285
7641
|
auth = "";
|
|
@@ -7320,7 +7676,7 @@ var SonioxSTT = class {
|
|
|
7320
7676
|
this.onDiag({ t: now(), kind, ...fields });
|
|
7321
7677
|
} catch (e) {
|
|
7322
7678
|
this.diagOn = false;
|
|
7323
|
-
|
|
7679
|
+
log14.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7324
7680
|
}
|
|
7325
7681
|
}
|
|
7326
7682
|
lastChunkAt = 0;
|
|
@@ -7362,11 +7718,11 @@ var SonioxSTT = class {
|
|
|
7362
7718
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
7363
7719
|
this.ws.onclose = (ev) => {
|
|
7364
7720
|
if (this.stopped) return;
|
|
7365
|
-
|
|
7721
|
+
log14.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
7366
7722
|
this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
|
|
7367
7723
|
this.reset();
|
|
7368
7724
|
this.connectWs().catch((e) => {
|
|
7369
|
-
|
|
7725
|
+
log14.error(`soniox reconnect failed: ${e.message}`);
|
|
7370
7726
|
this.diag("stt_reconnect_failed", { message: e.message });
|
|
7371
7727
|
});
|
|
7372
7728
|
};
|
|
@@ -7378,7 +7734,7 @@ var SonioxSTT = class {
|
|
|
7378
7734
|
this.endpointTimer = setInterval(() => {
|
|
7379
7735
|
const combined = (this.finalText + this.partialText).trim();
|
|
7380
7736
|
if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
7381
|
-
if (this.firstTokenAt)
|
|
7737
|
+
if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
7382
7738
|
this.reset();
|
|
7383
7739
|
this.onUtterance(combined, now());
|
|
7384
7740
|
}, 120);
|
|
@@ -7390,7 +7746,7 @@ var SonioxSTT = class {
|
|
|
7390
7746
|
if (this.stopped) return;
|
|
7391
7747
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
7392
7748
|
if (now() - ref > noAudioMs) {
|
|
7393
|
-
|
|
7749
|
+
log14.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
7394
7750
|
this.diag("stt_watchdog_fatal", { noAudioMs });
|
|
7395
7751
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
7396
7752
|
this.stop();
|
|
@@ -7413,7 +7769,7 @@ var SonioxSTT = class {
|
|
|
7413
7769
|
handle(m) {
|
|
7414
7770
|
if (m.error_message) {
|
|
7415
7771
|
this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
|
|
7416
|
-
return
|
|
7772
|
+
return log14.error(`soniox: ${m.error_message}`);
|
|
7417
7773
|
}
|
|
7418
7774
|
let endpoint = false;
|
|
7419
7775
|
for (const t of m.tokens ?? []) {
|
|
@@ -7430,7 +7786,7 @@ var SonioxSTT = class {
|
|
|
7430
7786
|
this.onPartial(combined);
|
|
7431
7787
|
if (endpoint && this.finalText.trim()) {
|
|
7432
7788
|
const utterance = this.finalText.trim();
|
|
7433
|
-
if (this.firstTokenAt)
|
|
7789
|
+
if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
7434
7790
|
this.reset();
|
|
7435
7791
|
this.onUtterance(utterance, now());
|
|
7436
7792
|
}
|
|
@@ -7452,7 +7808,7 @@ var SonioxSTT = class {
|
|
|
7452
7808
|
};
|
|
7453
7809
|
|
|
7454
7810
|
// node_modules/@bod.ee/voice/src/adapters/cartesia.ts
|
|
7455
|
-
var
|
|
7811
|
+
var log15 = forComponent2("CartesiaTTS");
|
|
7456
7812
|
var now2 = () => performance.now();
|
|
7457
7813
|
var CartesiaTTSOptions = class {
|
|
7458
7814
|
auth = "";
|
|
@@ -7488,7 +7844,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7488
7844
|
this.onDiag({ t: now2(), kind, ...fields });
|
|
7489
7845
|
} catch (e) {
|
|
7490
7846
|
this.diagOn = false;
|
|
7491
|
-
|
|
7847
|
+
log15.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7492
7848
|
}
|
|
7493
7849
|
}
|
|
7494
7850
|
firstAudioAt = 0;
|
|
@@ -7523,11 +7879,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7523
7879
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
7524
7880
|
});
|
|
7525
7881
|
this.ws.onclose = (ev) => {
|
|
7526
|
-
|
|
7882
|
+
log15.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
7527
7883
|
this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
|
|
7528
7884
|
if (!this.closed) {
|
|
7529
7885
|
this.connecting = this.doConnect().catch((e) => {
|
|
7530
|
-
|
|
7886
|
+
log15.error(`cartesia reconnect failed: ${e.message}`);
|
|
7531
7887
|
this.diag("tts_reconnect_failed", { message: e.message });
|
|
7532
7888
|
});
|
|
7533
7889
|
}
|
|
@@ -7555,12 +7911,12 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7555
7911
|
this.down = true;
|
|
7556
7912
|
this.downAt = now2();
|
|
7557
7913
|
this.consecutiveOk = 0;
|
|
7558
|
-
|
|
7914
|
+
log15.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
7559
7915
|
this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
|
|
7560
7916
|
this.onDone();
|
|
7561
7917
|
this.startProbe();
|
|
7562
7918
|
} else if (!this.down) {
|
|
7563
|
-
(/No valid transcripts/i.test(m.message || "") ?
|
|
7919
|
+
(/No valid transcripts/i.test(m.message || "") ? log15.debug : log15.warn)(`cartesia: ${JSON.stringify(m)}`);
|
|
7564
7920
|
}
|
|
7565
7921
|
}
|
|
7566
7922
|
};
|
|
@@ -7575,7 +7931,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7575
7931
|
this.stopProbe();
|
|
7576
7932
|
const downMs = this.downAt ? now2() - this.downAt : 0;
|
|
7577
7933
|
this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
|
|
7578
|
-
(downMs < 2e3 ?
|
|
7934
|
+
(downMs < 2e3 ? log15.debug : log15.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
7579
7935
|
}
|
|
7580
7936
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
7581
7937
|
async ensureConnected() {
|
|
@@ -7666,7 +8022,7 @@ configureLogging((name) => {
|
|
|
7666
8022
|
});
|
|
7667
8023
|
|
|
7668
8024
|
// src/duplex.ts
|
|
7669
|
-
var
|
|
8025
|
+
var log16 = forComponent("DuplexAgent");
|
|
7670
8026
|
function describeCall(call) {
|
|
7671
8027
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
7672
8028
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -7877,7 +8233,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7877
8233
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
7878
8234
|
if (m) {
|
|
7879
8235
|
this.fabricationCut = true;
|
|
7880
|
-
|
|
8236
|
+
log16.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
7881
8237
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
7882
8238
|
if (!safe) return;
|
|
7883
8239
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -7972,7 +8328,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7972
8328
|
return {
|
|
7973
8329
|
preToolUse: (call) => {
|
|
7974
8330
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
7975
|
-
|
|
8331
|
+
log16.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
7976
8332
|
this.abortSpeculation();
|
|
7977
8333
|
return { block: true, reason: "Speculative turn aborted." };
|
|
7978
8334
|
}
|
|
@@ -8010,7 +8366,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8010
8366
|
try {
|
|
8011
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.");
|
|
8012
8368
|
} catch (e) {
|
|
8013
|
-
|
|
8369
|
+
log16.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
8014
8370
|
} finally {
|
|
8015
8371
|
this.nudging = false;
|
|
8016
8372
|
}
|
|
@@ -8103,7 +8459,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8103
8459
|
try {
|
|
8104
8460
|
res = await this.voice.send(spec.text);
|
|
8105
8461
|
} catch (e) {
|
|
8106
|
-
|
|
8462
|
+
log16.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
8107
8463
|
} finally {
|
|
8108
8464
|
this.voice.options.signal = prevSignal;
|
|
8109
8465
|
}
|
|
@@ -8117,7 +8473,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8117
8473
|
if (d === "abort") {
|
|
8118
8474
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
8119
8475
|
this.speculativeAbortedCalls++;
|
|
8120
|
-
|
|
8476
|
+
log16.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
8121
8477
|
if (this.spec === spec) this.spec = void 0;
|
|
8122
8478
|
return res ?? empty;
|
|
8123
8479
|
}
|
|
@@ -8382,7 +8738,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8382
8738
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
8383
8739
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
8384
8740
|
if (cres.finishReason !== "stop") {
|
|
8385
|
-
|
|
8741
|
+
log16.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
8386
8742
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
8387
8743
|
}
|
|
8388
8744
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -8518,7 +8874,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8518
8874
|
rec.status = "done";
|
|
8519
8875
|
rec.result = res.text;
|
|
8520
8876
|
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
8521
|
-
|
|
8877
|
+
log16.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
8522
8878
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
8523
8879
|
id,
|
|
8524
8880
|
text: res.text,
|
|
@@ -8560,14 +8916,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8560
8916
|
foldIfSuperseded(rec, note) {
|
|
8561
8917
|
if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
|
|
8562
8918
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
8563
|
-
|
|
8919
|
+
log16.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
8564
8920
|
return true;
|
|
8565
8921
|
}
|
|
8566
8922
|
failTask(rec, msg) {
|
|
8567
8923
|
this.dropAsk(rec.id);
|
|
8568
8924
|
rec.status = "error";
|
|
8569
8925
|
rec.result = msg;
|
|
8570
|
-
|
|
8926
|
+
log16.warn(`task ${rec.id} failed: ${msg}`);
|
|
8571
8927
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
8572
8928
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
8573
8929
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
@@ -8816,17 +9172,42 @@ async function withDeadline(p, ms) {
|
|
|
8816
9172
|
}
|
|
8817
9173
|
}
|
|
8818
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
|
+
}
|
|
8819
9203
|
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
|
|
8820
9204
|
return {
|
|
8821
9205
|
name: `${prefix}${spec.name}`,
|
|
8822
9206
|
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
8823
9207
|
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
8824
9208
|
maxDurationMs: timeoutMs,
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
|
|
8828
|
-
const r = toResult(raw);
|
|
8829
|
-
return r.images?.length ? r : r.text;
|
|
9209
|
+
run(args, ctx) {
|
|
9210
|
+
return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
|
|
8830
9211
|
}
|
|
8831
9212
|
};
|
|
8832
9213
|
}
|
|
@@ -8867,13 +9248,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
8867
9248
|
}
|
|
8868
9249
|
},
|
|
8869
9250
|
maxDurationMs: callTimeoutMs,
|
|
8870
|
-
async run({ name, args }) {
|
|
9251
|
+
async run({ name, args }, ctx) {
|
|
8871
9252
|
const n = String(name ?? "");
|
|
8872
9253
|
if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
|
|
8873
|
-
|
|
8874
|
-
if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
|
|
8875
|
-
const r = toResult(raw);
|
|
8876
|
-
return r.images?.length ? r : r.text;
|
|
9254
|
+
return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
|
|
8877
9255
|
}
|
|
8878
9256
|
};
|
|
8879
9257
|
return [searchTool, callMcpTool];
|
|
@@ -8893,17 +9271,17 @@ function buildMcpCatalog(servers) {
|
|
|
8893
9271
|
return { specs, routes };
|
|
8894
9272
|
}
|
|
8895
9273
|
function searchOverCatalog(servers, specs, routes, resolve2, options) {
|
|
8896
|
-
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
9274
|
+
const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
|
|
8897
9275
|
const r = routes.get(name);
|
|
8898
9276
|
if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
|
|
8899
|
-
return resolve2(r.server, r.rawName, args ?? {});
|
|
9277
|
+
return resolve2(r.server, r.rawName, args ?? {}, o);
|
|
8900
9278
|
}, options) : [];
|
|
8901
9279
|
return { tools, serverNames: servers, toolCount: specs.length };
|
|
8902
9280
|
}
|
|
8903
9281
|
function makeMcpToolSearchFromMounted(mounted, options) {
|
|
8904
9282
|
const { specs, routes } = buildMcpCatalog(mounted);
|
|
8905
9283
|
const byName = new Map(mounted.map((m) => [m.name, m]));
|
|
8906
|
-
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);
|
|
8907
9285
|
}
|
|
8908
9286
|
function makeLazyMcpToolSearch(servers, resolve2, options) {
|
|
8909
9287
|
const { specs, routes } = buildMcpCatalog(servers);
|
|
@@ -8988,6 +9366,8 @@ export {
|
|
|
8988
9366
|
IndexedDbFilesystem,
|
|
8989
9367
|
JailOptions,
|
|
8990
9368
|
JailedFilesystem,
|
|
9369
|
+
JobRegistry,
|
|
9370
|
+
JobRegistryOptions,
|
|
8991
9371
|
LessonOptionsDefaults,
|
|
8992
9372
|
MEMORY_PROMPT,
|
|
8993
9373
|
MODEL_ALIASES,
|
|
@@ -9042,6 +9422,7 @@ export {
|
|
|
9042
9422
|
expandTemplate,
|
|
9043
9423
|
findGitRoot,
|
|
9044
9424
|
forComponent,
|
|
9425
|
+
formatJobNotice,
|
|
9045
9426
|
fullAgentOptions,
|
|
9046
9427
|
getOrCreateWorktree,
|
|
9047
9428
|
getWorktreeSession,
|