0agent 1.0.11 → 1.0.12
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/dist/daemon.mjs +912 -505
- package/package.json +1 -1
package/dist/daemon.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/daemon/src/ZeroAgentDaemon.ts
|
|
2
|
-
import { writeFileSync as
|
|
3
|
-
import { resolve as
|
|
2
|
+
import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
|
|
3
|
+
import { resolve as resolve5 } from "node:path";
|
|
4
4
|
import { homedir as homedir3 } from "node:os";
|
|
5
5
|
|
|
6
6
|
// packages/core/src/graph/GraphNode.ts
|
|
@@ -1686,429 +1686,535 @@ var EntityScopedContextLoader = class {
|
|
|
1686
1686
|
};
|
|
1687
1687
|
|
|
1688
1688
|
// packages/daemon/src/AgentExecutor.ts
|
|
1689
|
-
import { spawn } from "node:child_process";
|
|
1690
|
-
import { writeFileSync, readFileSync as
|
|
1691
|
-
import { resolve as
|
|
1689
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1690
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "node:fs";
|
|
1691
|
+
import { resolve as resolve3, dirname as dirname2, relative } from "node:path";
|
|
1692
1692
|
|
|
1693
|
-
// packages/daemon/src/
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1693
|
+
// packages/daemon/src/capabilities/WebSearchCapability.ts
|
|
1694
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
1695
|
+
var WebSearchCapability = class {
|
|
1696
|
+
name = "web_search";
|
|
1697
|
+
description = "Search the web. Returns titles, URLs, and snippets. No API key needed.";
|
|
1698
|
+
toolDefinition = {
|
|
1699
|
+
name: "web_search",
|
|
1700
|
+
description: "Search the web and return titles, URLs, and snippets. No API key needed. Use first to find pages, then scrape_url for full content.",
|
|
1698
1701
|
input_schema: {
|
|
1699
1702
|
type: "object",
|
|
1700
1703
|
properties: {
|
|
1701
|
-
|
|
1702
|
-
|
|
1704
|
+
query: { type: "string", description: "Search query" },
|
|
1705
|
+
num_results: { type: "number", description: "Number of results (default 5, max 10)" }
|
|
1703
1706
|
},
|
|
1704
|
-
required: ["
|
|
1707
|
+
required: ["query"]
|
|
1705
1708
|
}
|
|
1706
|
-
}
|
|
1707
|
-
{
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1709
|
+
};
|
|
1710
|
+
async execute(input, _cwd) {
|
|
1711
|
+
const query = String(input.query ?? "");
|
|
1712
|
+
const n = Math.min(10, Number(input.num_results ?? 5));
|
|
1713
|
+
const start = Date.now();
|
|
1714
|
+
try {
|
|
1715
|
+
const output = await this.ddgHtml(query, n);
|
|
1716
|
+
if (output && output.length > 50) {
|
|
1717
|
+
return { success: true, output, duration_ms: Date.now() - start };
|
|
1718
|
+
}
|
|
1719
|
+
} catch {
|
|
1720
|
+
}
|
|
1721
|
+
try {
|
|
1722
|
+
const output = await this.browserSearch(query, n);
|
|
1723
|
+
return { success: true, output, fallback_used: "browser", duration_ms: Date.now() - start };
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
return {
|
|
1726
|
+
success: false,
|
|
1727
|
+
output: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1728
|
+
duration_ms: Date.now() - start
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
async ddgHtml(query, n) {
|
|
1733
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=us-en`;
|
|
1734
|
+
const res = await fetch(url, {
|
|
1735
|
+
headers: {
|
|
1736
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0 Safari/537.36",
|
|
1737
|
+
"Accept": "text/html",
|
|
1738
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
1715
1739
|
},
|
|
1716
|
-
|
|
1740
|
+
signal: AbortSignal.timeout(1e4)
|
|
1741
|
+
});
|
|
1742
|
+
const html = await res.text();
|
|
1743
|
+
const titleRe = /<a[^>]+class="result__a"[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
1744
|
+
const snippetRe = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
1745
|
+
const titles = [];
|
|
1746
|
+
const snippets = [];
|
|
1747
|
+
let m;
|
|
1748
|
+
while ((m = titleRe.exec(html)) !== null && titles.length < n) {
|
|
1749
|
+
let href = m[1];
|
|
1750
|
+
const title = m[2].replace(/<[^>]+>/g, "").replace(/&/g, "&").trim();
|
|
1751
|
+
const uddg = href.match(/[?&]uddg=([^&]+)/);
|
|
1752
|
+
if (uddg) href = decodeURIComponent(uddg[1]);
|
|
1753
|
+
if (href.startsWith("http") && title) titles.push({ url: href, title });
|
|
1754
|
+
}
|
|
1755
|
+
while ((m = snippetRe.exec(html)) !== null && snippets.length < n) {
|
|
1756
|
+
snippets.push(m[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim());
|
|
1757
|
+
}
|
|
1758
|
+
if (titles.length === 0) throw new Error("No results parsed from DDG");
|
|
1759
|
+
return titles.map(
|
|
1760
|
+
(t, i) => `${i + 1}. ${t.title}
|
|
1761
|
+
URL: ${t.url}${snippets[i] ? `
|
|
1762
|
+
${snippets[i]}` : ""}`
|
|
1763
|
+
).join("\n\n");
|
|
1764
|
+
}
|
|
1765
|
+
async browserSearch(query, n) {
|
|
1766
|
+
const searchUrl = `https://duckduckgo.com/?q=${encodeURIComponent(query)}`;
|
|
1767
|
+
try {
|
|
1768
|
+
const result = spawnSync("node", ["-e", `
|
|
1769
|
+
const { chromium } = require('playwright');
|
|
1770
|
+
(async () => {
|
|
1771
|
+
const b = await chromium.launch({ headless: true });
|
|
1772
|
+
const p = await b.newPage();
|
|
1773
|
+
await p.goto('${searchUrl}', { timeout: 15000 });
|
|
1774
|
+
await p.waitForSelector('[data-result]', { timeout: 8000 }).catch(() => {});
|
|
1775
|
+
const results = await p.$$eval('[data-result]', els =>
|
|
1776
|
+
els.slice(0, ${n}).map(el => ({
|
|
1777
|
+
title: el.querySelector('h2')?.innerText ?? '',
|
|
1778
|
+
url: el.querySelector('a')?.href ?? '',
|
|
1779
|
+
snippet: el.querySelector('[data-result="snippet"]')?.innerText ?? ''
|
|
1780
|
+
}))
|
|
1781
|
+
);
|
|
1782
|
+
await b.close();
|
|
1783
|
+
console.log(JSON.stringify(results));
|
|
1784
|
+
})();
|
|
1785
|
+
`], { timeout: 25e3, encoding: "utf8" });
|
|
1786
|
+
if (result.status === 0 && result.stdout) {
|
|
1787
|
+
const results = JSON.parse(result.stdout);
|
|
1788
|
+
return results.map(
|
|
1789
|
+
(r, i) => `${i + 1}. ${r.title}
|
|
1790
|
+
URL: ${r.url}${r.snippet ? `
|
|
1791
|
+
${r.snippet}` : ""}`
|
|
1792
|
+
).join("\n\n");
|
|
1793
|
+
}
|
|
1794
|
+
} catch {
|
|
1795
|
+
}
|
|
1796
|
+
const chrome = this.findChrome();
|
|
1797
|
+
if (chrome) {
|
|
1798
|
+
const result = spawnSync(chrome, [
|
|
1799
|
+
"--headless",
|
|
1800
|
+
"--no-sandbox",
|
|
1801
|
+
"--disable-gpu",
|
|
1802
|
+
`--dump-dom`,
|
|
1803
|
+
searchUrl
|
|
1804
|
+
], { timeout: 15e3, encoding: "utf8" });
|
|
1805
|
+
if (result.stdout) {
|
|
1806
|
+
const html = result.stdout;
|
|
1807
|
+
const titles = [...html.matchAll(/<h2[^>]*>([\s\S]*?)<\/h2>/g)].map((m) => m[1].replace(/<[^>]+>/g, "").trim()).filter((t) => t.length > 5).slice(0, n);
|
|
1808
|
+
if (titles.length > 0) return titles.map((t, i) => `${i + 1}. ${t}`).join("\n");
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
throw new Error("No browser available for fallback search");
|
|
1812
|
+
}
|
|
1813
|
+
findChrome() {
|
|
1814
|
+
const candidates = [
|
|
1815
|
+
"google-chrome",
|
|
1816
|
+
"google-chrome-stable",
|
|
1817
|
+
"chromium-browser",
|
|
1818
|
+
"chromium",
|
|
1819
|
+
"/usr/bin/google-chrome",
|
|
1820
|
+
"/usr/bin/chromium-browser",
|
|
1821
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
1822
|
+
];
|
|
1823
|
+
for (const c of candidates) {
|
|
1824
|
+
try {
|
|
1825
|
+
execSync(`which "${c}" 2>/dev/null || test -f "${c}"`, { stdio: "pipe" });
|
|
1826
|
+
return c;
|
|
1827
|
+
} catch {
|
|
1828
|
+
}
|
|
1717
1829
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
// packages/daemon/src/capabilities/BrowserCapability.ts
|
|
1835
|
+
import { spawnSync as spawnSync2, execSync as execSync2 } from "node:child_process";
|
|
1836
|
+
var BrowserCapability = class {
|
|
1837
|
+
name = "browser_open";
|
|
1838
|
+
description = "Open a URL in a real browser. Returns page content, can take screenshots. Use when scrape_url fails on JS-heavy pages.";
|
|
1839
|
+
toolDefinition = {
|
|
1840
|
+
name: "browser_open",
|
|
1841
|
+
description: "Open a URL in a real headless browser. Handles JavaScript-rendered pages, SPAs, login flows. Use when scrape_url fails.",
|
|
1722
1842
|
input_schema: {
|
|
1723
1843
|
type: "object",
|
|
1724
1844
|
properties: {
|
|
1725
|
-
|
|
1845
|
+
url: { type: "string", description: "URL to open" },
|
|
1846
|
+
action: { type: "string", description: 'What to do: "read" (default), "screenshot", "click <selector>", "fill <selector> <value>"' },
|
|
1847
|
+
wait_for: { type: "string", description: "CSS selector to wait for before extracting content" },
|
|
1848
|
+
extract: { type: "string", description: "CSS selector to extract specific element text" }
|
|
1726
1849
|
},
|
|
1727
|
-
required: ["
|
|
1850
|
+
required: ["url"]
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
async execute(input, _cwd) {
|
|
1854
|
+
const url = String(input.url ?? "");
|
|
1855
|
+
const action = String(input.action ?? "read");
|
|
1856
|
+
const waitFor = input.wait_for ? String(input.wait_for) : null;
|
|
1857
|
+
const extract = input.extract ? String(input.extract) : null;
|
|
1858
|
+
const start = Date.now();
|
|
1859
|
+
if (!url.startsWith("http")) {
|
|
1860
|
+
return { success: false, output: "URL must start with http:// or https://", duration_ms: 0 };
|
|
1861
|
+
}
|
|
1862
|
+
try {
|
|
1863
|
+
const output = await this.playwrightFetch(url, action, waitFor, extract);
|
|
1864
|
+
return { success: true, output, duration_ms: Date.now() - start };
|
|
1865
|
+
} catch {
|
|
1866
|
+
}
|
|
1867
|
+
try {
|
|
1868
|
+
const output = await this.chromeFetch(url);
|
|
1869
|
+
return { success: true, output, fallback_used: "system-chrome", duration_ms: Date.now() - start };
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
const res = await fetch(url, {
|
|
1874
|
+
headers: {
|
|
1875
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0 Safari/537.36",
|
|
1876
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
1877
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
1878
|
+
"Accept-Encoding": "gzip, deflate, br"
|
|
1879
|
+
},
|
|
1880
|
+
signal: AbortSignal.timeout(15e3)
|
|
1881
|
+
});
|
|
1882
|
+
const text = await res.text();
|
|
1883
|
+
const plain = text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 5e3);
|
|
1884
|
+
return { success: true, output: plain, fallback_used: "fetch", duration_ms: Date.now() - start };
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
return {
|
|
1887
|
+
success: false,
|
|
1888
|
+
output: `Browser unavailable. Install Playwright: npx playwright install chromium`,
|
|
1889
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1890
|
+
duration_ms: Date.now() - start
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
async playwrightFetch(url, action, waitFor, extract) {
|
|
1895
|
+
const waitLine = waitFor ? `await p.waitForSelector('${waitFor}', { timeout: 8000 }).catch(() => {});` : "";
|
|
1896
|
+
const extractLine = extract ? `const text = await p.$eval('${extract}', el => el.innerText).catch(() => page_text);` : `const text = page_text;`;
|
|
1897
|
+
const script = `
|
|
1898
|
+
const { chromium } = require('playwright');
|
|
1899
|
+
(async () => {
|
|
1900
|
+
const b = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
|
1901
|
+
const p = await b.newPage();
|
|
1902
|
+
await p.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36');
|
|
1903
|
+
await p.goto('${url}', { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
1904
|
+
${waitLine}
|
|
1905
|
+
const page_text = await p.evaluate(() => document.body.innerText ?? '');
|
|
1906
|
+
${extractLine}
|
|
1907
|
+
console.log(text.slice(0, 6000));
|
|
1908
|
+
await b.close();
|
|
1909
|
+
})().catch(e => { console.error(e.message); process.exit(1); });
|
|
1910
|
+
`;
|
|
1911
|
+
const result = spawnSync2("node", ["-e", script], { timeout: 3e4, encoding: "utf8" });
|
|
1912
|
+
if (result.status !== 0) throw new Error(result.stderr || "Playwright failed");
|
|
1913
|
+
return result.stdout.trim();
|
|
1914
|
+
}
|
|
1915
|
+
async chromeFetch(url) {
|
|
1916
|
+
const candidates = [
|
|
1917
|
+
"google-chrome",
|
|
1918
|
+
"chromium-browser",
|
|
1919
|
+
"chromium",
|
|
1920
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
1921
|
+
];
|
|
1922
|
+
for (const chrome of candidates) {
|
|
1923
|
+
try {
|
|
1924
|
+
execSync2(`which "${chrome}" 2>/dev/null || test -f "${chrome}"`, { stdio: "pipe" });
|
|
1925
|
+
const result = spawnSync2(chrome, ["--headless", "--no-sandbox", "--disable-gpu", "--dump-dom", url], {
|
|
1926
|
+
timeout: 15e3,
|
|
1927
|
+
encoding: "utf8"
|
|
1928
|
+
});
|
|
1929
|
+
if (result.status === 0 && result.stdout) {
|
|
1930
|
+
return result.stdout.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 5e3);
|
|
1931
|
+
}
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1728
1934
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1935
|
+
throw new Error("No system Chrome found");
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
// packages/daemon/src/capabilities/ScraperCapability.ts
|
|
1940
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1941
|
+
var ScraperCapability = class {
|
|
1942
|
+
name = "scrape_url";
|
|
1943
|
+
description = "Scrape a URL. Tries fast HTTP fetch, then Scrapling, then browser if needed.";
|
|
1944
|
+
browser = new BrowserCapability();
|
|
1945
|
+
toolDefinition = {
|
|
1946
|
+
name: "scrape_url",
|
|
1947
|
+
description: "Scrape a URL and return clean content. Handles JS-rendered pages. Fallback chain: HTTP \u2192 Scrapling \u2192 Browser.",
|
|
1733
1948
|
input_schema: {
|
|
1734
1949
|
type: "object",
|
|
1735
1950
|
properties: {
|
|
1736
|
-
|
|
1951
|
+
url: { type: "string", description: "URL to scrape" },
|
|
1952
|
+
mode: { type: "string", description: '"text" (default), "links", "tables", "markdown"' },
|
|
1953
|
+
selector: { type: "string", description: "Optional CSS selector to target element" }
|
|
1954
|
+
},
|
|
1955
|
+
required: ["url"]
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
async execute(input, cwd) {
|
|
1959
|
+
const url = String(input.url ?? "");
|
|
1960
|
+
const mode = String(input.mode ?? "text");
|
|
1961
|
+
const selector = input.selector ? String(input.selector) : null;
|
|
1962
|
+
const start = Date.now();
|
|
1963
|
+
if (!url.startsWith("http")) {
|
|
1964
|
+
return { success: false, output: "URL must start with http:// or https://", duration_ms: 0 };
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
const output = await this.plainFetch(url, mode, selector);
|
|
1968
|
+
if (output && output.length > 100) {
|
|
1969
|
+
return { success: true, output, duration_ms: Date.now() - start };
|
|
1737
1970
|
}
|
|
1971
|
+
} catch {
|
|
1738
1972
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1973
|
+
try {
|
|
1974
|
+
const output = await this.scraplingFetch(url, mode);
|
|
1975
|
+
if (output && output.length > 100) {
|
|
1976
|
+
return { success: true, output, fallback_used: "scrapling", duration_ms: Date.now() - start };
|
|
1977
|
+
}
|
|
1978
|
+
} catch {
|
|
1979
|
+
}
|
|
1980
|
+
const browserResult = await this.browser.execute({ url, extract: selector ?? void 0 }, cwd);
|
|
1981
|
+
return { ...browserResult, fallback_used: "browser", duration_ms: Date.now() - start };
|
|
1982
|
+
}
|
|
1983
|
+
async plainFetch(url, mode, selector) {
|
|
1984
|
+
const res = await fetch(url, {
|
|
1985
|
+
headers: { "User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/120.0" },
|
|
1986
|
+
signal: AbortSignal.timeout(12e3)
|
|
1987
|
+
});
|
|
1988
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1989
|
+
const html = await res.text();
|
|
1990
|
+
if (mode === "links") {
|
|
1991
|
+
const links = [...html.matchAll(/href="(https?:\/\/[^"]+)"/g)].map((m) => m[1]).filter((v, i, a) => a.indexOf(v) === i).slice(0, 30);
|
|
1992
|
+
return links.join("\n");
|
|
1993
|
+
}
|
|
1994
|
+
if (mode === "tables") {
|
|
1995
|
+
const tables = [...html.matchAll(/<table[\s\S]*?<\/table>/gi)].map(
|
|
1996
|
+
(m) => m[0].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
|
|
1997
|
+
).slice(0, 5);
|
|
1998
|
+
return tables.join("\n---\n");
|
|
1999
|
+
}
|
|
2000
|
+
const text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
2001
|
+
return text.slice(0, 6e3) + (text.length > 6e3 ? "\n\u2026[truncated]" : "");
|
|
2002
|
+
}
|
|
2003
|
+
async scraplingFetch(url, mode) {
|
|
2004
|
+
const extractLine = mode === "links" ? `result = [a.attrib.get('href','') for a in page.find_all('a') if a.attrib.get('href','').startswith('http')]` : `result = page.get_all_text()`;
|
|
2005
|
+
const script = `
|
|
2006
|
+
import sys
|
|
2007
|
+
try:
|
|
2008
|
+
from scrapling import Fetcher
|
|
2009
|
+
except ImportError:
|
|
2010
|
+
import subprocess
|
|
2011
|
+
subprocess.run([sys.executable,'-m','pip','install','scrapling','-q'],check=True)
|
|
2012
|
+
from scrapling import Fetcher
|
|
2013
|
+
f = Fetcher(auto_match=False)
|
|
2014
|
+
page = f.get('${url}', timeout=20)
|
|
2015
|
+
${extractLine}
|
|
2016
|
+
if isinstance(result, list):
|
|
2017
|
+
print('\\n'.join(str(r) for r in result[:30]))
|
|
2018
|
+
else:
|
|
2019
|
+
t = str(result).strip()
|
|
2020
|
+
print(t[:5000] + ('...' if len(t)>5000 else ''))
|
|
2021
|
+
`.trim();
|
|
2022
|
+
const result = spawnSync3("python3", ["-c", script], { timeout: 35e3, encoding: "utf8" });
|
|
2023
|
+
if (result.status !== 0) throw new Error(result.stderr || "Scrapling failed");
|
|
2024
|
+
return result.stdout.trim();
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
|
|
2028
|
+
// packages/daemon/src/capabilities/ShellCapability.ts
|
|
2029
|
+
import { spawn } from "node:child_process";
|
|
2030
|
+
var ShellCapability = class {
|
|
2031
|
+
name = "shell_exec";
|
|
2032
|
+
description = "Execute shell commands in the working directory.";
|
|
2033
|
+
toolDefinition = {
|
|
2034
|
+
name: "shell_exec",
|
|
2035
|
+
description: "Execute a shell command. Use & for background processes. Returns stdout+stderr.",
|
|
1743
2036
|
input_schema: {
|
|
1744
2037
|
type: "object",
|
|
1745
2038
|
properties: {
|
|
1746
|
-
|
|
1747
|
-
|
|
2039
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
2040
|
+
timeout_ms: { type: "number", description: "Timeout (default 30000ms)" }
|
|
1748
2041
|
},
|
|
1749
|
-
required: ["
|
|
2042
|
+
required: ["command"]
|
|
1750
2043
|
}
|
|
1751
|
-
}
|
|
1752
|
-
{
|
|
1753
|
-
|
|
1754
|
-
|
|
2044
|
+
};
|
|
2045
|
+
async execute(input, cwd) {
|
|
2046
|
+
const command = String(input.command ?? "");
|
|
2047
|
+
const timeout = Number(input.timeout_ms ?? 3e4);
|
|
2048
|
+
const start = Date.now();
|
|
2049
|
+
return new Promise((resolve_) => {
|
|
2050
|
+
const chunks = [];
|
|
2051
|
+
const proc = spawn("bash", ["-c", command], {
|
|
2052
|
+
cwd,
|
|
2053
|
+
env: { ...process.env, TERM: "dumb" },
|
|
2054
|
+
timeout
|
|
2055
|
+
});
|
|
2056
|
+
proc.stdout.on("data", (d) => chunks.push(d.toString()));
|
|
2057
|
+
proc.stderr.on("data", (d) => chunks.push(d.toString()));
|
|
2058
|
+
proc.on("close", (code) => {
|
|
2059
|
+
const output = chunks.join("").trim();
|
|
2060
|
+
resolve_({
|
|
2061
|
+
success: code === 0,
|
|
2062
|
+
output: output || (code === 0 ? "(no output)" : `exit ${code}`),
|
|
2063
|
+
duration_ms: Date.now() - start,
|
|
2064
|
+
...code !== 0 && { error: `exit code ${code}` }
|
|
2065
|
+
});
|
|
2066
|
+
});
|
|
2067
|
+
proc.on("error", (err) => {
|
|
2068
|
+
resolve_({ success: false, output: err.message, error: err.message, duration_ms: Date.now() - start });
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
// packages/daemon/src/capabilities/FileCapability.ts
|
|
2075
|
+
import { readFileSync as readFileSync2, writeFileSync, readdirSync, mkdirSync, existsSync as existsSync2 } from "node:fs";
|
|
2076
|
+
import { resolve as resolve2, dirname } from "node:path";
|
|
2077
|
+
var FileCapability = class {
|
|
2078
|
+
name = "file_op";
|
|
2079
|
+
description = "Read, write, or list files. Scoped to working directory.";
|
|
2080
|
+
toolDefinition = {
|
|
2081
|
+
name: "file_op",
|
|
2082
|
+
description: "Read, write, or list files in the working directory.",
|
|
1755
2083
|
input_schema: {
|
|
1756
2084
|
type: "object",
|
|
1757
2085
|
properties: {
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
wait_ms: { type: "number", description: "Wait N ms after page load (for JS-heavy pages, default 0)" }
|
|
2086
|
+
op: { type: "string", description: '"read", "write", or "list"' },
|
|
2087
|
+
path: { type: "string", description: "File or directory path (relative to cwd)" },
|
|
2088
|
+
content: { type: "string", description: "Content for write operation" }
|
|
1762
2089
|
},
|
|
1763
|
-
required: ["
|
|
2090
|
+
required: ["op", "path"]
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
async execute(input, cwd) {
|
|
2094
|
+
const op = String(input.op ?? "read");
|
|
2095
|
+
const rel = String(input.path ?? ".");
|
|
2096
|
+
const safe = resolve2(cwd, rel);
|
|
2097
|
+
const start = Date.now();
|
|
2098
|
+
if (!safe.startsWith(cwd)) {
|
|
2099
|
+
return { success: false, output: "Path outside working directory", duration_ms: 0 };
|
|
2100
|
+
}
|
|
2101
|
+
try {
|
|
2102
|
+
if (op === "read") {
|
|
2103
|
+
if (!existsSync2(safe)) return { success: false, output: `Not found: ${rel}`, duration_ms: Date.now() - start };
|
|
2104
|
+
const content = readFileSync2(safe, "utf8");
|
|
2105
|
+
return {
|
|
2106
|
+
success: true,
|
|
2107
|
+
output: content.length > 8e3 ? content.slice(0, 8e3) + "\n\u2026[truncated]" : content,
|
|
2108
|
+
duration_ms: Date.now() - start
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
if (op === "write") {
|
|
2112
|
+
mkdirSync(dirname(safe), { recursive: true });
|
|
2113
|
+
writeFileSync(safe, String(input.content ?? ""), "utf8");
|
|
2114
|
+
return { success: true, output: `Written: ${rel} (${String(input.content ?? "").length} bytes)`, duration_ms: Date.now() - start };
|
|
2115
|
+
}
|
|
2116
|
+
if (op === "list") {
|
|
2117
|
+
if (!existsSync2(safe)) return { success: false, output: `Not found: ${rel}`, duration_ms: Date.now() - start };
|
|
2118
|
+
const entries = readdirSync(safe, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").map((e) => `${e.isDirectory() ? "d" : "f"} ${e.name}`).join("\n");
|
|
2119
|
+
return { success: true, output: entries || "(empty)", duration_ms: Date.now() - start };
|
|
2120
|
+
}
|
|
2121
|
+
return { success: false, output: `Unknown op: ${op}`, duration_ms: Date.now() - start };
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
return { success: false, output: `Error: ${err instanceof Error ? err.message : String(err)}`, duration_ms: Date.now() - start };
|
|
1764
2124
|
}
|
|
1765
2125
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
// packages/daemon/src/capabilities/CapabilityRegistry.ts
|
|
2129
|
+
var CapabilityRegistry = class {
|
|
2130
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
2131
|
+
constructor() {
|
|
2132
|
+
this.register(new WebSearchCapability());
|
|
2133
|
+
this.register(new BrowserCapability());
|
|
2134
|
+
this.register(new ScraperCapability());
|
|
2135
|
+
this.register(new ShellCapability());
|
|
2136
|
+
this.register(new FileCapability());
|
|
1770
2137
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
return !!this.config.api_key?.trim();
|
|
2138
|
+
register(cap) {
|
|
2139
|
+
this.capabilities.set(cap.name, cap);
|
|
1774
2140
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
const res = await this.completeWithTools(messages, [], system, void 0);
|
|
1778
|
-
return { content: res.content, tokens_used: res.tokens_used, model: res.model };
|
|
2141
|
+
get(name) {
|
|
2142
|
+
return this.capabilities.get(name);
|
|
1779
2143
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
2144
|
+
getToolDefinitions() {
|
|
2145
|
+
return [...this.capabilities.values()].map((c) => c.toolDefinition);
|
|
2146
|
+
}
|
|
2147
|
+
async execute(toolName, input, cwd) {
|
|
2148
|
+
const cap = this.capabilities.get(toolName);
|
|
2149
|
+
if (!cap) {
|
|
2150
|
+
return { success: false, output: `Unknown capability: ${toolName}`, duration_ms: 0 };
|
|
2151
|
+
}
|
|
2152
|
+
try {
|
|
2153
|
+
return await cap.execute(input, cwd);
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
return {
|
|
2156
|
+
success: false,
|
|
2157
|
+
output: `Capability error: ${err instanceof Error ? err.message : String(err)}`,
|
|
2158
|
+
duration_ms: 0
|
|
2159
|
+
};
|
|
1795
2160
|
}
|
|
1796
2161
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2162
|
+
list() {
|
|
2163
|
+
return [...this.capabilities.values()].map((c) => ({ name: c.name, description: c.description }));
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
// packages/daemon/src/AgentExecutor.ts
|
|
2168
|
+
var AgentExecutor = class {
|
|
2169
|
+
constructor(llm, config, onStep, onToken) {
|
|
2170
|
+
this.llm = llm;
|
|
2171
|
+
this.config = config;
|
|
2172
|
+
this.onStep = onStep;
|
|
2173
|
+
this.onToken = onToken;
|
|
2174
|
+
this.cwd = config.cwd;
|
|
2175
|
+
this.maxIterations = config.max_iterations ?? 20;
|
|
2176
|
+
this.maxCommandMs = config.max_command_ms ?? 3e4;
|
|
2177
|
+
this.registry = new CapabilityRegistry();
|
|
2178
|
+
}
|
|
2179
|
+
cwd;
|
|
2180
|
+
maxIterations;
|
|
2181
|
+
maxCommandMs;
|
|
2182
|
+
registry;
|
|
2183
|
+
async execute(task, systemContext) {
|
|
2184
|
+
const filesWritten = [];
|
|
2185
|
+
const commandsRun = [];
|
|
2186
|
+
let totalTokens = 0;
|
|
2187
|
+
let modelName = "";
|
|
2188
|
+
const systemPrompt = this.buildSystemPrompt(systemContext);
|
|
2189
|
+
const messages = [
|
|
2190
|
+
{ role: "user", content: task }
|
|
2191
|
+
];
|
|
2192
|
+
let finalOutput = "";
|
|
2193
|
+
for (let i = 0; i < this.maxIterations; i++) {
|
|
2194
|
+
this.onStep(i === 0 ? "Thinking\u2026" : "Continuing\u2026");
|
|
2195
|
+
let response;
|
|
2196
|
+
try {
|
|
2197
|
+
response = await this.llm.completeWithTools(
|
|
2198
|
+
messages,
|
|
2199
|
+
this.registry.getToolDefinitions(),
|
|
2200
|
+
systemPrompt,
|
|
2201
|
+
// Only stream tokens on the final (non-tool) turn
|
|
2202
|
+
(token) => {
|
|
2203
|
+
this.onToken(token);
|
|
2204
|
+
finalOutput += token;
|
|
2205
|
+
}
|
|
2206
|
+
);
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2209
|
+
this.onStep(`LLM error: ${msg}`);
|
|
2210
|
+
finalOutput = `Error: ${msg}`;
|
|
2211
|
+
break;
|
|
1807
2212
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
...m.tool_calls.map((tc) => ({
|
|
1814
|
-
type: "tool_use",
|
|
1815
|
-
id: tc.id,
|
|
1816
|
-
name: tc.name,
|
|
1817
|
-
input: tc.input
|
|
1818
|
-
}))
|
|
1819
|
-
]
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
return { role: m.role, content: m.content };
|
|
1823
|
-
});
|
|
1824
|
-
const body = {
|
|
1825
|
-
model: this.config.model,
|
|
1826
|
-
max_tokens: 8192,
|
|
1827
|
-
messages: anthropicMsgs,
|
|
1828
|
-
stream: true
|
|
1829
|
-
};
|
|
1830
|
-
if (sysContent) body.system = sysContent;
|
|
1831
|
-
if (tools.length > 0) {
|
|
1832
|
-
body.tools = tools.map((t) => ({
|
|
1833
|
-
name: t.name,
|
|
1834
|
-
description: t.description,
|
|
1835
|
-
input_schema: t.input_schema
|
|
1836
|
-
}));
|
|
1837
|
-
}
|
|
1838
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1839
|
-
method: "POST",
|
|
1840
|
-
headers: {
|
|
1841
|
-
"Content-Type": "application/json",
|
|
1842
|
-
"x-api-key": this.config.api_key,
|
|
1843
|
-
"anthropic-version": "2023-06-01"
|
|
1844
|
-
},
|
|
1845
|
-
body: JSON.stringify(body)
|
|
1846
|
-
});
|
|
1847
|
-
if (!res.ok) {
|
|
1848
|
-
const err = await res.text();
|
|
1849
|
-
throw new Error(`Anthropic ${res.status}: ${err}`);
|
|
1850
|
-
}
|
|
1851
|
-
let textContent = "";
|
|
1852
|
-
let stopReason = "end_turn";
|
|
1853
|
-
let inputTokens = 0;
|
|
1854
|
-
let outputTokens = 0;
|
|
1855
|
-
let modelName = this.config.model;
|
|
1856
|
-
const toolCalls = [];
|
|
1857
|
-
const toolInputBuffers = {};
|
|
1858
|
-
let currentToolId = "";
|
|
1859
|
-
const reader = res.body.getReader();
|
|
1860
|
-
const decoder = new TextDecoder();
|
|
1861
|
-
let buf = "";
|
|
1862
|
-
while (true) {
|
|
1863
|
-
const { done, value } = await reader.read();
|
|
1864
|
-
if (done) break;
|
|
1865
|
-
buf += decoder.decode(value, { stream: true });
|
|
1866
|
-
const lines = buf.split("\n");
|
|
1867
|
-
buf = lines.pop() ?? "";
|
|
1868
|
-
for (const line of lines) {
|
|
1869
|
-
if (!line.startsWith("data: ")) continue;
|
|
1870
|
-
const data = line.slice(6).trim();
|
|
1871
|
-
if (data === "[DONE]" || data === "") continue;
|
|
1872
|
-
let evt;
|
|
1873
|
-
try {
|
|
1874
|
-
evt = JSON.parse(data);
|
|
1875
|
-
} catch {
|
|
1876
|
-
continue;
|
|
1877
|
-
}
|
|
1878
|
-
const type = evt.type;
|
|
1879
|
-
if (type === "message_start") {
|
|
1880
|
-
const usage = evt.message?.usage;
|
|
1881
|
-
inputTokens = usage?.input_tokens ?? 0;
|
|
1882
|
-
modelName = evt.message?.model ?? modelName;
|
|
1883
|
-
} else if (type === "content_block_start") {
|
|
1884
|
-
const block = evt.content_block;
|
|
1885
|
-
if (block?.type === "tool_use") {
|
|
1886
|
-
currentToolId = block.id;
|
|
1887
|
-
toolInputBuffers[currentToolId] = "";
|
|
1888
|
-
toolCalls.push({ id: currentToolId, name: block.name, input: {} });
|
|
1889
|
-
}
|
|
1890
|
-
} else if (type === "content_block_delta") {
|
|
1891
|
-
const delta = evt.delta;
|
|
1892
|
-
if (delta?.type === "text_delta") {
|
|
1893
|
-
const token = delta.text ?? "";
|
|
1894
|
-
textContent += token;
|
|
1895
|
-
if (onToken && token) onToken(token);
|
|
1896
|
-
} else if (delta?.type === "input_json_delta") {
|
|
1897
|
-
toolInputBuffers[currentToolId] = (toolInputBuffers[currentToolId] ?? "") + (delta.partial_json ?? "");
|
|
1898
|
-
}
|
|
1899
|
-
} else if (type === "content_block_stop") {
|
|
1900
|
-
if (currentToolId && toolInputBuffers[currentToolId]) {
|
|
1901
|
-
const tc = toolCalls.find((t) => t.id === currentToolId);
|
|
1902
|
-
if (tc) {
|
|
1903
|
-
try {
|
|
1904
|
-
tc.input = JSON.parse(toolInputBuffers[currentToolId]);
|
|
1905
|
-
} catch {
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
} else if (type === "message_delta") {
|
|
1910
|
-
const usage = evt.usage;
|
|
1911
|
-
outputTokens = usage?.output_tokens ?? 0;
|
|
1912
|
-
const stop = evt.delta?.stop_reason;
|
|
1913
|
-
if (stop === "tool_use") stopReason = "tool_use";
|
|
1914
|
-
else if (stop === "end_turn") stopReason = "end_turn";
|
|
1915
|
-
else if (stop === "max_tokens") stopReason = "max_tokens";
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
return {
|
|
1920
|
-
content: textContent,
|
|
1921
|
-
tool_calls: toolCalls.length > 0 ? toolCalls : null,
|
|
1922
|
-
stop_reason: stopReason,
|
|
1923
|
-
tokens_used: inputTokens + outputTokens,
|
|
1924
|
-
model: modelName
|
|
1925
|
-
};
|
|
1926
|
-
}
|
|
1927
|
-
// ─── OpenAI (also xAI, Gemini) ───────────────────────────────────────────
|
|
1928
|
-
async openai(messages, tools, system, onToken, baseUrl = "https://api.openai.com/v1") {
|
|
1929
|
-
const allMessages = [];
|
|
1930
|
-
const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
|
|
1931
|
-
if (sysContent) allMessages.push({ role: "system", content: sysContent });
|
|
1932
|
-
for (const m of messages.filter((m2) => m2.role !== "system")) {
|
|
1933
|
-
if (m.role === "tool") {
|
|
1934
|
-
allMessages.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content });
|
|
1935
|
-
} else if (m.role === "assistant" && m.tool_calls?.length) {
|
|
1936
|
-
allMessages.push({
|
|
1937
|
-
role: "assistant",
|
|
1938
|
-
content: m.content || null,
|
|
1939
|
-
tool_calls: m.tool_calls.map((tc) => ({
|
|
1940
|
-
id: tc.id,
|
|
1941
|
-
type: "function",
|
|
1942
|
-
function: { name: tc.name, arguments: JSON.stringify(tc.input) }
|
|
1943
|
-
}))
|
|
1944
|
-
});
|
|
1945
|
-
} else {
|
|
1946
|
-
allMessages.push({ role: m.role, content: m.content });
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
const body = {
|
|
1950
|
-
model: this.config.model,
|
|
1951
|
-
messages: allMessages,
|
|
1952
|
-
max_tokens: 8192,
|
|
1953
|
-
stream: true,
|
|
1954
|
-
stream_options: { include_usage: true }
|
|
1955
|
-
};
|
|
1956
|
-
if (tools.length > 0) {
|
|
1957
|
-
body.tools = tools.map((t) => ({
|
|
1958
|
-
type: "function",
|
|
1959
|
-
function: { name: t.name, description: t.description, parameters: t.input_schema }
|
|
1960
|
-
}));
|
|
1961
|
-
}
|
|
1962
|
-
const res = await fetch(`${this.config.base_url ?? baseUrl}/chat/completions`, {
|
|
1963
|
-
method: "POST",
|
|
1964
|
-
headers: {
|
|
1965
|
-
"Content-Type": "application/json",
|
|
1966
|
-
"Authorization": `Bearer ${this.config.api_key}`
|
|
1967
|
-
},
|
|
1968
|
-
body: JSON.stringify(body)
|
|
1969
|
-
});
|
|
1970
|
-
if (!res.ok) {
|
|
1971
|
-
const err = await res.text();
|
|
1972
|
-
throw new Error(`OpenAI ${res.status}: ${err}`);
|
|
1973
|
-
}
|
|
1974
|
-
let textContent = "";
|
|
1975
|
-
let tokensUsed = 0;
|
|
1976
|
-
let modelName = this.config.model;
|
|
1977
|
-
let stopReason = "end_turn";
|
|
1978
|
-
const toolCallMap = {};
|
|
1979
|
-
const reader = res.body.getReader();
|
|
1980
|
-
const decoder = new TextDecoder();
|
|
1981
|
-
let buf = "";
|
|
1982
|
-
while (true) {
|
|
1983
|
-
const { done, value } = await reader.read();
|
|
1984
|
-
if (done) break;
|
|
1985
|
-
buf += decoder.decode(value, { stream: true });
|
|
1986
|
-
const lines = buf.split("\n");
|
|
1987
|
-
buf = lines.pop() ?? "";
|
|
1988
|
-
for (const line of lines) {
|
|
1989
|
-
if (!line.startsWith("data: ")) continue;
|
|
1990
|
-
const data = line.slice(6).trim();
|
|
1991
|
-
if (data === "[DONE]") continue;
|
|
1992
|
-
let evt;
|
|
1993
|
-
try {
|
|
1994
|
-
evt = JSON.parse(data);
|
|
1995
|
-
} catch {
|
|
1996
|
-
continue;
|
|
1997
|
-
}
|
|
1998
|
-
modelName = evt.model ?? modelName;
|
|
1999
|
-
const usage = evt.usage;
|
|
2000
|
-
if (usage?.total_tokens) tokensUsed = usage.total_tokens;
|
|
2001
|
-
const choices = evt.choices;
|
|
2002
|
-
if (!choices?.length) continue;
|
|
2003
|
-
const delta = choices[0].delta;
|
|
2004
|
-
if (!delta) continue;
|
|
2005
|
-
const finish = choices[0].finish_reason;
|
|
2006
|
-
if (finish === "tool_calls") stopReason = "tool_use";
|
|
2007
|
-
else if (finish === "stop") stopReason = "end_turn";
|
|
2008
|
-
const token = delta.content;
|
|
2009
|
-
if (token) {
|
|
2010
|
-
textContent += token;
|
|
2011
|
-
if (onToken) onToken(token);
|
|
2012
|
-
}
|
|
2013
|
-
const toolCallDeltas = delta.tool_calls;
|
|
2014
|
-
if (toolCallDeltas) {
|
|
2015
|
-
for (const tc of toolCallDeltas) {
|
|
2016
|
-
const idx = tc.index;
|
|
2017
|
-
if (!toolCallMap[idx]) {
|
|
2018
|
-
toolCallMap[idx] = { id: "", name: "", args: "" };
|
|
2019
|
-
}
|
|
2020
|
-
const fn = tc.function;
|
|
2021
|
-
if (tc.id) toolCallMap[idx].id = tc.id;
|
|
2022
|
-
if (fn?.name) toolCallMap[idx].name = fn.name;
|
|
2023
|
-
if (fn?.arguments) toolCallMap[idx].args += fn.arguments;
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
const toolCalls = Object.values(toolCallMap).filter((tc) => tc.id && tc.name).map((tc) => {
|
|
2029
|
-
let input = {};
|
|
2030
|
-
try {
|
|
2031
|
-
input = JSON.parse(tc.args);
|
|
2032
|
-
} catch {
|
|
2033
|
-
}
|
|
2034
|
-
return { id: tc.id, name: tc.name, input };
|
|
2035
|
-
});
|
|
2036
|
-
return {
|
|
2037
|
-
content: textContent,
|
|
2038
|
-
tool_calls: toolCalls.length > 0 ? toolCalls : null,
|
|
2039
|
-
stop_reason: stopReason,
|
|
2040
|
-
tokens_used: tokensUsed,
|
|
2041
|
-
model: modelName
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
// ─── Ollama (no streaming for simplicity) ────────────────────────────────
|
|
2045
|
-
async ollama(messages, system, onToken) {
|
|
2046
|
-
const baseUrl = this.config.base_url ?? "http://localhost:11434";
|
|
2047
|
-
const allMessages = [];
|
|
2048
|
-
const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
|
|
2049
|
-
if (sysContent) allMessages.push({ role: "system", content: sysContent });
|
|
2050
|
-
allMessages.push(...messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
|
|
2051
|
-
const res = await fetch(`${baseUrl}/api/chat`, {
|
|
2052
|
-
method: "POST",
|
|
2053
|
-
headers: { "Content-Type": "application/json" },
|
|
2054
|
-
body: JSON.stringify({ model: this.config.model, messages: allMessages, stream: false })
|
|
2055
|
-
});
|
|
2056
|
-
if (!res.ok) throw new Error(`Ollama error ${res.status}`);
|
|
2057
|
-
const data = await res.json();
|
|
2058
|
-
if (onToken) onToken(data.message.content);
|
|
2059
|
-
return { content: data.message.content, tool_calls: null, stop_reason: "end_turn", tokens_used: data.eval_count ?? 0, model: this.config.model };
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2062
|
-
|
|
2063
|
-
// packages/daemon/src/AgentExecutor.ts
|
|
2064
|
-
var AgentExecutor = class {
|
|
2065
|
-
constructor(llm, config, onStep, onToken) {
|
|
2066
|
-
this.llm = llm;
|
|
2067
|
-
this.config = config;
|
|
2068
|
-
this.onStep = onStep;
|
|
2069
|
-
this.onToken = onToken;
|
|
2070
|
-
this.cwd = config.cwd;
|
|
2071
|
-
this.maxIterations = config.max_iterations ?? 20;
|
|
2072
|
-
this.maxCommandMs = config.max_command_ms ?? 3e4;
|
|
2073
|
-
}
|
|
2074
|
-
cwd;
|
|
2075
|
-
maxIterations;
|
|
2076
|
-
maxCommandMs;
|
|
2077
|
-
async execute(task, systemContext) {
|
|
2078
|
-
const filesWritten = [];
|
|
2079
|
-
const commandsRun = [];
|
|
2080
|
-
let totalTokens = 0;
|
|
2081
|
-
let modelName = "";
|
|
2082
|
-
const systemPrompt = this.buildSystemPrompt(systemContext);
|
|
2083
|
-
const messages = [
|
|
2084
|
-
{ role: "user", content: task }
|
|
2085
|
-
];
|
|
2086
|
-
let finalOutput = "";
|
|
2087
|
-
for (let i = 0; i < this.maxIterations; i++) {
|
|
2088
|
-
this.onStep(i === 0 ? "Thinking\u2026" : "Continuing\u2026");
|
|
2089
|
-
let response;
|
|
2090
|
-
try {
|
|
2091
|
-
response = await this.llm.completeWithTools(
|
|
2092
|
-
messages,
|
|
2093
|
-
AGENT_TOOLS,
|
|
2094
|
-
systemPrompt,
|
|
2095
|
-
// Only stream tokens on the final (non-tool) turn
|
|
2096
|
-
(token) => {
|
|
2097
|
-
this.onToken(token);
|
|
2098
|
-
finalOutput += token;
|
|
2099
|
-
}
|
|
2100
|
-
);
|
|
2101
|
-
} catch (err) {
|
|
2102
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2103
|
-
this.onStep(`LLM error: ${msg}`);
|
|
2104
|
-
finalOutput = `Error: ${msg}`;
|
|
2105
|
-
break;
|
|
2106
|
-
}
|
|
2107
|
-
totalTokens += response.tokens_used;
|
|
2108
|
-
modelName = response.model;
|
|
2109
|
-
if (response.stop_reason === "end_turn" || !response.tool_calls?.length) {
|
|
2110
|
-
if (!finalOutput && response.content) finalOutput = response.content;
|
|
2111
|
-
break;
|
|
2213
|
+
totalTokens += response.tokens_used;
|
|
2214
|
+
modelName = response.model;
|
|
2215
|
+
if (response.stop_reason === "end_turn" || !response.tool_calls?.length) {
|
|
2216
|
+
if (!finalOutput && response.content) finalOutput = response.content;
|
|
2217
|
+
break;
|
|
2112
2218
|
}
|
|
2113
2219
|
finalOutput = "";
|
|
2114
2220
|
messages.push({
|
|
@@ -2120,8 +2226,12 @@ var AgentExecutor = class {
|
|
|
2120
2226
|
this.onStep(`\u25B6 ${tc.name}(${this.summariseInput(tc.name, tc.input)})`);
|
|
2121
2227
|
let result;
|
|
2122
2228
|
try {
|
|
2123
|
-
|
|
2124
|
-
|
|
2229
|
+
const capResult = await this.registry.execute(tc.name, tc.input, this.cwd);
|
|
2230
|
+
result = capResult.output;
|
|
2231
|
+
if (capResult.fallback_used) {
|
|
2232
|
+
this.onStep(` (used fallback: ${capResult.fallback_used})`);
|
|
2233
|
+
}
|
|
2234
|
+
if (tc.name === "file_op" && tc.input.op === "write" && tc.input.path) {
|
|
2125
2235
|
filesWritten.push(String(tc.input.path));
|
|
2126
2236
|
}
|
|
2127
2237
|
if (tc.name === "shell_exec" && tc.input.command) {
|
|
@@ -2178,9 +2288,9 @@ var AgentExecutor = class {
|
|
|
2178
2288
|
}
|
|
2179
2289
|
}
|
|
2180
2290
|
shellExec(command, timeoutMs) {
|
|
2181
|
-
return new Promise((
|
|
2291
|
+
return new Promise((resolve7) => {
|
|
2182
2292
|
const chunks = [];
|
|
2183
|
-
const proc =
|
|
2293
|
+
const proc = spawn2("bash", ["-c", command], {
|
|
2184
2294
|
cwd: this.cwd,
|
|
2185
2295
|
env: { ...process.env, TERM: "dumb" },
|
|
2186
2296
|
timeout: timeoutMs
|
|
@@ -2189,26 +2299,26 @@ var AgentExecutor = class {
|
|
|
2189
2299
|
proc.stderr.on("data", (d) => chunks.push(d.toString()));
|
|
2190
2300
|
proc.on("close", (code) => {
|
|
2191
2301
|
const output = chunks.join("").trim();
|
|
2192
|
-
|
|
2302
|
+
resolve7(output || (code === 0 ? "(command completed, no output)" : `exit code ${code}`));
|
|
2193
2303
|
});
|
|
2194
2304
|
proc.on("error", (err) => {
|
|
2195
|
-
|
|
2305
|
+
resolve7(`Error: ${err.message}`);
|
|
2196
2306
|
});
|
|
2197
2307
|
});
|
|
2198
2308
|
}
|
|
2199
2309
|
writeFile(filePath, content) {
|
|
2200
2310
|
const safe = this.safePath(filePath);
|
|
2201
2311
|
if (!safe) return "Error: path outside working directory";
|
|
2202
|
-
|
|
2203
|
-
|
|
2312
|
+
mkdirSync2(dirname2(safe), { recursive: true });
|
|
2313
|
+
writeFileSync2(safe, content, "utf8");
|
|
2204
2314
|
const rel = relative(this.cwd, safe);
|
|
2205
2315
|
return `Written: ${rel} (${content.length} bytes)`;
|
|
2206
2316
|
}
|
|
2207
2317
|
readFile(filePath) {
|
|
2208
2318
|
const safe = this.safePath(filePath);
|
|
2209
2319
|
if (!safe) return "Error: path outside working directory";
|
|
2210
|
-
if (!
|
|
2211
|
-
const content =
|
|
2320
|
+
if (!existsSync3(safe)) return `File not found: ${filePath}`;
|
|
2321
|
+
const content = readFileSync3(safe, "utf8");
|
|
2212
2322
|
return content.length > 8e3 ? content.slice(0, 8e3) + `
|
|
2213
2323
|
\u2026[truncated, ${content.length} total bytes]` : content;
|
|
2214
2324
|
}
|
|
@@ -2299,9 +2409,9 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
|
|
|
2299
2409
|
listDir(dirPath) {
|
|
2300
2410
|
const safe = this.safePath(dirPath ?? ".");
|
|
2301
2411
|
if (!safe) return "Error: path outside working directory";
|
|
2302
|
-
if (!
|
|
2412
|
+
if (!existsSync3(safe)) return `Directory not found: ${dirPath}`;
|
|
2303
2413
|
try {
|
|
2304
|
-
const entries =
|
|
2414
|
+
const entries = readdirSync2(safe, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").map((e) => `${e.isDirectory() ? "d" : "f"} ${e.name}`).join("\n");
|
|
2305
2415
|
return entries || "(empty directory)";
|
|
2306
2416
|
} catch (e) {
|
|
2307
2417
|
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
@@ -2309,7 +2419,7 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
|
|
|
2309
2419
|
}
|
|
2310
2420
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
2311
2421
|
safePath(p) {
|
|
2312
|
-
const resolved =
|
|
2422
|
+
const resolved = resolve3(this.cwd, p);
|
|
2313
2423
|
return resolved.startsWith(this.cwd) ? resolved : null;
|
|
2314
2424
|
}
|
|
2315
2425
|
buildSystemPrompt(extra) {
|
|
@@ -2794,7 +2904,7 @@ var BackgroundWorkers = class {
|
|
|
2794
2904
|
};
|
|
2795
2905
|
|
|
2796
2906
|
// packages/daemon/src/SkillRegistry.ts
|
|
2797
|
-
import { readFileSync as
|
|
2907
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync3, existsSync as existsSync4, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
2798
2908
|
import { join } from "node:path";
|
|
2799
2909
|
import { homedir as homedir2 } from "node:os";
|
|
2800
2910
|
import YAML2 from "yaml";
|
|
@@ -2817,11 +2927,11 @@ var SkillRegistry = class {
|
|
|
2817
2927
|
this.loadFromDir(this.customDir, false);
|
|
2818
2928
|
}
|
|
2819
2929
|
loadFromDir(dir, isBuiltin) {
|
|
2820
|
-
if (!
|
|
2821
|
-
const files =
|
|
2930
|
+
if (!existsSync4(dir)) return;
|
|
2931
|
+
const files = readdirSync3(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
2822
2932
|
for (const file of files) {
|
|
2823
2933
|
try {
|
|
2824
|
-
const raw =
|
|
2934
|
+
const raw = readFileSync4(join(dir, file), "utf8");
|
|
2825
2935
|
const skill = YAML2.parse(raw);
|
|
2826
2936
|
if (skill.name) {
|
|
2827
2937
|
this.skills.set(skill.name, skill);
|
|
@@ -2856,9 +2966,9 @@ var SkillRegistry = class {
|
|
|
2856
2966
|
if (this.builtinNames.has(name)) {
|
|
2857
2967
|
throw new Error(`Cannot override built-in skill: ${name}`);
|
|
2858
2968
|
}
|
|
2859
|
-
|
|
2969
|
+
mkdirSync3(this.customDir, { recursive: true });
|
|
2860
2970
|
const filePath = join(this.customDir, `${name}.yaml`);
|
|
2861
|
-
|
|
2971
|
+
writeFileSync3(filePath, yamlContent, "utf8");
|
|
2862
2972
|
const skill = YAML2.parse(yamlContent);
|
|
2863
2973
|
this.skills.set(name, skill);
|
|
2864
2974
|
return skill;
|
|
@@ -2871,7 +2981,7 @@ var SkillRegistry = class {
|
|
|
2871
2981
|
throw new Error(`Cannot delete built-in skill: ${name}`);
|
|
2872
2982
|
}
|
|
2873
2983
|
const filePath = join(this.customDir, `${name}.yaml`);
|
|
2874
|
-
if (
|
|
2984
|
+
if (existsSync4(filePath)) {
|
|
2875
2985
|
unlinkSync(filePath);
|
|
2876
2986
|
}
|
|
2877
2987
|
this.skills.delete(name);
|
|
@@ -2884,8 +2994,8 @@ var SkillRegistry = class {
|
|
|
2884
2994
|
// packages/daemon/src/HTTPServer.ts
|
|
2885
2995
|
import { Hono as Hono8 } from "hono";
|
|
2886
2996
|
import { serve } from "@hono/node-server";
|
|
2887
|
-
import { readFileSync as
|
|
2888
|
-
import { resolve as
|
|
2997
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2998
|
+
import { resolve as resolve4, dirname as dirname3 } from "node:path";
|
|
2889
2999
|
import { fileURLToPath } from "node:url";
|
|
2890
3000
|
|
|
2891
3001
|
// packages/daemon/src/routes/health.ts
|
|
@@ -3116,99 +3226,396 @@ function skillRoutes(deps) {
|
|
|
3116
3226
|
const message = err instanceof Error ? err.message : String(err);
|
|
3117
3227
|
return c.json({ error: message }, 500);
|
|
3118
3228
|
}
|
|
3119
|
-
});
|
|
3120
|
-
app.delete("/:name", (c) => {
|
|
3121
|
-
const name = c.req.param("name");
|
|
3122
|
-
if (deps.skillRegistry.isBuiltin(name)) {
|
|
3123
|
-
return c.json({ error: "Cannot delete built-in skill" }, 403);
|
|
3229
|
+
});
|
|
3230
|
+
app.delete("/:name", (c) => {
|
|
3231
|
+
const name = c.req.param("name");
|
|
3232
|
+
if (deps.skillRegistry.isBuiltin(name)) {
|
|
3233
|
+
return c.json({ error: "Cannot delete built-in skill" }, 403);
|
|
3234
|
+
}
|
|
3235
|
+
const skill = deps.skillRegistry.get(name);
|
|
3236
|
+
if (!skill) {
|
|
3237
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
3238
|
+
}
|
|
3239
|
+
try {
|
|
3240
|
+
deps.skillRegistry.removeCustom(name);
|
|
3241
|
+
return c.json({ ok: true });
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3244
|
+
return c.json({ error: message }, 500);
|
|
3245
|
+
}
|
|
3246
|
+
});
|
|
3247
|
+
return app;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// packages/daemon/src/HTTPServer.ts
|
|
3251
|
+
function findGraphHtml() {
|
|
3252
|
+
const candidates = [
|
|
3253
|
+
resolve4(dirname3(fileURLToPath(import.meta.url)), "graph.html"),
|
|
3254
|
+
// dev (src/)
|
|
3255
|
+
resolve4(dirname3(fileURLToPath(import.meta.url)), "..", "graph.html"),
|
|
3256
|
+
// bundled (dist/../)
|
|
3257
|
+
resolve4(dirname3(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
|
|
3258
|
+
];
|
|
3259
|
+
for (const p of candidates) {
|
|
3260
|
+
try {
|
|
3261
|
+
readFileSync5(p);
|
|
3262
|
+
return p;
|
|
3263
|
+
} catch {
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return candidates[0];
|
|
3267
|
+
}
|
|
3268
|
+
var GRAPH_HTML_PATH = findGraphHtml();
|
|
3269
|
+
var HTTPServer = class {
|
|
3270
|
+
app;
|
|
3271
|
+
server = null;
|
|
3272
|
+
deps;
|
|
3273
|
+
constructor(deps) {
|
|
3274
|
+
this.deps = deps;
|
|
3275
|
+
this.app = new Hono8();
|
|
3276
|
+
this.app.route("/api/health", healthRoutes({ getStatus: deps.getStatus }));
|
|
3277
|
+
this.app.route("/api/sessions", sessionRoutes({ sessions: deps.sessions }));
|
|
3278
|
+
this.app.route("/api/graph", graphRoutes({ graph: deps.graph }));
|
|
3279
|
+
this.app.route("/api/entities", entityRoutes({ graph: deps.graph }));
|
|
3280
|
+
this.app.route("/api/traces", traceRoutes({ traceStore: deps.traceStore }));
|
|
3281
|
+
this.app.route("/api/subagents", subagentRoutes());
|
|
3282
|
+
this.app.route("/api/skills", skillRoutes({ skillRegistry: deps.skillRegistry }));
|
|
3283
|
+
const serveGraph = (c) => {
|
|
3284
|
+
try {
|
|
3285
|
+
const html = readFileSync5(GRAPH_HTML_PATH, "utf8");
|
|
3286
|
+
return c.html(html);
|
|
3287
|
+
} catch {
|
|
3288
|
+
return c.html("<p>Graph UI not found. Run: pnpm build</p>");
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
this.app.get("/", serveGraph);
|
|
3292
|
+
this.app.get("/graph", serveGraph);
|
|
3293
|
+
}
|
|
3294
|
+
start() {
|
|
3295
|
+
return new Promise((resolve7) => {
|
|
3296
|
+
this.server = serve(
|
|
3297
|
+
{
|
|
3298
|
+
fetch: this.app.fetch,
|
|
3299
|
+
port: this.deps.port,
|
|
3300
|
+
hostname: this.deps.host
|
|
3301
|
+
},
|
|
3302
|
+
() => {
|
|
3303
|
+
resolve7();
|
|
3304
|
+
}
|
|
3305
|
+
);
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
stop() {
|
|
3309
|
+
return new Promise((resolve7, reject) => {
|
|
3310
|
+
if (!this.server) {
|
|
3311
|
+
resolve7();
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
this.server.close((err) => {
|
|
3315
|
+
if (err) reject(err);
|
|
3316
|
+
else resolve7();
|
|
3317
|
+
});
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
getApp() {
|
|
3321
|
+
return this.app;
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
|
|
3325
|
+
// packages/daemon/src/LLMExecutor.ts
|
|
3326
|
+
var LLMExecutor = class {
|
|
3327
|
+
constructor(config) {
|
|
3328
|
+
this.config = config;
|
|
3329
|
+
}
|
|
3330
|
+
get isConfigured() {
|
|
3331
|
+
if (this.config.provider === "ollama") return true;
|
|
3332
|
+
return !!this.config.api_key?.trim();
|
|
3333
|
+
}
|
|
3334
|
+
// ─── Single completion (no tools, no streaming) ──────────────────────────
|
|
3335
|
+
async complete(messages, system) {
|
|
3336
|
+
const res = await this.completeWithTools(messages, [], system, void 0);
|
|
3337
|
+
return { content: res.content, tokens_used: res.tokens_used, model: res.model };
|
|
3338
|
+
}
|
|
3339
|
+
// ─── Tool-calling completion with optional streaming ─────────────────────
|
|
3340
|
+
async completeWithTools(messages, tools, system, onToken) {
|
|
3341
|
+
switch (this.config.provider) {
|
|
3342
|
+
case "anthropic":
|
|
3343
|
+
return this.anthropic(messages, tools, system, onToken);
|
|
3344
|
+
case "openai":
|
|
3345
|
+
return this.openai(messages, tools, system, onToken);
|
|
3346
|
+
case "xai":
|
|
3347
|
+
return this.openai(messages, tools, system, onToken, "https://api.x.ai/v1");
|
|
3348
|
+
case "gemini":
|
|
3349
|
+
return this.openai(messages, tools, system, onToken, "https://generativelanguage.googleapis.com/v1beta/openai");
|
|
3350
|
+
case "ollama":
|
|
3351
|
+
return this.ollama(messages, system, onToken);
|
|
3352
|
+
default:
|
|
3353
|
+
return this.openai(messages, tools, system, onToken);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
// ─── Anthropic ───────────────────────────────────────────────────────────
|
|
3357
|
+
async anthropic(messages, tools, system, onToken) {
|
|
3358
|
+
const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
|
|
3359
|
+
const filtered = messages.filter((m) => m.role !== "system");
|
|
3360
|
+
const anthropicMsgs = filtered.map((m) => {
|
|
3361
|
+
if (m.role === "tool") {
|
|
3362
|
+
return {
|
|
3363
|
+
role: "user",
|
|
3364
|
+
content: [{ type: "tool_result", tool_use_id: m.tool_call_id, content: m.content }]
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
if (m.role === "assistant" && m.tool_calls?.length) {
|
|
3368
|
+
return {
|
|
3369
|
+
role: "assistant",
|
|
3370
|
+
content: [
|
|
3371
|
+
...m.content ? [{ type: "text", text: m.content }] : [],
|
|
3372
|
+
...m.tool_calls.map((tc) => ({
|
|
3373
|
+
type: "tool_use",
|
|
3374
|
+
id: tc.id,
|
|
3375
|
+
name: tc.name,
|
|
3376
|
+
input: tc.input
|
|
3377
|
+
}))
|
|
3378
|
+
]
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
return { role: m.role, content: m.content };
|
|
3382
|
+
});
|
|
3383
|
+
const body = {
|
|
3384
|
+
model: this.config.model,
|
|
3385
|
+
max_tokens: 8192,
|
|
3386
|
+
messages: anthropicMsgs,
|
|
3387
|
+
stream: true
|
|
3388
|
+
};
|
|
3389
|
+
if (sysContent) body.system = sysContent;
|
|
3390
|
+
if (tools.length > 0) {
|
|
3391
|
+
body.tools = tools.map((t) => ({
|
|
3392
|
+
name: t.name,
|
|
3393
|
+
description: t.description,
|
|
3394
|
+
input_schema: t.input_schema
|
|
3395
|
+
}));
|
|
3396
|
+
}
|
|
3397
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
3398
|
+
method: "POST",
|
|
3399
|
+
headers: {
|
|
3400
|
+
"Content-Type": "application/json",
|
|
3401
|
+
"x-api-key": this.config.api_key,
|
|
3402
|
+
"anthropic-version": "2023-06-01"
|
|
3403
|
+
},
|
|
3404
|
+
body: JSON.stringify(body)
|
|
3405
|
+
});
|
|
3406
|
+
if (!res.ok) {
|
|
3407
|
+
const err = await res.text();
|
|
3408
|
+
throw new Error(`Anthropic ${res.status}: ${err}`);
|
|
3409
|
+
}
|
|
3410
|
+
let textContent = "";
|
|
3411
|
+
let stopReason = "end_turn";
|
|
3412
|
+
let inputTokens = 0;
|
|
3413
|
+
let outputTokens = 0;
|
|
3414
|
+
let modelName = this.config.model;
|
|
3415
|
+
const toolCalls = [];
|
|
3416
|
+
const toolInputBuffers = {};
|
|
3417
|
+
let currentToolId = "";
|
|
3418
|
+
const reader = res.body.getReader();
|
|
3419
|
+
const decoder = new TextDecoder();
|
|
3420
|
+
let buf = "";
|
|
3421
|
+
while (true) {
|
|
3422
|
+
const { done, value } = await reader.read();
|
|
3423
|
+
if (done) break;
|
|
3424
|
+
buf += decoder.decode(value, { stream: true });
|
|
3425
|
+
const lines = buf.split("\n");
|
|
3426
|
+
buf = lines.pop() ?? "";
|
|
3427
|
+
for (const line of lines) {
|
|
3428
|
+
if (!line.startsWith("data: ")) continue;
|
|
3429
|
+
const data = line.slice(6).trim();
|
|
3430
|
+
if (data === "[DONE]" || data === "") continue;
|
|
3431
|
+
let evt;
|
|
3432
|
+
try {
|
|
3433
|
+
evt = JSON.parse(data);
|
|
3434
|
+
} catch {
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
const type = evt.type;
|
|
3438
|
+
if (type === "message_start") {
|
|
3439
|
+
const usage = evt.message?.usage;
|
|
3440
|
+
inputTokens = usage?.input_tokens ?? 0;
|
|
3441
|
+
modelName = evt.message?.model ?? modelName;
|
|
3442
|
+
} else if (type === "content_block_start") {
|
|
3443
|
+
const block = evt.content_block;
|
|
3444
|
+
if (block?.type === "tool_use") {
|
|
3445
|
+
currentToolId = block.id;
|
|
3446
|
+
toolInputBuffers[currentToolId] = "";
|
|
3447
|
+
toolCalls.push({ id: currentToolId, name: block.name, input: {} });
|
|
3448
|
+
}
|
|
3449
|
+
} else if (type === "content_block_delta") {
|
|
3450
|
+
const delta = evt.delta;
|
|
3451
|
+
if (delta?.type === "text_delta") {
|
|
3452
|
+
const token = delta.text ?? "";
|
|
3453
|
+
textContent += token;
|
|
3454
|
+
if (onToken && token) onToken(token);
|
|
3455
|
+
} else if (delta?.type === "input_json_delta") {
|
|
3456
|
+
toolInputBuffers[currentToolId] = (toolInputBuffers[currentToolId] ?? "") + (delta.partial_json ?? "");
|
|
3457
|
+
}
|
|
3458
|
+
} else if (type === "content_block_stop") {
|
|
3459
|
+
if (currentToolId && toolInputBuffers[currentToolId]) {
|
|
3460
|
+
const tc = toolCalls.find((t) => t.id === currentToolId);
|
|
3461
|
+
if (tc) {
|
|
3462
|
+
try {
|
|
3463
|
+
tc.input = JSON.parse(toolInputBuffers[currentToolId]);
|
|
3464
|
+
} catch {
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
} else if (type === "message_delta") {
|
|
3469
|
+
const usage = evt.usage;
|
|
3470
|
+
outputTokens = usage?.output_tokens ?? 0;
|
|
3471
|
+
const stop = evt.delta?.stop_reason;
|
|
3472
|
+
if (stop === "tool_use") stopReason = "tool_use";
|
|
3473
|
+
else if (stop === "end_turn") stopReason = "end_turn";
|
|
3474
|
+
else if (stop === "max_tokens") stopReason = "max_tokens";
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
return {
|
|
3479
|
+
content: textContent,
|
|
3480
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : null,
|
|
3481
|
+
stop_reason: stopReason,
|
|
3482
|
+
tokens_used: inputTokens + outputTokens,
|
|
3483
|
+
model: modelName
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
// ─── OpenAI (also xAI, Gemini) ───────────────────────────────────────────
|
|
3487
|
+
async openai(messages, tools, system, onToken, baseUrl = "https://api.openai.com/v1") {
|
|
3488
|
+
const allMessages = [];
|
|
3489
|
+
const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
|
|
3490
|
+
if (sysContent) allMessages.push({ role: "system", content: sysContent });
|
|
3491
|
+
for (const m of messages.filter((m2) => m2.role !== "system")) {
|
|
3492
|
+
if (m.role === "tool") {
|
|
3493
|
+
allMessages.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content });
|
|
3494
|
+
} else if (m.role === "assistant" && m.tool_calls?.length) {
|
|
3495
|
+
allMessages.push({
|
|
3496
|
+
role: "assistant",
|
|
3497
|
+
content: m.content || null,
|
|
3498
|
+
tool_calls: m.tool_calls.map((tc) => ({
|
|
3499
|
+
id: tc.id,
|
|
3500
|
+
type: "function",
|
|
3501
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) }
|
|
3502
|
+
}))
|
|
3503
|
+
});
|
|
3504
|
+
} else {
|
|
3505
|
+
allMessages.push({ role: m.role, content: m.content });
|
|
3506
|
+
}
|
|
3124
3507
|
}
|
|
3125
|
-
const
|
|
3126
|
-
|
|
3127
|
-
|
|
3508
|
+
const body = {
|
|
3509
|
+
model: this.config.model,
|
|
3510
|
+
messages: allMessages,
|
|
3511
|
+
max_tokens: 8192,
|
|
3512
|
+
stream: true,
|
|
3513
|
+
stream_options: { include_usage: true }
|
|
3514
|
+
};
|
|
3515
|
+
if (tools.length > 0) {
|
|
3516
|
+
body.tools = tools.map((t) => ({
|
|
3517
|
+
type: "function",
|
|
3518
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema }
|
|
3519
|
+
}));
|
|
3128
3520
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3521
|
+
const res = await fetch(`${this.config.base_url ?? baseUrl}/chat/completions`, {
|
|
3522
|
+
method: "POST",
|
|
3523
|
+
headers: {
|
|
3524
|
+
"Content-Type": "application/json",
|
|
3525
|
+
"Authorization": `Bearer ${this.config.api_key}`
|
|
3526
|
+
},
|
|
3527
|
+
body: JSON.stringify(body)
|
|
3528
|
+
});
|
|
3529
|
+
if (!res.ok) {
|
|
3530
|
+
const err = await res.text();
|
|
3531
|
+
throw new Error(`OpenAI ${res.status}: ${err}`);
|
|
3135
3532
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3533
|
+
let textContent = "";
|
|
3534
|
+
let tokensUsed = 0;
|
|
3535
|
+
let modelName = this.config.model;
|
|
3536
|
+
let stopReason = "end_turn";
|
|
3537
|
+
const toolCallMap = {};
|
|
3538
|
+
const reader = res.body.getReader();
|
|
3539
|
+
const decoder = new TextDecoder();
|
|
3540
|
+
let buf = "";
|
|
3541
|
+
while (true) {
|
|
3542
|
+
const { done, value } = await reader.read();
|
|
3543
|
+
if (done) break;
|
|
3544
|
+
buf += decoder.decode(value, { stream: true });
|
|
3545
|
+
const lines = buf.split("\n");
|
|
3546
|
+
buf = lines.pop() ?? "";
|
|
3547
|
+
for (const line of lines) {
|
|
3548
|
+
if (!line.startsWith("data: ")) continue;
|
|
3549
|
+
const data = line.slice(6).trim();
|
|
3550
|
+
if (data === "[DONE]") continue;
|
|
3551
|
+
let evt;
|
|
3552
|
+
try {
|
|
3553
|
+
evt = JSON.parse(data);
|
|
3554
|
+
} catch {
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
modelName = evt.model ?? modelName;
|
|
3558
|
+
const usage = evt.usage;
|
|
3559
|
+
if (usage?.total_tokens) tokensUsed = usage.total_tokens;
|
|
3560
|
+
const choices = evt.choices;
|
|
3561
|
+
if (!choices?.length) continue;
|
|
3562
|
+
const delta = choices[0].delta;
|
|
3563
|
+
if (!delta) continue;
|
|
3564
|
+
const finish = choices[0].finish_reason;
|
|
3565
|
+
if (finish === "tool_calls") stopReason = "tool_use";
|
|
3566
|
+
else if (finish === "stop") stopReason = "end_turn";
|
|
3567
|
+
const token = delta.content;
|
|
3568
|
+
if (token) {
|
|
3569
|
+
textContent += token;
|
|
3570
|
+
if (onToken) onToken(token);
|
|
3571
|
+
}
|
|
3572
|
+
const toolCallDeltas = delta.tool_calls;
|
|
3573
|
+
if (toolCallDeltas) {
|
|
3574
|
+
for (const tc of toolCallDeltas) {
|
|
3575
|
+
const idx = tc.index;
|
|
3576
|
+
if (!toolCallMap[idx]) {
|
|
3577
|
+
toolCallMap[idx] = { id: "", name: "", args: "" };
|
|
3578
|
+
}
|
|
3579
|
+
const fn = tc.function;
|
|
3580
|
+
if (tc.id) toolCallMap[idx].id = tc.id;
|
|
3581
|
+
if (fn?.name) toolCallMap[idx].name = fn.name;
|
|
3582
|
+
if (fn?.arguments) toolCallMap[idx].args += fn.arguments;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3154
3586
|
}
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
}
|
|
3158
|
-
var GRAPH_HTML_PATH = findGraphHtml();
|
|
3159
|
-
var HTTPServer = class {
|
|
3160
|
-
app;
|
|
3161
|
-
server = null;
|
|
3162
|
-
deps;
|
|
3163
|
-
constructor(deps) {
|
|
3164
|
-
this.deps = deps;
|
|
3165
|
-
this.app = new Hono8();
|
|
3166
|
-
this.app.route("/api/health", healthRoutes({ getStatus: deps.getStatus }));
|
|
3167
|
-
this.app.route("/api/sessions", sessionRoutes({ sessions: deps.sessions }));
|
|
3168
|
-
this.app.route("/api/graph", graphRoutes({ graph: deps.graph }));
|
|
3169
|
-
this.app.route("/api/entities", entityRoutes({ graph: deps.graph }));
|
|
3170
|
-
this.app.route("/api/traces", traceRoutes({ traceStore: deps.traceStore }));
|
|
3171
|
-
this.app.route("/api/subagents", subagentRoutes());
|
|
3172
|
-
this.app.route("/api/skills", skillRoutes({ skillRegistry: deps.skillRegistry }));
|
|
3173
|
-
const serveGraph = (c) => {
|
|
3587
|
+
const toolCalls = Object.values(toolCallMap).filter((tc) => tc.id && tc.name).map((tc) => {
|
|
3588
|
+
let input = {};
|
|
3174
3589
|
try {
|
|
3175
|
-
|
|
3176
|
-
return c.html(html);
|
|
3590
|
+
input = JSON.parse(tc.args);
|
|
3177
3591
|
} catch {
|
|
3178
|
-
return c.html("<p>Graph UI not found. Run: pnpm build</p>");
|
|
3179
3592
|
}
|
|
3180
|
-
|
|
3181
|
-
this.app.get("/", serveGraph);
|
|
3182
|
-
this.app.get("/graph", serveGraph);
|
|
3183
|
-
}
|
|
3184
|
-
start() {
|
|
3185
|
-
return new Promise((resolve6) => {
|
|
3186
|
-
this.server = serve(
|
|
3187
|
-
{
|
|
3188
|
-
fetch: this.app.fetch,
|
|
3189
|
-
port: this.deps.port,
|
|
3190
|
-
hostname: this.deps.host
|
|
3191
|
-
},
|
|
3192
|
-
() => {
|
|
3193
|
-
resolve6();
|
|
3194
|
-
}
|
|
3195
|
-
);
|
|
3593
|
+
return { id: tc.id, name: tc.name, input };
|
|
3196
3594
|
});
|
|
3595
|
+
return {
|
|
3596
|
+
content: textContent,
|
|
3597
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : null,
|
|
3598
|
+
stop_reason: stopReason,
|
|
3599
|
+
tokens_used: tokensUsed,
|
|
3600
|
+
model: modelName
|
|
3601
|
+
};
|
|
3197
3602
|
}
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
}
|
|
3603
|
+
// ─── Ollama (no streaming for simplicity) ────────────────────────────────
|
|
3604
|
+
async ollama(messages, system, onToken) {
|
|
3605
|
+
const baseUrl = this.config.base_url ?? "http://localhost:11434";
|
|
3606
|
+
const allMessages = [];
|
|
3607
|
+
const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
|
|
3608
|
+
if (sysContent) allMessages.push({ role: "system", content: sysContent });
|
|
3609
|
+
allMessages.push(...messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
|
|
3610
|
+
const res = await fetch(`${baseUrl}/api/chat`, {
|
|
3611
|
+
method: "POST",
|
|
3612
|
+
headers: { "Content-Type": "application/json" },
|
|
3613
|
+
body: JSON.stringify({ model: this.config.model, messages: allMessages, stream: false })
|
|
3208
3614
|
});
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3615
|
+
if (!res.ok) throw new Error(`Ollama error ${res.status}`);
|
|
3616
|
+
const data = await res.json();
|
|
3617
|
+
if (onToken) onToken(data.message.content);
|
|
3618
|
+
return { content: data.message.content, tool_calls: null, stop_reason: "end_turn", tokens_used: data.eval_count ?? 0, model: this.config.model };
|
|
3212
3619
|
}
|
|
3213
3620
|
};
|
|
3214
3621
|
|
|
@@ -3227,13 +3634,13 @@ var ZeroAgentDaemon = class {
|
|
|
3227
3634
|
startedAt = 0;
|
|
3228
3635
|
pidFilePath;
|
|
3229
3636
|
constructor() {
|
|
3230
|
-
this.pidFilePath =
|
|
3637
|
+
this.pidFilePath = resolve5(homedir3(), ".0agent", "daemon.pid");
|
|
3231
3638
|
}
|
|
3232
3639
|
async start(opts) {
|
|
3233
3640
|
this.config = await loadConfig(opts?.config_path);
|
|
3234
|
-
const dotDir =
|
|
3235
|
-
if (!
|
|
3236
|
-
|
|
3641
|
+
const dotDir = resolve5(homedir3(), ".0agent");
|
|
3642
|
+
if (!existsSync5(dotDir)) {
|
|
3643
|
+
mkdirSync4(dotDir, { recursive: true });
|
|
3237
3644
|
}
|
|
3238
3645
|
this.adapter = new SQLiteAdapter({ db_path: this.config.graph.db_path });
|
|
3239
3646
|
this.graph = new KnowledgeGraph(this.adapter);
|
|
@@ -3284,7 +3691,7 @@ var ZeroAgentDaemon = class {
|
|
|
3284
3691
|
getStatus: () => this.getStatus()
|
|
3285
3692
|
});
|
|
3286
3693
|
await this.httpServer.start();
|
|
3287
|
-
|
|
3694
|
+
writeFileSync4(this.pidFilePath, String(process.pid), "utf8");
|
|
3288
3695
|
console.log(
|
|
3289
3696
|
`[0agent] Daemon started on ${this.config.server.host}:${this.config.server.port} (PID: ${process.pid})`
|
|
3290
3697
|
);
|
|
@@ -3318,7 +3725,7 @@ var ZeroAgentDaemon = class {
|
|
|
3318
3725
|
this.graph = null;
|
|
3319
3726
|
}
|
|
3320
3727
|
this.adapter = null;
|
|
3321
|
-
if (
|
|
3728
|
+
if (existsSync5(this.pidFilePath)) {
|
|
3322
3729
|
try {
|
|
3323
3730
|
unlinkSync2(this.pidFilePath);
|
|
3324
3731
|
} catch {
|
|
@@ -3348,11 +3755,11 @@ var ZeroAgentDaemon = class {
|
|
|
3348
3755
|
};
|
|
3349
3756
|
|
|
3350
3757
|
// packages/daemon/src/start.ts
|
|
3351
|
-
import { resolve as
|
|
3758
|
+
import { resolve as resolve6 } from "node:path";
|
|
3352
3759
|
import { homedir as homedir4 } from "node:os";
|
|
3353
|
-
import { existsSync as
|
|
3354
|
-
var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ??
|
|
3355
|
-
if (!
|
|
3760
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
3761
|
+
var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve6(homedir4(), ".0agent", "config.yaml");
|
|
3762
|
+
if (!existsSync6(CONFIG_PATH)) {
|
|
3356
3763
|
console.error(`
|
|
3357
3764
|
0agent is not initialised.
|
|
3358
3765
|
|