0agent 1.0.6 → 1.0.8

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/bin/0agent.js CHANGED
@@ -415,6 +415,7 @@ async function streamSession(sessionId) {
415
415
 
416
416
  return new Promise((resolve) => {
417
417
  const ws = new WS(`ws://localhost:4200/ws`);
418
+ let streaming = false; // true when mid-token-stream
418
419
 
419
420
  ws.on('open', () => {
420
421
  ws.send(JSON.stringify({ type: 'subscribe', topics: ['sessions'] }));
@@ -427,18 +428,30 @@ async function streamSession(sessionId) {
427
428
 
428
429
  switch (event.type) {
429
430
  case 'session.step':
430
- console.log(` › ${event.step}`);
431
+ // Newline before step if we were mid-stream
432
+ if (streaming) { process.stdout.write('\n'); streaming = false; }
433
+ console.log(` \x1b[2m›\x1b[0m ${event.step}`);
434
+ break;
435
+ case 'session.token':
436
+ // Token-by-token streaming — print without newline
437
+ if (!streaming) { process.stdout.write('\n '); streaming = true; }
438
+ process.stdout.write(event.token);
431
439
  break;
432
440
  case 'session.completed': {
433
- console.log('\n ✓ Done\n');
434
- const out = event.result?.output ?? event.result;
435
- if (out && typeof out === 'string') console.log(` ${out}\n`);
441
+ if (streaming) { process.stdout.write('\n'); streaming = false; }
442
+ // Show files written + commands run
443
+ const r = event.result ?? {};
444
+ if (r.files_written?.length) console.log(`\n \x1b[32m✓\x1b[0m Files: ${r.files_written.join(', ')}`);
445
+ if (r.commands_run?.length) console.log(` \x1b[32m✓\x1b[0m Commands run: ${r.commands_run.length}`);
446
+ if (r.tokens_used) console.log(` \x1b[2m${r.tokens_used} tokens · ${r.model}\x1b[0m`);
447
+ console.log('\n \x1b[32m✓ Done\x1b[0m\n');
436
448
  ws.close();
437
449
  resolve();
438
450
  break;
439
451
  }
440
452
  case 'session.failed':
441
- console.log(`\n ✗ Failed: ${event.error}\n`);
453
+ if (streaming) { process.stdout.write('\n'); streaming = false; }
454
+ console.log(`\n \x1b[31m✗ Failed:\x1b[0m ${event.error}\n`);
442
455
  ws.close();
443
456
  resolve();
444
457
  break;
package/dist/daemon.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/daemon/src/ZeroAgentDaemon.ts
2
- import { writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
3
- import { resolve as resolve3 } from "node:path";
2
+ import { writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
3
+ import { resolve as resolve4 } from "node:path";
4
4
  import { homedir as homedir3 } from "node:os";
5
5
 
6
6
  // packages/core/src/graph/GraphNode.ts
@@ -1685,16 +1685,551 @@ var EntityScopedContextLoader = class {
1685
1685
  }
1686
1686
  };
1687
1687
 
1688
+ // packages/daemon/src/AgentExecutor.ts
1689
+ import { spawn } from "node:child_process";
1690
+ import { writeFileSync, readFileSync as readFileSync2, readdirSync, mkdirSync, existsSync as existsSync2 } from "node:fs";
1691
+ import { resolve as resolve2, dirname, relative } from "node:path";
1692
+
1693
+ // packages/daemon/src/LLMExecutor.ts
1694
+ var AGENT_TOOLS = [
1695
+ {
1696
+ name: "shell_exec",
1697
+ description: "Execute a shell command in the working directory. Use for running servers (with & for background), installing packages, running tests, git operations, etc. Returns stdout + stderr.",
1698
+ input_schema: {
1699
+ type: "object",
1700
+ properties: {
1701
+ command: { type: "string", description: "Shell command to execute" },
1702
+ timeout_ms: { type: "number", description: "Timeout in milliseconds (default 30000)" }
1703
+ },
1704
+ required: ["command"]
1705
+ }
1706
+ },
1707
+ {
1708
+ name: "write_file",
1709
+ description: "Write content to a file. Creates parent directories if needed. Use for creating HTML, CSS, JS, config files, etc.",
1710
+ input_schema: {
1711
+ type: "object",
1712
+ properties: {
1713
+ path: { type: "string", description: "File path relative to working directory" },
1714
+ content: { type: "string", description: "Full file content to write" }
1715
+ },
1716
+ required: ["path", "content"]
1717
+ }
1718
+ },
1719
+ {
1720
+ name: "read_file",
1721
+ description: "Read a file's contents.",
1722
+ input_schema: {
1723
+ type: "object",
1724
+ properties: {
1725
+ path: { type: "string", description: "File path relative to working directory" }
1726
+ },
1727
+ required: ["path"]
1728
+ }
1729
+ },
1730
+ {
1731
+ name: "list_dir",
1732
+ description: "List files and directories.",
1733
+ input_schema: {
1734
+ type: "object",
1735
+ properties: {
1736
+ path: { type: "string", description: 'Directory path relative to working directory (default: ".")' }
1737
+ }
1738
+ }
1739
+ }
1740
+ ];
1741
+ var LLMExecutor = class {
1742
+ constructor(config) {
1743
+ this.config = config;
1744
+ }
1745
+ get isConfigured() {
1746
+ if (this.config.provider === "ollama") return true;
1747
+ return !!this.config.api_key?.trim();
1748
+ }
1749
+ // ─── Single completion (no tools, no streaming) ──────────────────────────
1750
+ async complete(messages, system) {
1751
+ const res = await this.completeWithTools(messages, [], system, void 0);
1752
+ return { content: res.content, tokens_used: res.tokens_used, model: res.model };
1753
+ }
1754
+ // ─── Tool-calling completion with optional streaming ─────────────────────
1755
+ async completeWithTools(messages, tools, system, onToken) {
1756
+ switch (this.config.provider) {
1757
+ case "anthropic":
1758
+ return this.anthropic(messages, tools, system, onToken);
1759
+ case "openai":
1760
+ return this.openai(messages, tools, system, onToken);
1761
+ case "xai":
1762
+ return this.openai(messages, tools, system, onToken, "https://api.x.ai/v1");
1763
+ case "gemini":
1764
+ return this.openai(messages, tools, system, onToken, "https://generativelanguage.googleapis.com/v1beta/openai");
1765
+ case "ollama":
1766
+ return this.ollama(messages, system, onToken);
1767
+ default:
1768
+ return this.openai(messages, tools, system, onToken);
1769
+ }
1770
+ }
1771
+ // ─── Anthropic ───────────────────────────────────────────────────────────
1772
+ async anthropic(messages, tools, system, onToken) {
1773
+ const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
1774
+ const filtered = messages.filter((m) => m.role !== "system");
1775
+ const anthropicMsgs = filtered.map((m) => {
1776
+ if (m.role === "tool") {
1777
+ return {
1778
+ role: "user",
1779
+ content: [{ type: "tool_result", tool_use_id: m.tool_call_id, content: m.content }]
1780
+ };
1781
+ }
1782
+ if (m.role === "assistant" && m.tool_calls?.length) {
1783
+ return {
1784
+ role: "assistant",
1785
+ content: [
1786
+ ...m.content ? [{ type: "text", text: m.content }] : [],
1787
+ ...m.tool_calls.map((tc) => ({
1788
+ type: "tool_use",
1789
+ id: tc.id,
1790
+ name: tc.name,
1791
+ input: tc.input
1792
+ }))
1793
+ ]
1794
+ };
1795
+ }
1796
+ return { role: m.role, content: m.content };
1797
+ });
1798
+ const body = {
1799
+ model: this.config.model,
1800
+ max_tokens: 8192,
1801
+ messages: anthropicMsgs,
1802
+ stream: true
1803
+ };
1804
+ if (sysContent) body.system = sysContent;
1805
+ if (tools.length > 0) {
1806
+ body.tools = tools.map((t) => ({
1807
+ name: t.name,
1808
+ description: t.description,
1809
+ input_schema: t.input_schema
1810
+ }));
1811
+ }
1812
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
1813
+ method: "POST",
1814
+ headers: {
1815
+ "Content-Type": "application/json",
1816
+ "x-api-key": this.config.api_key,
1817
+ "anthropic-version": "2023-06-01"
1818
+ },
1819
+ body: JSON.stringify(body)
1820
+ });
1821
+ if (!res.ok) {
1822
+ const err = await res.text();
1823
+ throw new Error(`Anthropic ${res.status}: ${err}`);
1824
+ }
1825
+ let textContent = "";
1826
+ let stopReason = "end_turn";
1827
+ let inputTokens = 0;
1828
+ let outputTokens = 0;
1829
+ let modelName = this.config.model;
1830
+ const toolCalls = [];
1831
+ const toolInputBuffers = {};
1832
+ let currentToolId = "";
1833
+ const reader = res.body.getReader();
1834
+ const decoder = new TextDecoder();
1835
+ let buf = "";
1836
+ while (true) {
1837
+ const { done, value } = await reader.read();
1838
+ if (done) break;
1839
+ buf += decoder.decode(value, { stream: true });
1840
+ const lines = buf.split("\n");
1841
+ buf = lines.pop() ?? "";
1842
+ for (const line of lines) {
1843
+ if (!line.startsWith("data: ")) continue;
1844
+ const data = line.slice(6).trim();
1845
+ if (data === "[DONE]" || data === "") continue;
1846
+ let evt;
1847
+ try {
1848
+ evt = JSON.parse(data);
1849
+ } catch {
1850
+ continue;
1851
+ }
1852
+ const type = evt.type;
1853
+ if (type === "message_start") {
1854
+ const usage = evt.message?.usage;
1855
+ inputTokens = usage?.input_tokens ?? 0;
1856
+ modelName = evt.message?.model ?? modelName;
1857
+ } else if (type === "content_block_start") {
1858
+ const block = evt.content_block;
1859
+ if (block?.type === "tool_use") {
1860
+ currentToolId = block.id;
1861
+ toolInputBuffers[currentToolId] = "";
1862
+ toolCalls.push({ id: currentToolId, name: block.name, input: {} });
1863
+ }
1864
+ } else if (type === "content_block_delta") {
1865
+ const delta = evt.delta;
1866
+ if (delta?.type === "text_delta") {
1867
+ const token = delta.text ?? "";
1868
+ textContent += token;
1869
+ if (onToken && token) onToken(token);
1870
+ } else if (delta?.type === "input_json_delta") {
1871
+ toolInputBuffers[currentToolId] = (toolInputBuffers[currentToolId] ?? "") + (delta.partial_json ?? "");
1872
+ }
1873
+ } else if (type === "content_block_stop") {
1874
+ if (currentToolId && toolInputBuffers[currentToolId]) {
1875
+ const tc = toolCalls.find((t) => t.id === currentToolId);
1876
+ if (tc) {
1877
+ try {
1878
+ tc.input = JSON.parse(toolInputBuffers[currentToolId]);
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ }
1883
+ } else if (type === "message_delta") {
1884
+ const usage = evt.usage;
1885
+ outputTokens = usage?.output_tokens ?? 0;
1886
+ const stop = evt.delta?.stop_reason;
1887
+ if (stop === "tool_use") stopReason = "tool_use";
1888
+ else if (stop === "end_turn") stopReason = "end_turn";
1889
+ else if (stop === "max_tokens") stopReason = "max_tokens";
1890
+ }
1891
+ }
1892
+ }
1893
+ return {
1894
+ content: textContent,
1895
+ tool_calls: toolCalls.length > 0 ? toolCalls : null,
1896
+ stop_reason: stopReason,
1897
+ tokens_used: inputTokens + outputTokens,
1898
+ model: modelName
1899
+ };
1900
+ }
1901
+ // ─── OpenAI (also xAI, Gemini) ───────────────────────────────────────────
1902
+ async openai(messages, tools, system, onToken, baseUrl = "https://api.openai.com/v1") {
1903
+ const allMessages = [];
1904
+ const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
1905
+ if (sysContent) allMessages.push({ role: "system", content: sysContent });
1906
+ for (const m of messages.filter((m2) => m2.role !== "system")) {
1907
+ if (m.role === "tool") {
1908
+ allMessages.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content });
1909
+ } else if (m.role === "assistant" && m.tool_calls?.length) {
1910
+ allMessages.push({
1911
+ role: "assistant",
1912
+ content: m.content || null,
1913
+ tool_calls: m.tool_calls.map((tc) => ({
1914
+ id: tc.id,
1915
+ type: "function",
1916
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
1917
+ }))
1918
+ });
1919
+ } else {
1920
+ allMessages.push({ role: m.role, content: m.content });
1921
+ }
1922
+ }
1923
+ const body = {
1924
+ model: this.config.model,
1925
+ messages: allMessages,
1926
+ max_tokens: 8192,
1927
+ stream: true,
1928
+ stream_options: { include_usage: true }
1929
+ };
1930
+ if (tools.length > 0) {
1931
+ body.tools = tools.map((t) => ({
1932
+ type: "function",
1933
+ function: { name: t.name, description: t.description, parameters: t.input_schema }
1934
+ }));
1935
+ }
1936
+ const res = await fetch(`${this.config.base_url ?? baseUrl}/chat/completions`, {
1937
+ method: "POST",
1938
+ headers: {
1939
+ "Content-Type": "application/json",
1940
+ "Authorization": `Bearer ${this.config.api_key}`
1941
+ },
1942
+ body: JSON.stringify(body)
1943
+ });
1944
+ if (!res.ok) {
1945
+ const err = await res.text();
1946
+ throw new Error(`OpenAI ${res.status}: ${err}`);
1947
+ }
1948
+ let textContent = "";
1949
+ let tokensUsed = 0;
1950
+ let modelName = this.config.model;
1951
+ let stopReason = "end_turn";
1952
+ const toolCallMap = {};
1953
+ const reader = res.body.getReader();
1954
+ const decoder = new TextDecoder();
1955
+ let buf = "";
1956
+ while (true) {
1957
+ const { done, value } = await reader.read();
1958
+ if (done) break;
1959
+ buf += decoder.decode(value, { stream: true });
1960
+ const lines = buf.split("\n");
1961
+ buf = lines.pop() ?? "";
1962
+ for (const line of lines) {
1963
+ if (!line.startsWith("data: ")) continue;
1964
+ const data = line.slice(6).trim();
1965
+ if (data === "[DONE]") continue;
1966
+ let evt;
1967
+ try {
1968
+ evt = JSON.parse(data);
1969
+ } catch {
1970
+ continue;
1971
+ }
1972
+ modelName = evt.model ?? modelName;
1973
+ const usage = evt.usage;
1974
+ if (usage?.total_tokens) tokensUsed = usage.total_tokens;
1975
+ const choices = evt.choices;
1976
+ if (!choices?.length) continue;
1977
+ const delta = choices[0].delta;
1978
+ if (!delta) continue;
1979
+ const finish = choices[0].finish_reason;
1980
+ if (finish === "tool_calls") stopReason = "tool_use";
1981
+ else if (finish === "stop") stopReason = "end_turn";
1982
+ const token = delta.content;
1983
+ if (token) {
1984
+ textContent += token;
1985
+ if (onToken) onToken(token);
1986
+ }
1987
+ const toolCallDeltas = delta.tool_calls;
1988
+ if (toolCallDeltas) {
1989
+ for (const tc of toolCallDeltas) {
1990
+ const idx = tc.index;
1991
+ if (!toolCallMap[idx]) {
1992
+ toolCallMap[idx] = { id: "", name: "", args: "" };
1993
+ }
1994
+ const fn = tc.function;
1995
+ if (tc.id) toolCallMap[idx].id = tc.id;
1996
+ if (fn?.name) toolCallMap[idx].name = fn.name;
1997
+ if (fn?.arguments) toolCallMap[idx].args += fn.arguments;
1998
+ }
1999
+ }
2000
+ }
2001
+ }
2002
+ const toolCalls = Object.values(toolCallMap).filter((tc) => tc.id && tc.name).map((tc) => {
2003
+ let input = {};
2004
+ try {
2005
+ input = JSON.parse(tc.args);
2006
+ } catch {
2007
+ }
2008
+ return { id: tc.id, name: tc.name, input };
2009
+ });
2010
+ return {
2011
+ content: textContent,
2012
+ tool_calls: toolCalls.length > 0 ? toolCalls : null,
2013
+ stop_reason: stopReason,
2014
+ tokens_used: tokensUsed,
2015
+ model: modelName
2016
+ };
2017
+ }
2018
+ // ─── Ollama (no streaming for simplicity) ────────────────────────────────
2019
+ async ollama(messages, system, onToken) {
2020
+ const baseUrl = this.config.base_url ?? "http://localhost:11434";
2021
+ const allMessages = [];
2022
+ const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
2023
+ if (sysContent) allMessages.push({ role: "system", content: sysContent });
2024
+ allMessages.push(...messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
2025
+ const res = await fetch(`${baseUrl}/api/chat`, {
2026
+ method: "POST",
2027
+ headers: { "Content-Type": "application/json" },
2028
+ body: JSON.stringify({ model: this.config.model, messages: allMessages, stream: false })
2029
+ });
2030
+ if (!res.ok) throw new Error(`Ollama error ${res.status}`);
2031
+ const data = await res.json();
2032
+ if (onToken) onToken(data.message.content);
2033
+ return { content: data.message.content, tool_calls: null, stop_reason: "end_turn", tokens_used: data.eval_count ?? 0, model: this.config.model };
2034
+ }
2035
+ };
2036
+
2037
+ // packages/daemon/src/AgentExecutor.ts
2038
+ var AgentExecutor = class {
2039
+ constructor(llm, config, onStep, onToken) {
2040
+ this.llm = llm;
2041
+ this.config = config;
2042
+ this.onStep = onStep;
2043
+ this.onToken = onToken;
2044
+ this.cwd = config.cwd;
2045
+ this.maxIterations = config.max_iterations ?? 20;
2046
+ this.maxCommandMs = config.max_command_ms ?? 3e4;
2047
+ }
2048
+ cwd;
2049
+ maxIterations;
2050
+ maxCommandMs;
2051
+ async execute(task, systemContext) {
2052
+ const filesWritten = [];
2053
+ const commandsRun = [];
2054
+ let totalTokens = 0;
2055
+ let modelName = "";
2056
+ const systemPrompt = this.buildSystemPrompt(systemContext);
2057
+ const messages = [
2058
+ { role: "user", content: task }
2059
+ ];
2060
+ let finalOutput = "";
2061
+ for (let i = 0; i < this.maxIterations; i++) {
2062
+ this.onStep(i === 0 ? "Thinking\u2026" : "Continuing\u2026");
2063
+ let response;
2064
+ try {
2065
+ response = await this.llm.completeWithTools(
2066
+ messages,
2067
+ AGENT_TOOLS,
2068
+ systemPrompt,
2069
+ // Only stream tokens on the final (non-tool) turn
2070
+ (token) => {
2071
+ this.onToken(token);
2072
+ finalOutput += token;
2073
+ }
2074
+ );
2075
+ } catch (err) {
2076
+ const msg = err instanceof Error ? err.message : String(err);
2077
+ this.onStep(`LLM error: ${msg}`);
2078
+ finalOutput = `Error: ${msg}`;
2079
+ break;
2080
+ }
2081
+ totalTokens += response.tokens_used;
2082
+ modelName = response.model;
2083
+ if (response.stop_reason === "end_turn" || !response.tool_calls?.length) {
2084
+ if (!finalOutput && response.content) finalOutput = response.content;
2085
+ break;
2086
+ }
2087
+ finalOutput = "";
2088
+ messages.push({
2089
+ role: "assistant",
2090
+ content: response.content,
2091
+ tool_calls: response.tool_calls
2092
+ });
2093
+ for (const tc of response.tool_calls) {
2094
+ this.onStep(`\u25B6 ${tc.name}(${this.summariseInput(tc.name, tc.input)})`);
2095
+ let result;
2096
+ try {
2097
+ result = await this.executeTool(tc.name, tc.input);
2098
+ if (tc.name === "write_file" && tc.input.path) {
2099
+ filesWritten.push(String(tc.input.path));
2100
+ }
2101
+ if (tc.name === "shell_exec" && tc.input.command) {
2102
+ commandsRun.push(String(tc.input.command));
2103
+ }
2104
+ } catch (err) {
2105
+ result = `Error: ${err instanceof Error ? err.message : String(err)}`;
2106
+ }
2107
+ this.onStep(` \u21B3 ${result.slice(0, 120)}${result.length > 120 ? "\u2026" : ""}`);
2108
+ messages.push({
2109
+ role: "tool",
2110
+ content: result,
2111
+ tool_call_id: tc.id
2112
+ });
2113
+ }
2114
+ }
2115
+ return {
2116
+ output: finalOutput || "(no output)",
2117
+ files_written: filesWritten,
2118
+ commands_run: commandsRun,
2119
+ tokens_used: totalTokens,
2120
+ model: modelName,
2121
+ iterations: messages.filter((m) => m.role === "assistant").length
2122
+ };
2123
+ }
2124
+ // ─── Tool execution ────────────────────────────────────────────────────────
2125
+ async executeTool(name, input) {
2126
+ switch (name) {
2127
+ case "shell_exec":
2128
+ return this.shellExec(
2129
+ String(input.command ?? ""),
2130
+ Number(input.timeout_ms ?? this.maxCommandMs)
2131
+ );
2132
+ case "write_file":
2133
+ return this.writeFile(String(input.path ?? ""), String(input.content ?? ""));
2134
+ case "read_file":
2135
+ return this.readFile(String(input.path ?? ""));
2136
+ case "list_dir":
2137
+ return this.listDir(input.path ? String(input.path) : void 0);
2138
+ default:
2139
+ return `Unknown tool: ${name}`;
2140
+ }
2141
+ }
2142
+ shellExec(command, timeoutMs) {
2143
+ return new Promise((resolve6) => {
2144
+ const chunks = [];
2145
+ const proc = spawn("bash", ["-c", command], {
2146
+ cwd: this.cwd,
2147
+ env: { ...process.env, TERM: "dumb" },
2148
+ timeout: timeoutMs
2149
+ });
2150
+ proc.stdout.on("data", (d) => chunks.push(d.toString()));
2151
+ proc.stderr.on("data", (d) => chunks.push(d.toString()));
2152
+ proc.on("close", (code) => {
2153
+ const output = chunks.join("").trim();
2154
+ resolve6(output || (code === 0 ? "(command completed, no output)" : `exit code ${code}`));
2155
+ });
2156
+ proc.on("error", (err) => {
2157
+ resolve6(`Error: ${err.message}`);
2158
+ });
2159
+ });
2160
+ }
2161
+ writeFile(filePath, content) {
2162
+ const safe = this.safePath(filePath);
2163
+ if (!safe) return "Error: path outside working directory";
2164
+ mkdirSync(dirname(safe), { recursive: true });
2165
+ writeFileSync(safe, content, "utf8");
2166
+ const rel = relative(this.cwd, safe);
2167
+ return `Written: ${rel} (${content.length} bytes)`;
2168
+ }
2169
+ readFile(filePath) {
2170
+ const safe = this.safePath(filePath);
2171
+ if (!safe) return "Error: path outside working directory";
2172
+ if (!existsSync2(safe)) return `File not found: ${filePath}`;
2173
+ const content = readFileSync2(safe, "utf8");
2174
+ return content.length > 8e3 ? content.slice(0, 8e3) + `
2175
+ \u2026[truncated, ${content.length} total bytes]` : content;
2176
+ }
2177
+ listDir(dirPath) {
2178
+ const safe = this.safePath(dirPath ?? ".");
2179
+ if (!safe) return "Error: path outside working directory";
2180
+ if (!existsSync2(safe)) return `Directory not found: ${dirPath}`;
2181
+ try {
2182
+ 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");
2183
+ return entries || "(empty directory)";
2184
+ } catch (e) {
2185
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
2186
+ }
2187
+ }
2188
+ // ─── Helpers ───────────────────────────────────────────────────────────────
2189
+ safePath(p) {
2190
+ const resolved = resolve2(this.cwd, p);
2191
+ return resolved.startsWith(this.cwd) ? resolved : null;
2192
+ }
2193
+ buildSystemPrompt(extra) {
2194
+ const lines = [
2195
+ `You are 0agent, an AI software engineer. You can execute shell commands and manage files.`,
2196
+ `Working directory: ${this.cwd}`,
2197
+ ``,
2198
+ `Instructions:`,
2199
+ `- Use tools to actually accomplish tasks, don't just describe what to do`,
2200
+ `- For web servers: write the files, then start the server with & (background)`,
2201
+ `- For npm/node projects: check package.json first with read_file or list_dir`,
2202
+ `- After write_file, verify with read_file if needed`,
2203
+ `- After shell_exec, check output for errors and retry if needed`,
2204
+ `- Use relative paths from the working directory`,
2205
+ `- Be concise in your final response: state what was done and where to find it`
2206
+ ];
2207
+ if (extra) lines.push(``, `Context:`, extra);
2208
+ return lines.join("\n");
2209
+ }
2210
+ summariseInput(toolName, input) {
2211
+ if (toolName === "shell_exec") return `"${String(input.command ?? "").slice(0, 60)}"`;
2212
+ if (toolName === "write_file") return `"${input.path}"`;
2213
+ if (toolName === "read_file") return `"${input.path}"`;
2214
+ if (toolName === "list_dir") return `"${input.path ?? "."}"`;
2215
+ return JSON.stringify(input).slice(0, 60);
2216
+ }
2217
+ };
2218
+
1688
2219
  // packages/daemon/src/SessionManager.ts
1689
2220
  var SessionManager = class {
1690
2221
  sessions = /* @__PURE__ */ new Map();
1691
2222
  inferenceEngine;
1692
2223
  eventBus;
1693
2224
  graph;
2225
+ llm;
2226
+ cwd;
1694
2227
  constructor(deps = {}) {
1695
2228
  this.inferenceEngine = deps.inferenceEngine;
1696
2229
  this.eventBus = deps.eventBus;
1697
2230
  this.graph = deps.graph;
2231
+ this.llm = deps.llm;
2232
+ this.cwd = deps.cwd ?? process.cwd();
1698
2233
  }
1699
2234
  /**
1700
2235
  * Create a new session with status 'pending'.
@@ -1862,14 +2397,36 @@ var SessionManager = class {
1862
2397
  } else {
1863
2398
  this.addStep(session.id, "No inference engine connected \u2014 executing task directly");
1864
2399
  }
1865
- this.addStep(session.id, "Executing\u2026");
1866
- const output = session.plan?.reasoning ?? "Task queued \u2014 no plan selected";
1867
- this.addStep(session.id, `Completed: ${output}`);
1868
- this.completeSession(session.id, {
1869
- output,
1870
- plan: session.plan ?? null,
1871
- steps: session.steps.length
1872
- });
2400
+ if (this.llm?.isConfigured) {
2401
+ const executor = new AgentExecutor(
2402
+ this.llm,
2403
+ { cwd: this.cwd },
2404
+ // step callback → emit session.step events
2405
+ (step) => this.addStep(session.id, step),
2406
+ // token callback → emit session.token events
2407
+ (token) => this.emit({ type: "session.token", session_id: session.id, token })
2408
+ );
2409
+ const systemContext = enrichedReq.context?.system_context ? String(enrichedReq.context.system_context) : void 0;
2410
+ const agentResult = await executor.execute(enrichedReq.task, systemContext);
2411
+ if (agentResult.files_written.length > 0) {
2412
+ this.addStep(session.id, `Files written: ${agentResult.files_written.join(", ")}`);
2413
+ }
2414
+ if (agentResult.commands_run.length > 0) {
2415
+ this.addStep(session.id, `Commands run: ${agentResult.commands_run.length}`);
2416
+ }
2417
+ this.addStep(session.id, `Done (${agentResult.tokens_used} tokens, ${agentResult.iterations} LLM turns)`);
2418
+ this.completeSession(session.id, {
2419
+ output: agentResult.output,
2420
+ files_written: agentResult.files_written,
2421
+ commands_run: agentResult.commands_run,
2422
+ tokens_used: agentResult.tokens_used,
2423
+ model: agentResult.model
2424
+ });
2425
+ } else {
2426
+ const output = session.plan?.reasoning ?? "No LLM configured \u2014 add api_key to ~/.0agent/config.yaml";
2427
+ this.addStep(session.id, "No LLM API key configured");
2428
+ this.completeSession(session.id, { output });
2429
+ }
1873
2430
  } catch (err) {
1874
2431
  const message = err instanceof Error ? err.message : String(err);
1875
2432
  this.failSession(session.id, message);
@@ -2112,7 +2669,7 @@ var BackgroundWorkers = class {
2112
2669
  };
2113
2670
 
2114
2671
  // packages/daemon/src/SkillRegistry.ts
2115
- import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
2672
+ import { readFileSync as readFileSync3, readdirSync as readdirSync2, existsSync as existsSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "node:fs";
2116
2673
  import { join } from "node:path";
2117
2674
  import { homedir as homedir2 } from "node:os";
2118
2675
  import YAML2 from "yaml";
@@ -2135,11 +2692,11 @@ var SkillRegistry = class {
2135
2692
  this.loadFromDir(this.customDir, false);
2136
2693
  }
2137
2694
  loadFromDir(dir, isBuiltin) {
2138
- if (!existsSync2(dir)) return;
2139
- const files = readdirSync(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
2695
+ if (!existsSync3(dir)) return;
2696
+ const files = readdirSync2(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
2140
2697
  for (const file of files) {
2141
2698
  try {
2142
- const raw = readFileSync2(join(dir, file), "utf8");
2699
+ const raw = readFileSync3(join(dir, file), "utf8");
2143
2700
  const skill = YAML2.parse(raw);
2144
2701
  if (skill.name) {
2145
2702
  this.skills.set(skill.name, skill);
@@ -2174,9 +2731,9 @@ var SkillRegistry = class {
2174
2731
  if (this.builtinNames.has(name)) {
2175
2732
  throw new Error(`Cannot override built-in skill: ${name}`);
2176
2733
  }
2177
- mkdirSync(this.customDir, { recursive: true });
2734
+ mkdirSync2(this.customDir, { recursive: true });
2178
2735
  const filePath = join(this.customDir, `${name}.yaml`);
2179
- writeFileSync(filePath, yamlContent, "utf8");
2736
+ writeFileSync2(filePath, yamlContent, "utf8");
2180
2737
  const skill = YAML2.parse(yamlContent);
2181
2738
  this.skills.set(name, skill);
2182
2739
  return skill;
@@ -2189,7 +2746,7 @@ var SkillRegistry = class {
2189
2746
  throw new Error(`Cannot delete built-in skill: ${name}`);
2190
2747
  }
2191
2748
  const filePath = join(this.customDir, `${name}.yaml`);
2192
- if (existsSync2(filePath)) {
2749
+ if (existsSync3(filePath)) {
2193
2750
  unlinkSync(filePath);
2194
2751
  }
2195
2752
  this.skills.delete(name);
@@ -2202,8 +2759,8 @@ var SkillRegistry = class {
2202
2759
  // packages/daemon/src/HTTPServer.ts
2203
2760
  import { Hono as Hono8 } from "hono";
2204
2761
  import { serve } from "@hono/node-server";
2205
- import { readFileSync as readFileSync3 } from "node:fs";
2206
- import { resolve as resolve2, dirname } from "node:path";
2762
+ import { readFileSync as readFileSync4 } from "node:fs";
2763
+ import { resolve as resolve3, dirname as dirname2 } from "node:path";
2207
2764
  import { fileURLToPath } from "node:url";
2208
2765
 
2209
2766
  // packages/daemon/src/routes/health.ts
@@ -2458,15 +3015,15 @@ function skillRoutes(deps) {
2458
3015
  // packages/daemon/src/HTTPServer.ts
2459
3016
  function findGraphHtml() {
2460
3017
  const candidates = [
2461
- resolve2(dirname(fileURLToPath(import.meta.url)), "graph.html"),
3018
+ resolve3(dirname2(fileURLToPath(import.meta.url)), "graph.html"),
2462
3019
  // dev (src/)
2463
- resolve2(dirname(fileURLToPath(import.meta.url)), "..", "graph.html"),
3020
+ resolve3(dirname2(fileURLToPath(import.meta.url)), "..", "graph.html"),
2464
3021
  // bundled (dist/../)
2465
- resolve2(dirname(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
3022
+ resolve3(dirname2(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
2466
3023
  ];
2467
3024
  for (const p of candidates) {
2468
3025
  try {
2469
- readFileSync3(p);
3026
+ readFileSync4(p);
2470
3027
  return p;
2471
3028
  } catch {
2472
3029
  }
@@ -2490,7 +3047,7 @@ var HTTPServer = class {
2490
3047
  this.app.route("/api/skills", skillRoutes({ skillRegistry: deps.skillRegistry }));
2491
3048
  const serveGraph = (c) => {
2492
3049
  try {
2493
- const html = readFileSync3(GRAPH_HTML_PATH, "utf8");
3050
+ const html = readFileSync4(GRAPH_HTML_PATH, "utf8");
2494
3051
  return c.html(html);
2495
3052
  } catch {
2496
3053
  return c.html("<p>Graph UI not found. Run: pnpm build</p>");
@@ -2500,7 +3057,7 @@ var HTTPServer = class {
2500
3057
  this.app.get("/graph", serveGraph);
2501
3058
  }
2502
3059
  start() {
2503
- return new Promise((resolve5) => {
3060
+ return new Promise((resolve6) => {
2504
3061
  this.server = serve(
2505
3062
  {
2506
3063
  fetch: this.app.fetch,
@@ -2508,20 +3065,20 @@ var HTTPServer = class {
2508
3065
  hostname: this.deps.host
2509
3066
  },
2510
3067
  () => {
2511
- resolve5();
3068
+ resolve6();
2512
3069
  }
2513
3070
  );
2514
3071
  });
2515
3072
  }
2516
3073
  stop() {
2517
- return new Promise((resolve5, reject) => {
3074
+ return new Promise((resolve6, reject) => {
2518
3075
  if (!this.server) {
2519
- resolve5();
3076
+ resolve6();
2520
3077
  return;
2521
3078
  }
2522
3079
  this.server.close((err) => {
2523
3080
  if (err) reject(err);
2524
- else resolve5();
3081
+ else resolve6();
2525
3082
  });
2526
3083
  });
2527
3084
  }
@@ -2545,13 +3102,13 @@ var ZeroAgentDaemon = class {
2545
3102
  startedAt = 0;
2546
3103
  pidFilePath;
2547
3104
  constructor() {
2548
- this.pidFilePath = resolve3(homedir3(), ".0agent", "daemon.pid");
3105
+ this.pidFilePath = resolve4(homedir3(), ".0agent", "daemon.pid");
2549
3106
  }
2550
3107
  async start(opts) {
2551
3108
  this.config = await loadConfig(opts?.config_path);
2552
- const dotDir = resolve3(homedir3(), ".0agent");
2553
- if (!existsSync3(dotDir)) {
2554
- mkdirSync2(dotDir, { recursive: true });
3109
+ const dotDir = resolve4(homedir3(), ".0agent");
3110
+ if (!existsSync4(dotDir)) {
3111
+ mkdirSync3(dotDir, { recursive: true });
2555
3112
  }
2556
3113
  this.adapter = new SQLiteAdapter({ db_path: this.config.graph.db_path });
2557
3114
  this.graph = new KnowledgeGraph(this.adapter);
@@ -2562,10 +3119,25 @@ var ZeroAgentDaemon = class {
2562
3119
  this.inferenceEngine = new InferenceEngine(this.graph, resolver, policy);
2563
3120
  this.skillRegistry = new SkillRegistry();
2564
3121
  await this.skillRegistry.loadAll();
3122
+ const defaultLLM = this.config.llm_providers.find((p) => p.is_default) ?? this.config.llm_providers[0];
3123
+ const llmExecutor = defaultLLM ? new LLMExecutor({
3124
+ provider: defaultLLM.provider,
3125
+ model: defaultLLM.model,
3126
+ api_key: defaultLLM.api_key ?? "",
3127
+ base_url: defaultLLM.base_url
3128
+ }) : void 0;
3129
+ if (llmExecutor?.isConfigured) {
3130
+ console.log(`[0agent] LLM: ${defaultLLM?.provider}/${defaultLLM?.model}`);
3131
+ } else {
3132
+ console.warn("[0agent] No LLM API key configured \u2014 tasks will not call the LLM");
3133
+ }
2565
3134
  this.eventBus = new WebSocketEventBus();
2566
3135
  this.sessionManager = new SessionManager({
2567
3136
  inferenceEngine: this.inferenceEngine,
2568
- eventBus: this.eventBus
3137
+ eventBus: this.eventBus,
3138
+ graph: this.graph,
3139
+ llm: llmExecutor,
3140
+ cwd: process.env["ZEROAGENT_CWD"] ?? process.cwd()
2569
3141
  });
2570
3142
  this.backgroundWorkers = new BackgroundWorkers({
2571
3143
  graph: this.graph,
@@ -2587,7 +3159,7 @@ var ZeroAgentDaemon = class {
2587
3159
  getStatus: () => this.getStatus()
2588
3160
  });
2589
3161
  await this.httpServer.start();
2590
- writeFileSync2(this.pidFilePath, String(process.pid), "utf8");
3162
+ writeFileSync3(this.pidFilePath, String(process.pid), "utf8");
2591
3163
  console.log(
2592
3164
  `[0agent] Daemon started on ${this.config.server.host}:${this.config.server.port} (PID: ${process.pid})`
2593
3165
  );
@@ -2621,7 +3193,7 @@ var ZeroAgentDaemon = class {
2621
3193
  this.graph = null;
2622
3194
  }
2623
3195
  this.adapter = null;
2624
- if (existsSync3(this.pidFilePath)) {
3196
+ if (existsSync4(this.pidFilePath)) {
2625
3197
  try {
2626
3198
  unlinkSync2(this.pidFilePath);
2627
3199
  } catch {
@@ -2651,11 +3223,11 @@ var ZeroAgentDaemon = class {
2651
3223
  };
2652
3224
 
2653
3225
  // packages/daemon/src/start.ts
2654
- import { resolve as resolve4 } from "node:path";
3226
+ import { resolve as resolve5 } from "node:path";
2655
3227
  import { homedir as homedir4 } from "node:os";
2656
- import { existsSync as existsSync4 } from "node:fs";
2657
- var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve4(homedir4(), ".0agent", "config.yaml");
2658
- if (!existsSync4(CONFIG_PATH)) {
3228
+ import { existsSync as existsSync5 } from "node:fs";
3229
+ var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve5(homedir4(), ".0agent", "config.yaml");
3230
+ if (!existsSync5(CONFIG_PATH)) {
2659
3231
  console.error(`
2660
3232
  0agent is not initialised.
2661
3233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "A persistent, learning AI agent that runs on your machine. An agent that learns.",
5
5
  "private": false,
6
6
  "license": "Apache-2.0",