@livx.cc/agentx 0.99.47 → 0.99.49
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-CXkPfYX2.d.ts} +29 -12
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +913 -429
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +721 -316
- package/dist/index.js.map +1 -1
- package/dist/{mcp-kgG6rtsd.d.ts → mcp-CSLRyvEp.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-HsbxgqjF.d.ts} +185 -40
- package/dist/tools.shell.d.ts +40 -11
- package/dist/tools.shell.js +457 -91
- package/dist/tools.shell.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1511,6 +1511,404 @@ 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
|
+
* NOTE the asymmetry that default hides: for a HOST job silence really is uninformative (the caller holds
|
|
1584
|
+
* the pending dispatch promise — independent proof it is alive), but a NATIVE job has no such proof AND it
|
|
1585
|
+
* suppresses the stall watchdog, i.e. it turns the provider's own silence into permission to keep ignoring
|
|
1586
|
+
* that provider's silence. So Agent passes an explicit per-job `idleMs` (nativeHoldMs) for `native`: the
|
|
1587
|
+
* provider must keep re-proving liveness with `running` events or forfeit the hold. See Agent.nativeHoldMs. */
|
|
1588
|
+
idleMs = { shell: 10 * 6e4, mcp: 10 * 6e4 };
|
|
1589
|
+
/** Absolute ceiling per kind (0/absent = none). */
|
|
1590
|
+
hardCapMs = { mcp: 30 * 6e4 };
|
|
1591
|
+
/** Soft foreground timeout per kind: past it the caller stops waiting and gets a job id instead. */
|
|
1592
|
+
foregroundMs = { mcp: 3e4 };
|
|
1593
|
+
/** How often the reaper runs while reapable jobs exist. */
|
|
1594
|
+
sweepMs = 15e3;
|
|
1595
|
+
/** Grace between the kill signal and a forced kill (producers that escalate read this). */
|
|
1596
|
+
killGraceMs = 5e3;
|
|
1597
|
+
/** Finished jobs kept for inspection; older finished ones are pruned (every delegated tool call is a job). */
|
|
1598
|
+
keepFinished = 200;
|
|
1599
|
+
/** Clock (injectable for tests). Late-bound so a patched Date.now is honoured. */
|
|
1600
|
+
now = () => Date.now();
|
|
1601
|
+
/** Terminal-state notification for jobs with `notify` (the Agent wires this to `inject`). */
|
|
1602
|
+
onExit;
|
|
1603
|
+
};
|
|
1604
|
+
secs = (ms) => `${Math.round(ms / 1e3)}s`;
|
|
1605
|
+
JobRegistry = class {
|
|
1606
|
+
options;
|
|
1607
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1608
|
+
seq = 0;
|
|
1609
|
+
timer;
|
|
1610
|
+
constructor(options) {
|
|
1611
|
+
this.options = { ...new JobRegistryOptions(), ...options };
|
|
1612
|
+
}
|
|
1613
|
+
/** Register externally-driven work (a process, a request, a native tool) and get a handle to feed it. */
|
|
1614
|
+
track(opts = {}) {
|
|
1615
|
+
const o = this.options;
|
|
1616
|
+
const kind = opts.kind ?? "task";
|
|
1617
|
+
const now3 = o.now();
|
|
1618
|
+
let settle;
|
|
1619
|
+
const done = new Promise((r) => {
|
|
1620
|
+
settle = r;
|
|
1621
|
+
});
|
|
1622
|
+
const job = {
|
|
1623
|
+
id: `job-${++this.seq}`,
|
|
1624
|
+
kind,
|
|
1625
|
+
label: opts.label ?? "",
|
|
1626
|
+
status: "running",
|
|
1627
|
+
startedAt: now3,
|
|
1628
|
+
lastActivityAt: now3,
|
|
1629
|
+
bytes: 0,
|
|
1630
|
+
holds: !!opts.holds,
|
|
1631
|
+
buf: "",
|
|
1632
|
+
dropped: 0,
|
|
1633
|
+
idleMs: opts.idleMs ?? o.idleMs[kind] ?? 0,
|
|
1634
|
+
hardCapMs: opts.hardCapMs ?? o.hardCapMs[kind] ?? 0,
|
|
1635
|
+
drain: !!opts.drain,
|
|
1636
|
+
notify: !!opts.notify,
|
|
1637
|
+
idleAfterActivity: !!opts.idleAfterActivity,
|
|
1638
|
+
active: false,
|
|
1639
|
+
cpuTime: opts.cpuTime,
|
|
1640
|
+
onKill: opts.onKill,
|
|
1641
|
+
onExit: opts.onExit,
|
|
1642
|
+
done,
|
|
1643
|
+
settle
|
|
1644
|
+
};
|
|
1645
|
+
this.jobs.set(job.id, job);
|
|
1646
|
+
if (opts.seed) this.append(job, opts.seed);
|
|
1647
|
+
this.arm();
|
|
1648
|
+
return {
|
|
1649
|
+
id: job.id,
|
|
1650
|
+
chunk: (s) => {
|
|
1651
|
+
if (s) {
|
|
1652
|
+
this.append(job, s);
|
|
1653
|
+
job.lastActivityAt = o.now();
|
|
1654
|
+
job.active = true;
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
touch: () => {
|
|
1658
|
+
job.lastActivityAt = o.now();
|
|
1659
|
+
job.active = true;
|
|
1660
|
+
},
|
|
1661
|
+
finish: (status, result, reason) => this.end(job, status, result, reason),
|
|
1662
|
+
detach: () => {
|
|
1663
|
+
job.holds = false;
|
|
1664
|
+
job.notify = true;
|
|
1665
|
+
job.drain = true;
|
|
1666
|
+
},
|
|
1667
|
+
get status() {
|
|
1668
|
+
return job.status;
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
/** Start `fn` in the background and return a job id immediately (sandbox/subagent semantics: drained at turn end). */
|
|
1673
|
+
start(fn, opts = {}) {
|
|
1674
|
+
return this.run(fn, { drain: true, ...opts }).handle.id;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Run `fn` in the FOREGROUND with a soft timeout: resolves `{done:true,value}` if it settles in time, else
|
|
1678
|
+
* `{done:false,id}` — the work keeps going as a detached job whose completion is reported via `onExit`.
|
|
1679
|
+
* A rejection before the timeout is rethrown (the caller's normal error path). `softTimeoutMs` ≤ 0 waits forever.
|
|
1680
|
+
*/
|
|
1681
|
+
async runForeground(fn, opts = {}) {
|
|
1682
|
+
const { handle, promise } = this.run(fn, opts);
|
|
1683
|
+
const soft = opts.softTimeoutMs ?? this.options.foregroundMs[opts.kind ?? "task"] ?? 0;
|
|
1684
|
+
let t;
|
|
1685
|
+
const late = soft > 0 ? new Promise((r) => {
|
|
1686
|
+
t = setTimeout(() => r("late"), soft);
|
|
1687
|
+
}) : new Promise(() => {
|
|
1688
|
+
});
|
|
1689
|
+
try {
|
|
1690
|
+
const r = await Promise.race([promise.then((v) => ({ v }), (e) => ({ e })), late]);
|
|
1691
|
+
if (r !== "late" && handle.status !== "running") this.jobs.delete(handle.id);
|
|
1692
|
+
if (r === "late") {
|
|
1693
|
+
if (handle.status === "running") {
|
|
1694
|
+
handle.detach();
|
|
1695
|
+
return { done: false, id: handle.id };
|
|
1696
|
+
}
|
|
1697
|
+
const settled = await promise.then((v) => ({ v }), (e) => ({ e }));
|
|
1698
|
+
if ("e" in settled) throw settled.e;
|
|
1699
|
+
return { done: true, value: settled.v };
|
|
1700
|
+
}
|
|
1701
|
+
if ("e" in r) throw r.e;
|
|
1702
|
+
return { done: true, value: r.v };
|
|
1703
|
+
} finally {
|
|
1704
|
+
if (t) clearTimeout(t);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
run(fn, opts) {
|
|
1708
|
+
const ctl = new AbortController();
|
|
1709
|
+
const handle = this.track({ ...opts, onKill: (reason) => {
|
|
1710
|
+
ctl.abort(Object.assign(new Error(reason), { name: "AbortError", code: "job_killed" }));
|
|
1711
|
+
opts.onKill?.(reason);
|
|
1712
|
+
} });
|
|
1713
|
+
let promise;
|
|
1714
|
+
try {
|
|
1715
|
+
promise = fn({ signal: ctl.signal, onChunk: handle.chunk, touch: handle.touch });
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
promise = Promise.reject(e);
|
|
1718
|
+
}
|
|
1719
|
+
promise.then(
|
|
1720
|
+
(res) => {
|
|
1721
|
+
if (handle.status === "running" && typeof res === "string" && res) handle.chunk(res);
|
|
1722
|
+
handle.finish("done", res);
|
|
1723
|
+
},
|
|
1724
|
+
(e) => {
|
|
1725
|
+
if (handle.status === "running") handle.chunk(`
|
|
1726
|
+
[error] ${e?.message ?? e}`);
|
|
1727
|
+
handle.finish("error", void 0, e?.message ?? String(e));
|
|
1728
|
+
}
|
|
1729
|
+
);
|
|
1730
|
+
return { handle, promise };
|
|
1731
|
+
}
|
|
1732
|
+
append(job, s) {
|
|
1733
|
+
job.buf += s;
|
|
1734
|
+
const over = job.buf.length - this.options.maxBuffer;
|
|
1735
|
+
if (over > 0) {
|
|
1736
|
+
job.buf = job.buf.slice(over);
|
|
1737
|
+
job.dropped += over;
|
|
1738
|
+
}
|
|
1739
|
+
job.bytes = job.buf.length;
|
|
1740
|
+
}
|
|
1741
|
+
end(job, status, result, reason) {
|
|
1742
|
+
if (job.status !== "running") return;
|
|
1743
|
+
job.status = status;
|
|
1744
|
+
job.endedAt = this.options.now();
|
|
1745
|
+
job.result = result;
|
|
1746
|
+
if (reason && status !== "done") job.reason = reason;
|
|
1747
|
+
job.holds = false;
|
|
1748
|
+
job.settle();
|
|
1749
|
+
this.prune();
|
|
1750
|
+
const onExit = job.onExit ?? this.options.onExit;
|
|
1751
|
+
if (job.notify && onExit) {
|
|
1752
|
+
try {
|
|
1753
|
+
onExit({ id: job.id, kind: job.kind, label: job.label, status, reason: job.reason, tail: job.buf.slice(-4e3), result });
|
|
1754
|
+
} catch (e) {
|
|
1755
|
+
log5.warn(`job onExit handler threw for ${job.id}: ${e}`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
prune() {
|
|
1760
|
+
const finished = [...this.jobs.values()].filter((j) => j.status !== "running");
|
|
1761
|
+
for (const j of finished.slice(0, Math.max(0, finished.length - this.options.keepFinished))) this.jobs.delete(j.id);
|
|
1762
|
+
}
|
|
1763
|
+
/** Current tail output (null = no such job). */
|
|
1764
|
+
output(id) {
|
|
1765
|
+
return this.jobs.get(id)?.buf ?? null;
|
|
1766
|
+
}
|
|
1767
|
+
/** Incremental read: output from absolute char `offset` (clamped to what the ring still holds). */
|
|
1768
|
+
read(id, offset = 0) {
|
|
1769
|
+
const j = this.jobs.get(id);
|
|
1770
|
+
if (!j) return null;
|
|
1771
|
+
const start = Math.max(0, offset - j.dropped);
|
|
1772
|
+
return { text: j.buf.slice(start), next: j.dropped + j.buf.length, truncated: offset < j.dropped };
|
|
1773
|
+
}
|
|
1774
|
+
get(id) {
|
|
1775
|
+
const j = this.jobs.get(id);
|
|
1776
|
+
return j ? this.info(j) : null;
|
|
1777
|
+
}
|
|
1778
|
+
status(id) {
|
|
1779
|
+
const j = this.jobs.get(id);
|
|
1780
|
+
return j ? { status: j.status, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} } : null;
|
|
1781
|
+
}
|
|
1782
|
+
list() {
|
|
1783
|
+
return [...this.jobs.values()].map((j) => this.info(j));
|
|
1784
|
+
}
|
|
1785
|
+
info(j) {
|
|
1786
|
+
const { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result } = j;
|
|
1787
|
+
return { id, kind, label, status, startedAt, lastActivityAt, endedAt, reason, bytes, holds, result };
|
|
1788
|
+
}
|
|
1789
|
+
/** Wait (bounded) for a job to settle. Resolves the job's info either way; null for an unknown id. */
|
|
1790
|
+
async wait(id, timeoutMs) {
|
|
1791
|
+
const j = this.jobs.get(id);
|
|
1792
|
+
if (!j) return null;
|
|
1793
|
+
let t;
|
|
1794
|
+
await Promise.race([j.done, new Promise((r) => {
|
|
1795
|
+
t = setTimeout(r, Math.max(0, timeoutMs));
|
|
1796
|
+
})]);
|
|
1797
|
+
if (t) clearTimeout(t);
|
|
1798
|
+
return this.info(j);
|
|
1799
|
+
}
|
|
1800
|
+
/** Stop a job: runs its `onKill` (abort/SIGTERM) and marks it killed with `reason`. */
|
|
1801
|
+
kill(id, reason = "killed by request") {
|
|
1802
|
+
const j = this.jobs.get(id);
|
|
1803
|
+
if (!j) return false;
|
|
1804
|
+
if (j.status === "running") {
|
|
1805
|
+
this.end(j, "killed", void 0, reason);
|
|
1806
|
+
try {
|
|
1807
|
+
j.onKill?.(reason);
|
|
1808
|
+
} catch (e) {
|
|
1809
|
+
log5.warn(`onKill for ${id} threw: ${e}`);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1814
|
+
/** Kill every running job — or, with `onlyDrainable`, only turn-scoped ones (sandbox/sub-agent/handed-off MCP),
|
|
1815
|
+
* leaving jobs meant to outlive a run (a background dev server) alone. */
|
|
1816
|
+
killAll(reason = "run ended", onlyDrainable = false) {
|
|
1817
|
+
for (const j of this.jobs.values()) if (!onlyDrainable || j.drain) this.kill(j.id, reason);
|
|
1818
|
+
}
|
|
1819
|
+
/** Jobs something is synchronously waiting on (see `JobTrackOptions.holds`) that are still running. */
|
|
1820
|
+
holding() {
|
|
1821
|
+
return [...this.jobs.values()].filter((j) => j.status === "running" && j.holds).map((j) => this.info(j));
|
|
1822
|
+
}
|
|
1823
|
+
/** Await every drainable running job (turn end). Returns the ids that were still running when drain began. */
|
|
1824
|
+
async drain(signal) {
|
|
1825
|
+
const pending = [...this.jobs.values()].filter((j) => j.status === "running" && j.drain);
|
|
1826
|
+
const all = Promise.all(pending.map((j) => j.done));
|
|
1827
|
+
await (signal ? Promise.race([all, new Promise((r) => signal.aborted ? r() : signal.addEventListener("abort", () => r(), { once: true }))]) : all);
|
|
1828
|
+
return pending.map((j) => j.id);
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* The stuck-job decision, synchronous part: kill every running job past its hard cap, or past its idle
|
|
1832
|
+
* window when it has no CPU probe to argue otherwise. Cheap — the stall watchdog calls it on every tick.
|
|
1833
|
+
* Returns the killed ids. Jobs WITH a probe are left to `sweep()`.
|
|
1834
|
+
*/
|
|
1835
|
+
reap() {
|
|
1836
|
+
const now3 = this.options.now();
|
|
1837
|
+
const killed = [];
|
|
1838
|
+
for (const j of this.jobs.values()) {
|
|
1839
|
+
if (j.status !== "running") continue;
|
|
1840
|
+
const verdict = this.verdict(j, now3);
|
|
1841
|
+
if (!verdict || verdict.idle && j.cpuTime) continue;
|
|
1842
|
+
this.stuck(j, verdict.reason, now3);
|
|
1843
|
+
killed.push(j.id);
|
|
1844
|
+
}
|
|
1845
|
+
return killed;
|
|
1846
|
+
}
|
|
1847
|
+
/** Full sweep: sample CPU probes (advancing CPU = activity), then `reap()` including probed jobs. */
|
|
1848
|
+
async sweep() {
|
|
1849
|
+
const now3 = this.options.now();
|
|
1850
|
+
for (const j of [...this.jobs.values()]) {
|
|
1851
|
+
if (j.status !== "running" || !j.cpuTime || !j.idleMs) continue;
|
|
1852
|
+
let cpu;
|
|
1853
|
+
try {
|
|
1854
|
+
cpu = await j.cpuTime();
|
|
1855
|
+
} catch (e) {
|
|
1856
|
+
log5.debug(`cpu probe for ${j.id} failed: ${e}`);
|
|
1857
|
+
}
|
|
1858
|
+
if (cpu != null && j.lastCpu != null && cpu > j.lastCpu) j.lastActivityAt = this.options.now();
|
|
1859
|
+
const prev = j.lastCpu;
|
|
1860
|
+
if (cpu != null) j.lastCpu = cpu;
|
|
1861
|
+
const verdict = this.verdict(j, now3);
|
|
1862
|
+
if (!verdict?.idle || j.status !== "running") continue;
|
|
1863
|
+
if (cpu == null) {
|
|
1864
|
+
log5.debug(`not reaping ${j.id}: ${verdict.reason} but CPU unknown`);
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
if (prev == null) continue;
|
|
1868
|
+
this.stuck(j, `${verdict.reason}, no CPU progress (${prev}s \u2192 ${cpu}s)`, now3);
|
|
1869
|
+
}
|
|
1870
|
+
return this.reap();
|
|
1871
|
+
}
|
|
1872
|
+
verdict(j, now3) {
|
|
1873
|
+
if (j.hardCapMs && now3 - j.startedAt >= j.hardCapMs) return { reason: `stuck: exceeded hard cap ${secs(j.hardCapMs)}`, idle: false };
|
|
1874
|
+
if (j.idleMs && (!j.idleAfterActivity || j.active) && now3 - j.lastActivityAt >= j.idleMs) return { reason: `stuck: idle ${secs(now3 - j.lastActivityAt)}`, idle: true };
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
stuck(j, reason, now3) {
|
|
1878
|
+
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`);
|
|
1879
|
+
if (j.onKill) j.notify = true;
|
|
1880
|
+
this.kill(j.id, reason);
|
|
1881
|
+
}
|
|
1882
|
+
/** Keep a reaper interval alive only while a reapable job is running (unref'd: never holds the process open). */
|
|
1883
|
+
arm() {
|
|
1884
|
+
if (this.timer || !this.options.sweepMs) return;
|
|
1885
|
+
this.timer = setInterval(() => {
|
|
1886
|
+
const live = [...this.jobs.values()].some((j) => j.status === "running" && (j.idleMs || j.hardCapMs));
|
|
1887
|
+
if (!live) {
|
|
1888
|
+
clearInterval(this.timer);
|
|
1889
|
+
this.timer = void 0;
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
void this.sweep().catch((e) => log5.warn(`job sweep failed: ${e}`));
|
|
1893
|
+
}, this.options.sweepMs);
|
|
1894
|
+
this.timer.unref?.();
|
|
1895
|
+
}
|
|
1896
|
+
/** Stop the reaper timer (tests / teardown). */
|
|
1897
|
+
dispose() {
|
|
1898
|
+
if (this.timer) clearInterval(this.timer);
|
|
1899
|
+
this.timer = void 0;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
SandboxJobRegistry = class extends JobRegistry {
|
|
1903
|
+
constructor(maxBuffer) {
|
|
1904
|
+
super(typeof maxBuffer === "number" ? { maxBuffer } : maxBuffer);
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
NO_JOB = (id) => `Error: no background job '${id}'. Use JobStatus with no id to list jobs.`;
|
|
1908
|
+
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)`;
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1514
1912
|
// src/NodeDiskFilesystem.ts
|
|
1515
1913
|
var NodeDiskFilesystem_exports = {};
|
|
1516
1914
|
__export(NodeDiskFilesystem_exports, {
|
|
@@ -1954,7 +2352,10 @@ __export(tools_shell_exports, {
|
|
|
1954
2352
|
ShellJobRegistry: () => ShellJobRegistry,
|
|
1955
2353
|
formatJobExit: () => formatJobExit,
|
|
1956
2354
|
makeRealShellTool: () => makeRealShellTool,
|
|
1957
|
-
makeShellJobTools: () => makeShellJobTools
|
|
2355
|
+
makeShellJobTools: () => makeShellJobTools,
|
|
2356
|
+
parsePsTime: () => parsePsTime,
|
|
2357
|
+
psGroupCpuSeconds: () => psGroupCpuSeconds,
|
|
2358
|
+
sumTreeCpu: () => sumTreeCpu
|
|
1958
2359
|
});
|
|
1959
2360
|
function killGroup(proc, signal) {
|
|
1960
2361
|
if (!proc?.pid) return false;
|
|
@@ -1984,10 +2385,39 @@ async function nodeSpawn() {
|
|
|
1984
2385
|
return _spawn;
|
|
1985
2386
|
}
|
|
1986
2387
|
function formatJobExit(n) {
|
|
1987
|
-
return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
|
|
2388
|
+
return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}${n.reason ? `: ${n.reason}` : ""}] \`${n.command}\`
|
|
1988
2389
|
` + (n.tail ? `${n.tail}
|
|
1989
2390
|
` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
|
|
1990
2391
|
}
|
|
2392
|
+
function parsePsTime(t) {
|
|
2393
|
+
const m = t.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/);
|
|
2394
|
+
if (!m) return void 0;
|
|
2395
|
+
return Number(m[1] ?? 0) * 86400 + Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
|
|
2396
|
+
}
|
|
2397
|
+
async function psGroupCpuSeconds(pgid) {
|
|
2398
|
+
try {
|
|
2399
|
+
const { execFile } = await import("child_process");
|
|
2400
|
+
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))));
|
|
2401
|
+
return sumTreeCpu(out, pgid);
|
|
2402
|
+
} catch (e) {
|
|
2403
|
+
log8.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
|
|
2404
|
+
return void 0;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
function sumTreeCpu(psOut, root) {
|
|
2408
|
+
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) }));
|
|
2409
|
+
const inJob = new Set(rows.filter((r) => r.pgid === root || r.pid === root).map((r) => r.pid));
|
|
2410
|
+
for (let grew = true; grew; ) {
|
|
2411
|
+
grew = false;
|
|
2412
|
+
for (const r of rows) if (!inJob.has(r.pid) && inJob.has(r.ppid)) {
|
|
2413
|
+
inJob.add(r.pid);
|
|
2414
|
+
grew = true;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
let total;
|
|
2418
|
+
for (const r of rows) if (inJob.has(r.pid) && r.sec != null) total = (total ?? 0) + r.sec;
|
|
2419
|
+
return total;
|
|
2420
|
+
}
|
|
1991
2421
|
function makeRealShellTool(options) {
|
|
1992
2422
|
const defaultTimeoutMs = options.timeoutMs ?? 12e4;
|
|
1993
2423
|
const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
|
|
@@ -2118,7 +2548,7 @@ function makeRealShellTool(options) {
|
|
|
2118
2548
|
proc.stderr?.on("data", collect);
|
|
2119
2549
|
proc.on("error", (err) => {
|
|
2120
2550
|
if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
2121
|
-
|
|
2551
|
+
log8.debug("shell spawn error", err);
|
|
2122
2552
|
finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
|
|
2123
2553
|
});
|
|
2124
2554
|
proc.on("close", (code) => {
|
|
@@ -2138,7 +2568,7 @@ function makeRealShellTool(options) {
|
|
|
2138
2568
|
};
|
|
2139
2569
|
}
|
|
2140
2570
|
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.");
|
|
2571
|
+
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
2572
|
return body ? `${head}
|
|
2143
2573
|
Output so far:
|
|
2144
2574
|
${body}` : head;
|
|
@@ -2162,7 +2592,7 @@ function makeShellJobTools(registry) {
|
|
|
2162
2592
|
const out = registry.output(String(id));
|
|
2163
2593
|
if (out == null) return NO_JOB2(String(id));
|
|
2164
2594
|
const st = registry.status(String(id));
|
|
2165
|
-
return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}]
|
|
2595
|
+
return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ""}${st.reason ? `: ${st.reason}` : ""}]
|
|
2166
2596
|
${clean(out) || "(no output yet)"}`;
|
|
2167
2597
|
}
|
|
2168
2598
|
},
|
|
@@ -2177,13 +2607,13 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
2177
2607
|
return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join("\n") : "(no background jobs)";
|
|
2178
2608
|
}
|
|
2179
2609
|
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));
|
|
2610
|
+
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
2611
|
}
|
|
2182
2612
|
},
|
|
2183
2613
|
{
|
|
2184
2614
|
...dropOnRebind,
|
|
2185
2615
|
name: "ShellKill",
|
|
2186
|
-
description: "Stop a running background Shell job by id (SIGTERM).",
|
|
2616
|
+
description: "Stop a running background Shell job by id (SIGTERM to its process group, SIGKILL if it ignores that).",
|
|
2187
2617
|
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
2188
2618
|
async run({ id }) {
|
|
2189
2619
|
return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB2(String(id));
|
|
@@ -2191,7 +2621,7 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
2191
2621
|
}
|
|
2192
2622
|
];
|
|
2193
2623
|
}
|
|
2194
|
-
var
|
|
2624
|
+
var log8, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
|
|
2195
2625
|
var init_tools_shell = __esm({
|
|
2196
2626
|
"src/tools.shell.ts"() {
|
|
2197
2627
|
"use strict";
|
|
@@ -2199,62 +2629,94 @@ var init_tools_shell = __esm({
|
|
|
2199
2629
|
init_redact();
|
|
2200
2630
|
init_logging();
|
|
2201
2631
|
init_shell_sandbox();
|
|
2202
|
-
|
|
2632
|
+
init_tools_jobs();
|
|
2633
|
+
log8 = forComponent("shell");
|
|
2203
2634
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
2204
2635
|
DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
|
|
2205
2636
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
2206
2637
|
ShellJobRegistry = class {
|
|
2207
2638
|
constructor(cfg) {
|
|
2208
2639
|
this.cfg = cfg;
|
|
2640
|
+
this.jobs = cfg.jobs ?? new JobRegistry(cfg.maxBuffer ? { maxBuffer: cfg.maxBuffer } : {});
|
|
2209
2641
|
if (cfg.killOnExit && typeof process !== "undefined") process.once("exit", () => this.killAll());
|
|
2210
2642
|
}
|
|
2211
2643
|
cfg;
|
|
2212
|
-
jobs
|
|
2213
|
-
|
|
2644
|
+
jobs;
|
|
2645
|
+
meta = /* @__PURE__ */ new Map();
|
|
2214
2646
|
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
|
-
};
|
|
2647
|
+
const h = this.track(command, { idleMs: 0 });
|
|
2222
2648
|
try {
|
|
2223
2649
|
const spawn = this.cfg.spawn ?? await nodeSpawn();
|
|
2224
2650
|
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
|
-
});
|
|
2651
|
+
this.attach(h, spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED }));
|
|
2244
2652
|
} catch (e) {
|
|
2245
|
-
|
|
2246
|
-
|
|
2653
|
+
h.chunk(`failed to spawn: ${e?.message ?? e}`);
|
|
2654
|
+
h.finish("error", void 0, e?.message ?? String(e));
|
|
2655
|
+
}
|
|
2656
|
+
return h.id;
|
|
2657
|
+
}
|
|
2658
|
+
track(command, extra) {
|
|
2659
|
+
const meta = { command };
|
|
2660
|
+
let id = "";
|
|
2661
|
+
const h = this.jobs.track({
|
|
2662
|
+
kind: "shell",
|
|
2663
|
+
label: command,
|
|
2664
|
+
seed: extra.seed,
|
|
2665
|
+
idleMs: extra.idleMs,
|
|
2666
|
+
cpuTime: extra.cpu ? async () => meta.proc?.pid ? (this.cfg.cpuTime ?? psGroupCpuSeconds)(meta.proc.pid) : void 0 : void 0,
|
|
2667
|
+
onKill: () => this.signal(meta),
|
|
2668
|
+
onExit: (n) => this.notifyExit(id, meta, n.status, n.reason),
|
|
2669
|
+
notify: true
|
|
2670
|
+
});
|
|
2671
|
+
id = h.id;
|
|
2672
|
+
this.meta.set(id, meta);
|
|
2673
|
+
return h;
|
|
2674
|
+
}
|
|
2675
|
+
attach(h, proc) {
|
|
2676
|
+
const meta = this.meta.get(h.id);
|
|
2677
|
+
meta.proc = proc;
|
|
2678
|
+
const append = (chunk) => h.chunk(typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "");
|
|
2679
|
+
proc.stdout?.on("data", append);
|
|
2680
|
+
proc.stderr?.on("data", append);
|
|
2681
|
+
proc.on("error", (err) => {
|
|
2682
|
+
if (h.status === "running") {
|
|
2683
|
+
append(`
|
|
2684
|
+
[error] ${err?.message ?? err}`);
|
|
2685
|
+
h.finish("error", void 0, err?.message ?? String(err));
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
proc.on("close", (code) => {
|
|
2689
|
+
if (h.status === "running") {
|
|
2690
|
+
meta.exitCode = code ?? void 0;
|
|
2691
|
+
h.finish("done");
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
/** SIGTERM the whole group (bg children are detached group leaders — /bin/sh alone would orphan a forked
|
|
2696
|
+
* server), then SIGKILL after the grace period if it is still there. Falls back to the pid for fakes. */
|
|
2697
|
+
signal(meta) {
|
|
2698
|
+
const proc = meta.proc;
|
|
2699
|
+
if (!killGroup(proc, "SIGTERM")) {
|
|
2700
|
+
try {
|
|
2701
|
+
proc?.kill("SIGTERM");
|
|
2702
|
+
} catch {
|
|
2703
|
+
}
|
|
2247
2704
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
this.
|
|
2705
|
+
if (!proc?.pid) return;
|
|
2706
|
+
let closed = false;
|
|
2707
|
+
proc.on("close", () => {
|
|
2708
|
+
closed = true;
|
|
2709
|
+
});
|
|
2710
|
+
const t = setTimeout(() => {
|
|
2711
|
+
if (!closed) killGroup(proc, "SIGKILL");
|
|
2712
|
+
}, this.jobs.options.killGraceMs);
|
|
2713
|
+
t.unref?.();
|
|
2714
|
+
}
|
|
2715
|
+
/** 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. */
|
|
2716
|
+
notifyExit(id, meta, status, reason) {
|
|
2717
|
+
if (!this.cfg.onExit || status === "killed" && !reason?.startsWith("stuck")) return;
|
|
2256
2718
|
try {
|
|
2257
|
-
this.cfg.onExit({ id, command:
|
|
2719
|
+
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
2720
|
} catch {
|
|
2259
2721
|
}
|
|
2260
2722
|
}
|
|
@@ -2273,14 +2735,20 @@ var init_tools_shell = __esm({
|
|
|
2273
2735
|
}
|
|
2274
2736
|
/** Current tail output for a job (null = no such job). */
|
|
2275
2737
|
output(id) {
|
|
2276
|
-
return this.
|
|
2738
|
+
return this.meta.has(id) ? this.jobs.output(id) : null;
|
|
2277
2739
|
}
|
|
2740
|
+
// null too once the shared registry pruned it
|
|
2278
2741
|
status(id) {
|
|
2279
|
-
const
|
|
2280
|
-
|
|
2742
|
+
const m = this.meta.get(id);
|
|
2743
|
+
const j = m && this.jobs.get(id);
|
|
2744
|
+
if (!m || !j) {
|
|
2745
|
+
this.meta.delete(id);
|
|
2746
|
+
return null;
|
|
2747
|
+
}
|
|
2748
|
+
return { status: shellStatus(j.status), exitCode: m.exitCode, bytes: j.bytes, ...j.reason ? { reason: j.reason } : {} };
|
|
2281
2749
|
}
|
|
2282
2750
|
list() {
|
|
2283
|
-
return [...this.
|
|
2751
|
+
return [...this.meta].filter(([id]) => this.jobs.get(id)).map(([id, m]) => ({ id, command: m.command, status: shellStatus(this.jobs.get(id).status) }));
|
|
2284
2752
|
}
|
|
2285
2753
|
/**
|
|
2286
2754
|
* Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its
|
|
@@ -2288,53 +2756,22 @@ var init_tools_shell = __esm({
|
|
|
2288
2756
|
* worse, a launcher that spawned its own detached worker leaves that worker running with nothing
|
|
2289
2757
|
* tracking it. Adopting hands the model a handle instead: the command keeps going, its completion
|
|
2290
2758
|
* is reported like any other job, and `seed` carries the output produced before the handoff.
|
|
2759
|
+
* Unlike an explicit background job it IS liveness-checked (idle window + CPU probe): nobody asked for
|
|
2760
|
+
* a long-lived process here, so one that goes silent and CPU-idle is stuck, not serving.
|
|
2291
2761
|
*/
|
|
2292
2762
|
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;
|
|
2763
|
+
const h = this.track(command, { seed, cpu: true });
|
|
2764
|
+
this.attach(h, proc);
|
|
2765
|
+
return h.id;
|
|
2319
2766
|
}
|
|
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;
|
|
2767
|
+
kill(id, reason = "killed by ShellKill") {
|
|
2768
|
+
return this.meta.has(id) && this.jobs.kill(id, reason);
|
|
2333
2769
|
}
|
|
2334
2770
|
killAll() {
|
|
2335
|
-
for (const id of this.
|
|
2771
|
+
for (const id of this.meta.keys()) this.jobs.kill(id, "agent exiting");
|
|
2336
2772
|
}
|
|
2337
2773
|
};
|
|
2774
|
+
shellStatus = (s) => s === "done" ? "exited" : s === "running" ? "running" : s === "killed" ? "killed" : "error";
|
|
2338
2775
|
NO_JOB2 = (id) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;
|
|
2339
2776
|
}
|
|
2340
2777
|
});
|
|
@@ -2342,111 +2779,7 @@ var init_tools_shell = __esm({
|
|
|
2342
2779
|
// src/Agent.ts
|
|
2343
2780
|
init_llm();
|
|
2344
2781
|
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
|
-
}
|
|
2782
|
+
init_tools_jobs();
|
|
2450
2783
|
|
|
2451
2784
|
// src/host.ts
|
|
2452
2785
|
var askUserQuestionTool = {
|
|
@@ -3229,7 +3562,7 @@ function exitWorktree() {
|
|
|
3229
3562
|
// src/subagent.ts
|
|
3230
3563
|
init_NodeDiskFilesystem();
|
|
3231
3564
|
init_logging();
|
|
3232
|
-
var
|
|
3565
|
+
var log6 = forComponent("subagent");
|
|
3233
3566
|
async function boundedPool(items, limit, fn) {
|
|
3234
3567
|
const out = new Array(items.length);
|
|
3235
3568
|
let next = 0;
|
|
@@ -3303,7 +3636,7 @@ function traceFileFor(dir, label, depth) {
|
|
|
3303
3636
|
const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
|
|
3304
3637
|
return join3(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
|
|
3305
3638
|
} catch (e) {
|
|
3306
|
-
|
|
3639
|
+
log6.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
|
|
3307
3640
|
return void 0;
|
|
3308
3641
|
}
|
|
3309
3642
|
}
|
|
@@ -3311,7 +3644,7 @@ function flushTrace(file, meta, messages, status) {
|
|
|
3311
3644
|
try {
|
|
3312
3645
|
writeFileSync(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
|
|
3313
3646
|
} catch (e) {
|
|
3314
|
-
|
|
3647
|
+
log6.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
|
|
3315
3648
|
}
|
|
3316
3649
|
}
|
|
3317
3650
|
function installTrace(child, file, meta) {
|
|
@@ -3352,7 +3685,7 @@ async function runChildTraced(args) {
|
|
|
3352
3685
|
if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
|
|
3353
3686
|
let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
|
|
3354
3687
|
if (wc.timedOut()) {
|
|
3355
|
-
|
|
3688
|
+
log6.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
|
|
3356
3689
|
result += deadlineNote(deadlineMs);
|
|
3357
3690
|
}
|
|
3358
3691
|
await opts.hooks?.onSubagentStop?.(result, { label, agentType });
|
|
@@ -3806,7 +4139,7 @@ function reasoningToChatFragment(model, effort) {
|
|
|
3806
4139
|
}
|
|
3807
4140
|
|
|
3808
4141
|
// src/Agent.ts
|
|
3809
|
-
var
|
|
4142
|
+
var log7 = forComponent("Agent");
|
|
3810
4143
|
function envInt(key, fallback) {
|
|
3811
4144
|
const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
|
|
3812
4145
|
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
@@ -3843,6 +4176,15 @@ var AgentOptions = class {
|
|
|
3843
4176
|
* arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
|
|
3844
4177
|
* provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
|
|
3845
4178
|
stallMs = 6e4;
|
|
4179
|
+
/** IDLE bound on a NATIVE delegated tool's hold over the stall watchdog (cursor's own shell/mcp/task:
|
|
4180
|
+
* `running` → silence → `completed`). Such a hold is the provider's own unverified CLAIM that it is
|
|
4181
|
+
* busy, and it SUSPENDS the very stall detection that would catch that provider going silent — so its
|
|
4182
|
+
* only backstop was `hardCapMs = toolHoldMs`, the TRANSPORT deadline (≥30m, sized for host-tool
|
|
4183
|
+
* round-trips a native call never makes). A wedged native call therefore bought 30m of total silence,
|
|
4184
|
+
* ×2 attempts = the 61-minute zero-output run (blank 2026-09-18). Idle, not absolute: every further
|
|
4185
|
+
* `running` event `touch()`es the job, so a native tool that keeps reporting progress holds
|
|
4186
|
+
* indefinitely — what expires is silence, not work. 0 = off (hard cap only). */
|
|
4187
|
+
nativeHoldMs = 3e5;
|
|
3846
4188
|
/** Stop if the identical tool-call batch (name+args) repeats this many times in a row. 0 = off. */
|
|
3847
4189
|
maxRepeats = 3;
|
|
3848
4190
|
/** Cumulative cap on tool calls dispatched across the run. 0 = unbounded. */
|
|
@@ -3958,6 +4300,12 @@ var AgentOptions = class {
|
|
|
3958
4300
|
/** Enable `bash({background:true})` + JobOutput/JobStatus/JobKill — sandbox background jobs (overlay-isolated,
|
|
3959
4301
|
* committed on completion, drained at turn end). Useful when the VFS backend is slow (remote) or for sub-agents. */
|
|
3960
4302
|
backgroundJobs = false;
|
|
4303
|
+
/** The job registry every long-lived tool call lives in (MCP calls past their soft timeout, delegated host/native
|
|
4304
|
+
* tools, and — when the host shares it with its `ShellJobRegistry` — shell jobs). Its liveness is what the stall
|
|
4305
|
+
* watchdog and the stuck-job reaper decide from. Default: a fresh registry per Agent. Knobs (idle windows, hard
|
|
4306
|
+
* caps, MCP soft timeout) live on `JobRegistryOptions`. Completion notices reach the model via `inject` unless
|
|
4307
|
+
* the host set its own `onExit`. */
|
|
4308
|
+
jobs;
|
|
3961
4309
|
/** Plan mode: block mutating tools until the agent calls `ExitPlanMode` (host-approved). */
|
|
3962
4310
|
planMode = false;
|
|
3963
4311
|
/** Permission policy gating each tool call (allow / ask / deny). */
|
|
@@ -4024,17 +4372,18 @@ var Agent = class _Agent {
|
|
|
4024
4372
|
/** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
|
|
4025
4373
|
* runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
|
|
4026
4374
|
invokedTools = /* @__PURE__ */ new Set();
|
|
4027
|
-
/**
|
|
4028
|
-
*
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
*
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4375
|
+
/** Every long-lived tool call is a job here (see AgentOptions.jobs). Delegated-runtime HOST tools (cursor
|
|
4376
|
+
* toolExecutor) and NATIVE tools (cursor's own shell/mcp/task, `running` → `completed`) are tracked as
|
|
4377
|
+
* `holds` jobs: while one runs the provider is not silent — it is waiting on a tool — so the idle-stall
|
|
4378
|
+
* watchdog must not fire. Each is bounded by the transport tool deadline (`hardCapMs = toolHoldMs`), so a
|
|
4379
|
+
* tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection.
|
|
4380
|
+
* A NATIVE job carries a second, much tighter bound (`idleMs = nativeHoldMs`): the transport deadline is
|
|
4381
|
+
* sized for a host-tool round-trip it never makes, and silence is the only thing it can actually prove. */
|
|
4382
|
+
jobs;
|
|
4383
|
+
/** Native delegated tool activity id → its job (per step attempt). */
|
|
4384
|
+
nativeJobs = /* @__PURE__ */ new Map();
|
|
4037
4385
|
toolHoldMs = 0;
|
|
4386
|
+
nativeHoldMs = 0;
|
|
4038
4387
|
/** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
|
|
4039
4388
|
* from the pre-step transcript, which would re-run it — so such a stall is not retried. */
|
|
4040
4389
|
toolRanThisAttempt = false;
|
|
@@ -4082,6 +4431,7 @@ var Agent = class _Agent {
|
|
|
4082
4431
|
addTools(tools) {
|
|
4083
4432
|
this.injectedTools.push(...tools);
|
|
4084
4433
|
this.activeTools.push(...tools);
|
|
4434
|
+
if (this.prepared && tools.some(isMcpTool) && !this.activeTools.some((t) => t.name === "JobWait")) this.activeTools.push(...makeJobTools(this.jobs));
|
|
4085
4435
|
}
|
|
4086
4436
|
/** Remove tools by name from a running agent. Returns the count removed. Also filters
|
|
4087
4437
|
* `options.tools` — otherwise a constructor-passed tool resurrects on the next prepare()
|
|
@@ -4133,6 +4483,11 @@ var Agent = class _Agent {
|
|
|
4133
4483
|
}
|
|
4134
4484
|
constructor(options) {
|
|
4135
4485
|
this.options = { ...new AgentOptions(), ...options };
|
|
4486
|
+
this.jobs = this.options.jobs ?? new JobRegistry();
|
|
4487
|
+
this.jobs.options.onExit ??= (n) => {
|
|
4488
|
+
const imgs = n.result?.images;
|
|
4489
|
+
this.inject(imgs?.length ? [{ type: "text", text: formatJobNotice(n) }, ...imgs.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : formatJobNotice(n));
|
|
4490
|
+
};
|
|
4136
4491
|
if (this.options.fs) this.buildCtx();
|
|
4137
4492
|
}
|
|
4138
4493
|
/** Build the tool context from the resolved fs + options. Idempotent-safe: called once (ctor or ensureFs). */
|
|
@@ -4144,7 +4499,8 @@ var Agent = class _Agent {
|
|
|
4144
4499
|
this.ctx.ai = this.options.ai;
|
|
4145
4500
|
this.ctx.model = this.options.model;
|
|
4146
4501
|
this.ctx.parkHuman = (p) => this.park(p);
|
|
4147
|
-
|
|
4502
|
+
this.ctx.toolJobs = this.jobs;
|
|
4503
|
+
if (this.options.backgroundJobs) this.ctx.jobs = this.jobs;
|
|
4148
4504
|
}
|
|
4149
4505
|
/**
|
|
4150
4506
|
* Resolve the filesystem + build the tool context if the ctor couldn't (no `fs` was passed). The disk
|
|
@@ -4160,7 +4516,7 @@ var Agent = class _Agent {
|
|
|
4160
4516
|
const disk = new NodeDiskFilesystem2(process.cwd());
|
|
4161
4517
|
await disk.init();
|
|
4162
4518
|
this.options.fs = new JailedFilesystem2(disk);
|
|
4163
|
-
|
|
4519
|
+
log7.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
|
|
4164
4520
|
}
|
|
4165
4521
|
this.buildCtx();
|
|
4166
4522
|
}
|
|
@@ -4230,7 +4586,7 @@ var Agent = class _Agent {
|
|
|
4230
4586
|
tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
|
|
4231
4587
|
}
|
|
4232
4588
|
if (o.checkpoints) tools = [...tools, ...checkpointTools()];
|
|
4233
|
-
if (this.ctx.jobs) tools = [...tools, ...makeJobTools(this.
|
|
4589
|
+
if (this.ctx.jobs || [...tools, ...this.injectedTools].some(isMcpTool)) tools = [...tools, ...makeJobTools(this.jobs)];
|
|
4234
4590
|
const plan = o.planMode ? planMode({ host: o.host }) : void 0;
|
|
4235
4591
|
if (plan) tools = [...tools, plan.tool];
|
|
4236
4592
|
this.activeHooks = composeHooks(o.hooks, plan?.hooks, o.permissions?.hooks());
|
|
@@ -4346,8 +4702,8 @@ var Agent = class _Agent {
|
|
|
4346
4702
|
let closureNudged = false;
|
|
4347
4703
|
let lastProgressAt = start;
|
|
4348
4704
|
const kill = (finishReason) => {
|
|
4349
|
-
|
|
4350
|
-
this.
|
|
4705
|
+
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` : ""})`);
|
|
4706
|
+
this.jobs.killAll(`run ended: ${finishReason}`, true);
|
|
4351
4707
|
const claims = this.survivingClaims();
|
|
4352
4708
|
return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
|
|
4353
4709
|
};
|
|
@@ -4369,22 +4725,26 @@ var Agent = class _Agent {
|
|
|
4369
4725
|
const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
|
|
4370
4726
|
const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
|
|
4371
4727
|
this.toolHoldMs = effectiveToolTimeoutMs;
|
|
4728
|
+
this.nativeHoldMs = o.nativeHoldMs;
|
|
4372
4729
|
if (isCursorWithTools) {
|
|
4373
4730
|
if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
|
|
4374
|
-
|
|
4731
|
+
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
4732
|
}
|
|
4376
|
-
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs))
|
|
4733
|
+
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log7.warn(v);
|
|
4377
4734
|
}
|
|
4378
4735
|
const cursorPo = isCursorWithTools ? {
|
|
4379
4736
|
toolTimeoutMs: effectiveToolTimeoutMs,
|
|
4380
4737
|
toolExecutor: async (name, args) => {
|
|
4381
4738
|
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);
|
|
4739
|
+
const job = this.jobs.track({ kind: "host", label: name, holds: true, hardCapMs: effectiveToolTimeoutMs });
|
|
4384
4740
|
try {
|
|
4385
|
-
|
|
4741
|
+
const r = await this.dispatch(tc);
|
|
4742
|
+
job.finish("done");
|
|
4743
|
+
return r;
|
|
4744
|
+
} catch (e) {
|
|
4745
|
+
job.finish("error", void 0, String(e));
|
|
4746
|
+
throw e;
|
|
4386
4747
|
} finally {
|
|
4387
|
-
this.hostToolsInFlight--;
|
|
4388
4748
|
this.toolRanThisAttempt = true;
|
|
4389
4749
|
this.stallPoke?.();
|
|
4390
4750
|
}
|
|
@@ -4398,7 +4758,8 @@ var Agent = class _Agent {
|
|
|
4398
4758
|
};
|
|
4399
4759
|
try {
|
|
4400
4760
|
for (let attempt = 0; ; attempt++) {
|
|
4401
|
-
this.
|
|
4761
|
+
for (const [, h] of this.nativeJobs) if (h.status === "running") this.jobs.kill(h.id, "step attempt ended");
|
|
4762
|
+
this.nativeJobs.clear();
|
|
4402
4763
|
this.toolRanThisAttempt = false;
|
|
4403
4764
|
try {
|
|
4404
4765
|
if (useStream) {
|
|
@@ -4430,11 +4791,11 @@ var Agent = class _Agent {
|
|
|
4430
4791
|
const empty = err?.code === "empty" || err?.code === "cursor_silent";
|
|
4431
4792
|
const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
|
|
4432
4793
|
const replaysTool = stalled && this.toolRanThisAttempt;
|
|
4433
|
-
if (replaysTool)
|
|
4794
|
+
if (replaysTool) log7.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
|
|
4434
4795
|
const transient = !o.signal?.aborted && !isAbortError(err) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
|
|
4435
4796
|
if (!transient) throw err;
|
|
4436
4797
|
const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
|
|
4437
|
-
|
|
4798
|
+
log7.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
|
|
4438
4799
|
o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
|
|
4439
4800
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
4440
4801
|
}
|
|
@@ -4442,12 +4803,12 @@ var Agent = class _Agent {
|
|
|
4442
4803
|
} catch (err) {
|
|
4443
4804
|
if (err?.code === "budget") return kill("budget");
|
|
4444
4805
|
if (err?.code === "stall") {
|
|
4445
|
-
|
|
4806
|
+
log7.warn(`model stalled out after retries: ${err?.message ?? err}`);
|
|
4446
4807
|
return { ...kill("timeout"), error: err };
|
|
4447
4808
|
}
|
|
4448
4809
|
if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
|
|
4449
4810
|
if (err?.code === "empty" || err?.code === "cursor_silent") {
|
|
4450
|
-
|
|
4811
|
+
log7.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
|
|
4451
4812
|
return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err };
|
|
4452
4813
|
}
|
|
4453
4814
|
const body = err?.body ?? err?.response?.data ?? err?.error;
|
|
@@ -4459,7 +4820,7 @@ var Agent = class _Agent {
|
|
|
4459
4820
|
}
|
|
4460
4821
|
if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
|
|
4461
4822
|
const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
|
|
4462
|
-
|
|
4823
|
+
log7.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
|
|
4463
4824
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
|
|
4464
4825
|
}
|
|
4465
4826
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -4515,21 +4876,21 @@ var Agent = class _Agent {
|
|
|
4515
4876
|
if (emptyTurn && o.failEmptyTurns) {
|
|
4516
4877
|
if (!emptyNudged && canNudge()) {
|
|
4517
4878
|
emptyNudged = true;
|
|
4518
|
-
|
|
4879
|
+
log7.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
|
|
4519
4880
|
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
4881
|
continue;
|
|
4521
4882
|
}
|
|
4522
|
-
|
|
4883
|
+
log7.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
|
|
4523
4884
|
const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
|
|
4524
4885
|
e.code = "empty";
|
|
4525
4886
|
return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
|
|
4526
4887
|
}
|
|
4527
4888
|
const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
|
|
4528
4889
|
for (const n of fabricated) this.fabricatedClaims.add(n);
|
|
4529
|
-
if (fabricated.length && !canNudge())
|
|
4890
|
+
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
4891
|
if (fabricated.length && canNudge()) {
|
|
4531
4892
|
closureNudged = true;
|
|
4532
|
-
|
|
4893
|
+
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
4894
|
this.transcript.push({
|
|
4534
4895
|
role: "user",
|
|
4535
4896
|
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 +4899,18 @@ var Agent = class _Agent {
|
|
|
4538
4899
|
}
|
|
4539
4900
|
if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
|
|
4540
4901
|
closureNudged = true;
|
|
4541
|
-
|
|
4902
|
+
log7.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
|
|
4542
4903
|
this.transcript.push({
|
|
4543
4904
|
role: "user",
|
|
4544
4905
|
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
4906
|
});
|
|
4546
4907
|
continue;
|
|
4547
4908
|
}
|
|
4548
|
-
|
|
4549
|
-
await this.
|
|
4909
|
+
log7.verbose(`completed in ${steps} step(s)`);
|
|
4910
|
+
const drained = await this.jobs.drain(o.signal);
|
|
4911
|
+
if (o.signal?.aborted) return kill("aborted");
|
|
4912
|
+
if (drained.length) lastProgressAt = Date.now();
|
|
4913
|
+
if (this.drainInjections()) continue;
|
|
4550
4914
|
this.releaseStaleImages();
|
|
4551
4915
|
await this.activeHooks?.onStop?.(res.content ?? "");
|
|
4552
4916
|
const claims = this.survivingClaims();
|
|
@@ -4572,11 +4936,12 @@ var Agent = class _Agent {
|
|
|
4572
4936
|
}
|
|
4573
4937
|
this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
|
|
4574
4938
|
}
|
|
4939
|
+
lastProgressAt = Date.now();
|
|
4575
4940
|
if (this.ctx.needsInput) {
|
|
4576
4941
|
const question = this.ctx.needsInput;
|
|
4577
4942
|
this.ctx.needsInput = void 0;
|
|
4578
|
-
|
|
4579
|
-
await this.
|
|
4943
|
+
log7.warn(`needs_input: ${question.question} (steps=${steps})`);
|
|
4944
|
+
await this.jobs.drain();
|
|
4580
4945
|
const text = [
|
|
4581
4946
|
`OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
|
|
4582
4947
|
...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
|
|
@@ -4614,7 +4979,15 @@ var Agent = class _Agent {
|
|
|
4614
4979
|
if (!stallMs) return;
|
|
4615
4980
|
if (timer) clearTimeout(timer);
|
|
4616
4981
|
timer = setTimeout(() => {
|
|
4617
|
-
if (this.toolHoldActive())
|
|
4982
|
+
if (this.toolHoldActive()) {
|
|
4983
|
+
const now3 = Date.now();
|
|
4984
|
+
const who = this.jobs.holding().map((j) => `${j.kind} "${j.label}" (${Math.round((now3 - j.startedAt) / 1e3)}s, quiet ${Math.round((now3 - j.lastActivityAt) / 1e3)}s)`).join(", ");
|
|
4985
|
+
log7.warn(`stall watchdog: no stream output for ${stallMs}ms \u2014 held by ${who}`);
|
|
4986
|
+
this.options.host?.notify?.({ kind: "tool_hold", message: `waiting on ${who}` });
|
|
4987
|
+
return poke();
|
|
4988
|
+
}
|
|
4989
|
+
const inFlight = this.jobs.list().filter((j) => j.status === "running");
|
|
4990
|
+
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
4991
|
fired = true;
|
|
4619
4992
|
ac.abort(stallError());
|
|
4620
4993
|
}, stallMs);
|
|
@@ -4634,12 +5007,11 @@ var Agent = class _Agent {
|
|
|
4634
5007
|
reclassify: (err) => stalled() ? stallError() : err
|
|
4635
5008
|
};
|
|
4636
5009
|
}
|
|
4637
|
-
/** A host or native delegated tool is running
|
|
5010
|
+
/** A host or native delegated tool is running and not stuck — the provider is quiet, not stalled. The reap runs
|
|
5011
|
+
* FIRST, so a holder past its deadline is released (logged with evidence) instead of holding forever. */
|
|
4638
5012
|
toolHoldActive() {
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
for (const until of this.nativeToolsInFlight.values()) if (now3 < until) return true;
|
|
4642
|
-
return false;
|
|
5013
|
+
this.jobs.reap();
|
|
5014
|
+
return this.jobs.holding().length > 0;
|
|
4643
5015
|
}
|
|
4644
5016
|
/** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
|
|
4645
5017
|
* non-result the model produced by going silent. Callers retry it rather than report "done". */
|
|
@@ -4679,10 +5051,16 @@ var Agent = class _Agent {
|
|
|
4679
5051
|
if (a.output !== void 0) prev.output = a.output;
|
|
4680
5052
|
prev.status = a.status;
|
|
4681
5053
|
}
|
|
5054
|
+
const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
|
|
4682
5055
|
if (a.status === "running") {
|
|
4683
|
-
if (
|
|
5056
|
+
if (nj) nj.touch();
|
|
5057
|
+
else if (a.id) this.nativeJobs.set(a.id, this.jobs.track({ kind: "native", label: a.name, holds: true, idleMs: this.nativeHoldMs, hardCapMs: this.toolHoldMs }));
|
|
4684
5058
|
} else {
|
|
4685
|
-
if (
|
|
5059
|
+
if (nj) {
|
|
5060
|
+
if (typeof a.output === "string") nj.chunk(a.output.slice(-4e3));
|
|
5061
|
+
nj.finish(a.status === "error" ? "error" : "done");
|
|
5062
|
+
this.nativeJobs.delete(a.id);
|
|
5063
|
+
}
|
|
4686
5064
|
this.toolRanThisAttempt = true;
|
|
4687
5065
|
}
|
|
4688
5066
|
sawTool = true;
|
|
@@ -4773,13 +5151,13 @@ var Agent = class _Agent {
|
|
|
4773
5151
|
const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
|
|
4774
5152
|
if (decision?.block) {
|
|
4775
5153
|
const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
|
|
4776
|
-
|
|
5154
|
+
log7.debug(`${tc.function.name} -> ${blocked}`);
|
|
4777
5155
|
await hooks?.postToolUse?.(call, blocked, meta);
|
|
4778
5156
|
return blocked;
|
|
4779
5157
|
}
|
|
4780
5158
|
this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
|
|
4781
5159
|
if (earlyError) {
|
|
4782
|
-
|
|
5160
|
+
log7.debug(`${tc.function.name} -> ${earlyError}`);
|
|
4783
5161
|
await hooks?.postToolUse?.(call, earlyError, meta);
|
|
4784
5162
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
|
|
4785
5163
|
return earlyError;
|
|
@@ -4788,12 +5166,12 @@ var Agent = class _Agent {
|
|
|
4788
5166
|
let images;
|
|
4789
5167
|
let threw = false;
|
|
4790
5168
|
try {
|
|
4791
|
-
|
|
5169
|
+
log7.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
4792
5170
|
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
4793
5171
|
try {
|
|
4794
5172
|
hooks.onToolOutput(call, chunk, meta);
|
|
4795
5173
|
} catch (e) {
|
|
4796
|
-
|
|
5174
|
+
log7.debug(`onToolOutput hook error: ${e}`);
|
|
4797
5175
|
}
|
|
4798
5176
|
} : void 0;
|
|
4799
5177
|
const raw = await tool.run(args, this.ctx);
|
|
@@ -4805,7 +5183,7 @@ var Agent = class _Agent {
|
|
|
4805
5183
|
}
|
|
4806
5184
|
} catch (e) {
|
|
4807
5185
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4808
|
-
|
|
5186
|
+
log7.debug(`${tc.function.name} -> error: ${msg}`);
|
|
4809
5187
|
result = `Error: ${msg}`;
|
|
4810
5188
|
threw = true;
|
|
4811
5189
|
} finally {
|
|
@@ -4897,6 +5275,7 @@ function estimateTokens(m, keepRecentImages = 1) {
|
|
|
4897
5275
|
return Math.ceil(sendBytes(m, keepRecentImages) / 4);
|
|
4898
5276
|
}
|
|
4899
5277
|
var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
|
|
5278
|
+
var isMcpTool = (t) => t.name.startsWith("mcp__") || t.name === "McpCall";
|
|
4900
5279
|
function transportHeadroomMs(t) {
|
|
4901
5280
|
const d = Number(t.maxDurationMs) || 0;
|
|
4902
5281
|
if (!d) return 0;
|
|
@@ -5072,7 +5451,7 @@ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
|
|
|
5072
5451
|
const ids = callIdSet(messages.slice(from));
|
|
5073
5452
|
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
5074
5453
|
if (total > maxTokens)
|
|
5075
|
-
|
|
5454
|
+
log7.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
5076
5455
|
return [...head, ...messages.slice(from)];
|
|
5077
5456
|
}
|
|
5078
5457
|
function compact(m, max, focus) {
|
|
@@ -5468,6 +5847,7 @@ var toolCall = (id, name, args) => ({
|
|
|
5468
5847
|
|
|
5469
5848
|
// src/index.ts
|
|
5470
5849
|
init_tools();
|
|
5850
|
+
init_tools_jobs();
|
|
5471
5851
|
|
|
5472
5852
|
// src/scheduler.ts
|
|
5473
5853
|
function parseCronField(field, min, max) {
|
|
@@ -5742,7 +6122,7 @@ async function fullAgentOptions(opts = {}) {
|
|
|
5742
6122
|
const registry = jobs ?? new ShellJobRegistry2({ cwd, killOnExit: true });
|
|
5743
6123
|
if (onJobExit) registry.setOnExit((n) => onJobExit(formatJobExit2(n)));
|
|
5744
6124
|
const shell = [makeRealShellTool2({ cwd, registry }), ...makeShellJobTools2(registry)];
|
|
5745
|
-
return { ...rest, tools: [...tools ?? defaultTools(), ...shell] };
|
|
6125
|
+
return { ...rest, jobs: registry.jobs, tools: [...tools ?? defaultTools(), ...shell] };
|
|
5746
6126
|
}
|
|
5747
6127
|
|
|
5748
6128
|
// src/index.ts
|
|
@@ -5754,7 +6134,7 @@ init_tools_web();
|
|
|
5754
6134
|
init_tools();
|
|
5755
6135
|
init_tools_structured();
|
|
5756
6136
|
init_logging();
|
|
5757
|
-
var
|
|
6137
|
+
var log9 = forComponent("scratch");
|
|
5758
6138
|
var SCRATCH_DIR = "/scratch";
|
|
5759
6139
|
function shortArgs(args) {
|
|
5760
6140
|
try {
|
|
@@ -5794,7 +6174,7 @@ var Scratch = class {
|
|
|
5794
6174
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5795
6175
|
await this.fs.writeFile(path, header + raw);
|
|
5796
6176
|
} catch (e) {
|
|
5797
|
-
|
|
6177
|
+
log9.debug("scratch write failed; returning raw", e);
|
|
5798
6178
|
return raw;
|
|
5799
6179
|
}
|
|
5800
6180
|
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
@@ -5824,7 +6204,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
|
|
|
5824
6204
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
5825
6205
|
await this.fs.writeFile(path, header + full);
|
|
5826
6206
|
} catch (e) {
|
|
5827
|
-
|
|
6207
|
+
log9.debug("scratch spill failed; cropping lossy", e);
|
|
5828
6208
|
return full.slice(0, pageBytes) + `
|
|
5829
6209
|
|
|
5830
6210
|
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
@@ -5870,7 +6250,7 @@ Question: ${q}`);
|
|
|
5870
6250
|
const answer = (res.text ?? "").trim();
|
|
5871
6251
|
return answer || "(no answer found in scratch)";
|
|
5872
6252
|
} catch (e) {
|
|
5873
|
-
|
|
6253
|
+
log9.debug("Ask peek failed", e);
|
|
5874
6254
|
return `Error querying scratch: ${e?.message ?? e}`;
|
|
5875
6255
|
}
|
|
5876
6256
|
}
|
|
@@ -5879,7 +6259,7 @@ Question: ${q}`);
|
|
|
5879
6259
|
|
|
5880
6260
|
// src/lessons.ts
|
|
5881
6261
|
init_logging();
|
|
5882
|
-
var
|
|
6262
|
+
var log10 = forComponent("Lessons");
|
|
5883
6263
|
var LessonOptionsDefaults = class {
|
|
5884
6264
|
minRepeats = 2;
|
|
5885
6265
|
};
|
|
@@ -5904,8 +6284,8 @@ function lessonCapture(options) {
|
|
|
5904
6284
|
counts.set(lesson.slug, n);
|
|
5905
6285
|
if (n < o.minRepeats) return;
|
|
5906
6286
|
written.add(lesson.slug);
|
|
5907
|
-
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) =>
|
|
5908
|
-
|
|
6287
|
+
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log10.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
|
|
6288
|
+
log10.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
|
|
5909
6289
|
}
|
|
5910
6290
|
};
|
|
5911
6291
|
}
|
|
@@ -5913,7 +6293,7 @@ function lessonCapture(options) {
|
|
|
5913
6293
|
// src/reflect.ts
|
|
5914
6294
|
init_llm();
|
|
5915
6295
|
init_logging();
|
|
5916
|
-
var
|
|
6296
|
+
var log11 = forComponent("Reflect");
|
|
5917
6297
|
async function reflectOnRun(o) {
|
|
5918
6298
|
const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
|
|
5919
6299
|
if (!digest.trim()) return null;
|
|
@@ -5929,7 +6309,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5929
6309
|
const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
|
|
5930
6310
|
text = r?.content ?? "";
|
|
5931
6311
|
} catch (e) {
|
|
5932
|
-
|
|
6312
|
+
log11.warn(`reflection call failed: ${e?.message ?? e}`);
|
|
5933
6313
|
return null;
|
|
5934
6314
|
}
|
|
5935
6315
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
@@ -5939,10 +6319,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
5939
6319
|
try {
|
|
5940
6320
|
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
5941
6321
|
} catch (e) {
|
|
5942
|
-
|
|
6322
|
+
log11.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
5943
6323
|
return null;
|
|
5944
6324
|
}
|
|
5945
|
-
|
|
6325
|
+
log11.debug(`reflection persisted ${slug2}`);
|
|
5946
6326
|
return slug2;
|
|
5947
6327
|
}
|
|
5948
6328
|
function digestRun(messages, maxChars) {
|
|
@@ -5991,7 +6371,7 @@ function forComponent2(name) {
|
|
|
5991
6371
|
}
|
|
5992
6372
|
|
|
5993
6373
|
// node_modules/@bod.ee/voice/src/core/emotion.ts
|
|
5994
|
-
var
|
|
6374
|
+
var log12 = forComponent2("Emotion");
|
|
5995
6375
|
var EMOTIONS = [
|
|
5996
6376
|
// primary (best results)
|
|
5997
6377
|
"neutral",
|
|
@@ -6145,7 +6525,7 @@ var EmotionStream = class {
|
|
|
6145
6525
|
if (tag) {
|
|
6146
6526
|
this.pending = tag;
|
|
6147
6527
|
if (this.show) display += m[0];
|
|
6148
|
-
} else
|
|
6528
|
+
} else log12.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
6149
6529
|
last = m.index + m[0].length;
|
|
6150
6530
|
}
|
|
6151
6531
|
this.emit(body.slice(last), (s, d, p) => {
|
|
@@ -6168,7 +6548,7 @@ var EmotionStream = class {
|
|
|
6168
6548
|
};
|
|
6169
6549
|
|
|
6170
6550
|
// node_modules/@bod.ee/voice/src/core/engine.ts
|
|
6171
|
-
var
|
|
6551
|
+
var log13 = forComponent2("VoiceEngine");
|
|
6172
6552
|
var realClock = {
|
|
6173
6553
|
now: () => performance.now(),
|
|
6174
6554
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -6443,7 +6823,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6443
6823
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
6444
6824
|
this.tts.warmup?.();
|
|
6445
6825
|
this.setState("listening");
|
|
6446
|
-
|
|
6826
|
+
log13.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
6447
6827
|
}
|
|
6448
6828
|
get usingAec() {
|
|
6449
6829
|
return this.stt.usingAec;
|
|
@@ -6465,7 +6845,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6465
6845
|
this.options.onDiag({ t: this.clock.now(), kind, ...fields });
|
|
6466
6846
|
} catch (e) {
|
|
6467
6847
|
this.diagOn = false;
|
|
6468
|
-
|
|
6848
|
+
log13.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
6469
6849
|
}
|
|
6470
6850
|
}
|
|
6471
6851
|
idleWaiters = [];
|
|
@@ -6574,7 +6954,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6574
6954
|
if (prose.trim()) this.repliedSinceDispatch = true;
|
|
6575
6955
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
6576
6956
|
this.tts.speak(forSpeech(speech), true);
|
|
6577
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
6957
|
+
if (!this.spokeDeltas && this.turnStartAt) log13.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
|
|
6578
6958
|
this.spokeDeltas = true;
|
|
6579
6959
|
this.setState("speaking");
|
|
6580
6960
|
if (this.options.revealMode !== "off" && display) {
|
|
@@ -6651,7 +7031,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6651
7031
|
this.drainTimer = null;
|
|
6652
7032
|
this.speaking = false;
|
|
6653
7033
|
this.stopWordReveal(true);
|
|
6654
|
-
if (this.turnStartAt)
|
|
7034
|
+
if (this.turnStartAt) log13.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
|
|
6655
7035
|
this.echoUntil = this.clock.now() + 2500;
|
|
6656
7036
|
if (!this.usingAec) this.stt.reset();
|
|
6657
7037
|
this.setState("listening");
|
|
@@ -6739,7 +7119,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6739
7119
|
this.clearAckTimer("interrupt");
|
|
6740
7120
|
this.adaptiveAckFired = false;
|
|
6741
7121
|
this.ackProbe = null;
|
|
6742
|
-
if (this.uttQueue.length)
|
|
7122
|
+
if (this.uttQueue.length) log13.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
|
|
6743
7123
|
const droppedQueued = this.uttQueue.length;
|
|
6744
7124
|
this.uttQueue = [];
|
|
6745
7125
|
if (!this.speaking && !this.drainTimer) return;
|
|
@@ -6885,7 +7265,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6885
7265
|
if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
|
|
6886
7266
|
this.specSpent = true;
|
|
6887
7267
|
this.specText = this.specPartial;
|
|
6888
|
-
|
|
7268
|
+
log13.debug(`speculate: "${this.specText.slice(0, 60)}"`);
|
|
6889
7269
|
this.diag("speculate", { text: this.specText });
|
|
6890
7270
|
this.options.onSpeculate(this.specText);
|
|
6891
7271
|
}, this.options.speculativeMs);
|
|
@@ -6954,7 +7334,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6954
7334
|
};
|
|
6955
7335
|
this.tts.newContext();
|
|
6956
7336
|
this.tts.speak(phrase, false);
|
|
6957
|
-
|
|
7337
|
+
log13.debug(`backchannel: "${phrase}"`);
|
|
6958
7338
|
this.diag("backchannel", { phrase });
|
|
6959
7339
|
this.options.onBackchannel(phrase);
|
|
6960
7340
|
}
|
|
@@ -6987,7 +7367,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6987
7367
|
});
|
|
6988
7368
|
if (idx < 0) return text;
|
|
6989
7369
|
toks.splice(idx, 1);
|
|
6990
|
-
|
|
7370
|
+
log13.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
|
|
6991
7371
|
return toks.join(" ");
|
|
6992
7372
|
}
|
|
6993
7373
|
/** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
|
|
@@ -7078,7 +7458,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7078
7458
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
7079
7459
|
if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
|
|
7080
7460
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
7081
|
-
|
|
7461
|
+
log13.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
7082
7462
|
this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
|
|
7083
7463
|
this.options.onHold();
|
|
7084
7464
|
if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
|
|
@@ -7113,13 +7493,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7113
7493
|
if (text) {
|
|
7114
7494
|
const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7115
7495
|
if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
|
|
7116
|
-
|
|
7496
|
+
log13.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
|
|
7117
7497
|
this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
|
|
7118
7498
|
return;
|
|
7119
7499
|
}
|
|
7120
7500
|
const refinal = this.refinalizes(text);
|
|
7121
7501
|
if (refinal && this.repliedSinceDispatch) {
|
|
7122
|
-
|
|
7502
|
+
log13.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
|
|
7123
7503
|
this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
|
|
7124
7504
|
this.lastDispatchFlat = flat;
|
|
7125
7505
|
this.lastDispatchWords = text.trim().split(/\s+/);
|
|
@@ -7279,7 +7659,7 @@ var SpokenSplitter = class {
|
|
|
7279
7659
|
};
|
|
7280
7660
|
|
|
7281
7661
|
// node_modules/@bod.ee/voice/src/adapters/soniox.ts
|
|
7282
|
-
var
|
|
7662
|
+
var log14 = forComponent2("SonioxSTT");
|
|
7283
7663
|
var now = () => performance.now();
|
|
7284
7664
|
var SonioxSTTOptions = class {
|
|
7285
7665
|
auth = "";
|
|
@@ -7320,7 +7700,7 @@ var SonioxSTT = class {
|
|
|
7320
7700
|
this.onDiag({ t: now(), kind, ...fields });
|
|
7321
7701
|
} catch (e) {
|
|
7322
7702
|
this.diagOn = false;
|
|
7323
|
-
|
|
7703
|
+
log14.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7324
7704
|
}
|
|
7325
7705
|
}
|
|
7326
7706
|
lastChunkAt = 0;
|
|
@@ -7362,11 +7742,11 @@ var SonioxSTT = class {
|
|
|
7362
7742
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
7363
7743
|
this.ws.onclose = (ev) => {
|
|
7364
7744
|
if (this.stopped) return;
|
|
7365
|
-
|
|
7745
|
+
log14.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
7366
7746
|
this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
|
|
7367
7747
|
this.reset();
|
|
7368
7748
|
this.connectWs().catch((e) => {
|
|
7369
|
-
|
|
7749
|
+
log14.error(`soniox reconnect failed: ${e.message}`);
|
|
7370
7750
|
this.diag("stt_reconnect_failed", { message: e.message });
|
|
7371
7751
|
});
|
|
7372
7752
|
};
|
|
@@ -7378,7 +7758,7 @@ var SonioxSTT = class {
|
|
|
7378
7758
|
this.endpointTimer = setInterval(() => {
|
|
7379
7759
|
const combined = (this.finalText + this.partialText).trim();
|
|
7380
7760
|
if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
7381
|
-
if (this.firstTokenAt)
|
|
7761
|
+
if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
7382
7762
|
this.reset();
|
|
7383
7763
|
this.onUtterance(combined, now());
|
|
7384
7764
|
}, 120);
|
|
@@ -7390,7 +7770,7 @@ var SonioxSTT = class {
|
|
|
7390
7770
|
if (this.stopped) return;
|
|
7391
7771
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
7392
7772
|
if (now() - ref > noAudioMs) {
|
|
7393
|
-
|
|
7773
|
+
log14.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
7394
7774
|
this.diag("stt_watchdog_fatal", { noAudioMs });
|
|
7395
7775
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
7396
7776
|
this.stop();
|
|
@@ -7413,7 +7793,7 @@ var SonioxSTT = class {
|
|
|
7413
7793
|
handle(m) {
|
|
7414
7794
|
if (m.error_message) {
|
|
7415
7795
|
this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
|
|
7416
|
-
return
|
|
7796
|
+
return log14.error(`soniox: ${m.error_message}`);
|
|
7417
7797
|
}
|
|
7418
7798
|
let endpoint = false;
|
|
7419
7799
|
for (const t of m.tokens ?? []) {
|
|
@@ -7430,7 +7810,7 @@ var SonioxSTT = class {
|
|
|
7430
7810
|
this.onPartial(combined);
|
|
7431
7811
|
if (endpoint && this.finalText.trim()) {
|
|
7432
7812
|
const utterance = this.finalText.trim();
|
|
7433
|
-
if (this.firstTokenAt)
|
|
7813
|
+
if (this.firstTokenAt) log14.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
7434
7814
|
this.reset();
|
|
7435
7815
|
this.onUtterance(utterance, now());
|
|
7436
7816
|
}
|
|
@@ -7452,7 +7832,7 @@ var SonioxSTT = class {
|
|
|
7452
7832
|
};
|
|
7453
7833
|
|
|
7454
7834
|
// node_modules/@bod.ee/voice/src/adapters/cartesia.ts
|
|
7455
|
-
var
|
|
7835
|
+
var log15 = forComponent2("CartesiaTTS");
|
|
7456
7836
|
var now2 = () => performance.now();
|
|
7457
7837
|
var CartesiaTTSOptions = class {
|
|
7458
7838
|
auth = "";
|
|
@@ -7488,7 +7868,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7488
7868
|
this.onDiag({ t: now2(), kind, ...fields });
|
|
7489
7869
|
} catch (e) {
|
|
7490
7870
|
this.diagOn = false;
|
|
7491
|
-
|
|
7871
|
+
log15.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
7492
7872
|
}
|
|
7493
7873
|
}
|
|
7494
7874
|
firstAudioAt = 0;
|
|
@@ -7523,11 +7903,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7523
7903
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
7524
7904
|
});
|
|
7525
7905
|
this.ws.onclose = (ev) => {
|
|
7526
|
-
|
|
7906
|
+
log15.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
7527
7907
|
this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
|
|
7528
7908
|
if (!this.closed) {
|
|
7529
7909
|
this.connecting = this.doConnect().catch((e) => {
|
|
7530
|
-
|
|
7910
|
+
log15.error(`cartesia reconnect failed: ${e.message}`);
|
|
7531
7911
|
this.diag("tts_reconnect_failed", { message: e.message });
|
|
7532
7912
|
});
|
|
7533
7913
|
}
|
|
@@ -7555,12 +7935,12 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7555
7935
|
this.down = true;
|
|
7556
7936
|
this.downAt = now2();
|
|
7557
7937
|
this.consecutiveOk = 0;
|
|
7558
|
-
|
|
7938
|
+
log15.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
7559
7939
|
this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
|
|
7560
7940
|
this.onDone();
|
|
7561
7941
|
this.startProbe();
|
|
7562
7942
|
} else if (!this.down) {
|
|
7563
|
-
(/No valid transcripts/i.test(m.message || "") ?
|
|
7943
|
+
(/No valid transcripts/i.test(m.message || "") ? log15.debug : log15.warn)(`cartesia: ${JSON.stringify(m)}`);
|
|
7564
7944
|
}
|
|
7565
7945
|
}
|
|
7566
7946
|
};
|
|
@@ -7575,7 +7955,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
7575
7955
|
this.stopProbe();
|
|
7576
7956
|
const downMs = this.downAt ? now2() - this.downAt : 0;
|
|
7577
7957
|
this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
|
|
7578
|
-
(downMs < 2e3 ?
|
|
7958
|
+
(downMs < 2e3 ? log15.debug : log15.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
7579
7959
|
}
|
|
7580
7960
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
7581
7961
|
async ensureConnected() {
|
|
@@ -7666,7 +8046,7 @@ configureLogging((name) => {
|
|
|
7666
8046
|
});
|
|
7667
8047
|
|
|
7668
8048
|
// src/duplex.ts
|
|
7669
|
-
var
|
|
8049
|
+
var log16 = forComponent("DuplexAgent");
|
|
7670
8050
|
function describeCall(call) {
|
|
7671
8051
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
7672
8052
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -7877,7 +8257,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7877
8257
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
7878
8258
|
if (m) {
|
|
7879
8259
|
this.fabricationCut = true;
|
|
7880
|
-
|
|
8260
|
+
log16.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
7881
8261
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
7882
8262
|
if (!safe) return;
|
|
7883
8263
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -7972,7 +8352,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7972
8352
|
return {
|
|
7973
8353
|
preToolUse: (call) => {
|
|
7974
8354
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
7975
|
-
|
|
8355
|
+
log16.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
7976
8356
|
this.abortSpeculation();
|
|
7977
8357
|
return { block: true, reason: "Speculative turn aborted." };
|
|
7978
8358
|
}
|
|
@@ -8010,7 +8390,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8010
8390
|
try {
|
|
8011
8391
|
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
8392
|
} catch (e) {
|
|
8013
|
-
|
|
8393
|
+
log16.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
8014
8394
|
} finally {
|
|
8015
8395
|
this.nudging = false;
|
|
8016
8396
|
}
|
|
@@ -8103,7 +8483,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8103
8483
|
try {
|
|
8104
8484
|
res = await this.voice.send(spec.text);
|
|
8105
8485
|
} catch (e) {
|
|
8106
|
-
|
|
8486
|
+
log16.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
8107
8487
|
} finally {
|
|
8108
8488
|
this.voice.options.signal = prevSignal;
|
|
8109
8489
|
}
|
|
@@ -8117,7 +8497,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
8117
8497
|
if (d === "abort") {
|
|
8118
8498
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
8119
8499
|
this.speculativeAbortedCalls++;
|
|
8120
|
-
|
|
8500
|
+
log16.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
8121
8501
|
if (this.spec === spec) this.spec = void 0;
|
|
8122
8502
|
return res ?? empty;
|
|
8123
8503
|
}
|
|
@@ -8382,7 +8762,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8382
8762
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
8383
8763
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
8384
8764
|
if (cres.finishReason !== "stop") {
|
|
8385
|
-
|
|
8765
|
+
log16.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
8386
8766
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
8387
8767
|
}
|
|
8388
8768
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -8518,7 +8898,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8518
8898
|
rec.status = "done";
|
|
8519
8899
|
rec.result = res.text;
|
|
8520
8900
|
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
8521
|
-
|
|
8901
|
+
log16.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
8522
8902
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
8523
8903
|
id,
|
|
8524
8904
|
text: res.text,
|
|
@@ -8560,14 +8940,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
8560
8940
|
foldIfSuperseded(rec, note) {
|
|
8561
8941
|
if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
|
|
8562
8942
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
8563
|
-
|
|
8943
|
+
log16.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
8564
8944
|
return true;
|
|
8565
8945
|
}
|
|
8566
8946
|
failTask(rec, msg) {
|
|
8567
8947
|
this.dropAsk(rec.id);
|
|
8568
8948
|
rec.status = "error";
|
|
8569
8949
|
rec.result = msg;
|
|
8570
|
-
|
|
8950
|
+
log16.warn(`task ${rec.id} failed: ${msg}`);
|
|
8571
8951
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
8572
8952
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
8573
8953
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
@@ -8816,17 +9196,42 @@ async function withDeadline(p, ms) {
|
|
|
8816
9196
|
}
|
|
8817
9197
|
}
|
|
8818
9198
|
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.`;
|
|
9199
|
+
async function runMcpCall(name, call, ctx, timeoutMs) {
|
|
9200
|
+
const jobs = ctx?.toolJobs;
|
|
9201
|
+
const shape = (raw) => {
|
|
9202
|
+
const r2 = toResult(raw);
|
|
9203
|
+
return r2.images?.length ? r2 : r2.text;
|
|
9204
|
+
};
|
|
9205
|
+
if (!jobs) {
|
|
9206
|
+
const raw = await withDeadline(call({}), timeoutMs);
|
|
9207
|
+
return raw === TIMED_OUT ? mcpTimedOut(name, timeoutMs) : shape(raw);
|
|
9208
|
+
}
|
|
9209
|
+
const r = await jobs.runForeground(async ({ signal, touch, onChunk }) => {
|
|
9210
|
+
const raw = await call({
|
|
9211
|
+
signal,
|
|
9212
|
+
onProgress: (p) => {
|
|
9213
|
+
touch();
|
|
9214
|
+
if (p?.message) onChunk(`[progress${p.progress != null ? ` ${p.progress}${p.total != null ? `/${p.total}` : ""}` : ""}] ${p.message}
|
|
9215
|
+
`);
|
|
9216
|
+
}
|
|
9217
|
+
});
|
|
9218
|
+
const res = toResult(raw);
|
|
9219
|
+
onChunk(res.text + (res.images?.length ? `
|
|
9220
|
+
[${res.images.length} image(s) in the result]` : ""));
|
|
9221
|
+
return res;
|
|
9222
|
+
}, { kind: "mcp", label: name, idleAfterActivity: true });
|
|
9223
|
+
if (r.done) return r.value.images?.length ? r.value : r.value.text;
|
|
9224
|
+
const soft = jobs.options.foregroundMs.mcp ?? 0;
|
|
9225
|
+
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.`;
|
|
9226
|
+
}
|
|
8819
9227
|
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
|
|
8820
9228
|
return {
|
|
8821
9229
|
name: `${prefix}${spec.name}`,
|
|
8822
9230
|
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
8823
9231
|
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
8824
9232
|
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;
|
|
9233
|
+
run(args, ctx) {
|
|
9234
|
+
return runMcpCall(spec.name, (o) => callTool(spec.name, args ?? {}, o), ctx, timeoutMs);
|
|
8830
9235
|
}
|
|
8831
9236
|
};
|
|
8832
9237
|
}
|
|
@@ -8867,13 +9272,10 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
8867
9272
|
}
|
|
8868
9273
|
},
|
|
8869
9274
|
maxDurationMs: callTimeoutMs,
|
|
8870
|
-
async run({ name, args }) {
|
|
9275
|
+
async run({ name, args }, ctx) {
|
|
8871
9276
|
const n = String(name ?? "");
|
|
8872
9277
|
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;
|
|
9278
|
+
return runMcpCall(n, (o) => callTool(n, args ?? {}, o), ctx, callTimeoutMs);
|
|
8877
9279
|
}
|
|
8878
9280
|
};
|
|
8879
9281
|
return [searchTool, callMcpTool];
|
|
@@ -8893,17 +9295,17 @@ function buildMcpCatalog(servers) {
|
|
|
8893
9295
|
return { specs, routes };
|
|
8894
9296
|
}
|
|
8895
9297
|
function searchOverCatalog(servers, specs, routes, resolve2, options) {
|
|
8896
|
-
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
9298
|
+
const tools = specs.length ? makeMcpToolSearch(specs, (name, args, o) => {
|
|
8897
9299
|
const r = routes.get(name);
|
|
8898
9300
|
if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
|
|
8899
|
-
return resolve2(r.server, r.rawName, args ?? {});
|
|
9301
|
+
return resolve2(r.server, r.rawName, args ?? {}, o);
|
|
8900
9302
|
}, options) : [];
|
|
8901
9303
|
return { tools, serverNames: servers, toolCount: specs.length };
|
|
8902
9304
|
}
|
|
8903
9305
|
function makeMcpToolSearchFromMounted(mounted, options) {
|
|
8904
9306
|
const { specs, routes } = buildMcpCatalog(mounted);
|
|
8905
9307
|
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);
|
|
9308
|
+
return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args, o) => byName.get(server).client.callTool(rawName, args, o), options);
|
|
8907
9309
|
}
|
|
8908
9310
|
function makeLazyMcpToolSearch(servers, resolve2, options) {
|
|
8909
9311
|
const { specs, routes } = buildMcpCatalog(servers);
|
|
@@ -8988,6 +9390,8 @@ export {
|
|
|
8988
9390
|
IndexedDbFilesystem,
|
|
8989
9391
|
JailOptions,
|
|
8990
9392
|
JailedFilesystem,
|
|
9393
|
+
JobRegistry,
|
|
9394
|
+
JobRegistryOptions,
|
|
8991
9395
|
LessonOptionsDefaults,
|
|
8992
9396
|
MEMORY_PROMPT,
|
|
8993
9397
|
MODEL_ALIASES,
|
|
@@ -9042,6 +9446,7 @@ export {
|
|
|
9042
9446
|
expandTemplate,
|
|
9043
9447
|
findGitRoot,
|
|
9044
9448
|
forComponent,
|
|
9449
|
+
formatJobNotice,
|
|
9045
9450
|
fullAgentOptions,
|
|
9046
9451
|
getOrCreateWorktree,
|
|
9047
9452
|
getWorktreeSession,
|