0agent 1.0.38 → 1.0.40

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.
Files changed (3) hide show
  1. package/bin/chat.js +67 -24
  2. package/dist/daemon.mjs +130 -25
  3. package/package.json +1 -1
package/bin/chat.js CHANGED
@@ -23,33 +23,64 @@ class Spinner {
23
23
  this._msg = msg;
24
24
  this._frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
25
25
  this._i = 0; this._timer = null; this._active = false;
26
+ this._sessionMode = false; // true during agent sessions — no \r animation
26
27
  }
28
+
29
+ // Animated spinner — safe only at startup when user cannot type yet.
30
+ // Uses \r which conflicts with readline when user is typing.
27
31
  start(msg) {
28
32
  if (this._active) return;
29
33
  if (msg) this._msg = msg;
30
34
  this._active = true;
35
+ this._sessionMode = false;
31
36
  this._timer = setInterval(() => {
32
37
  process.stdout.write(`\r \x1b[36m${this._frames[this._i++ % this._frames.length]}\x1b[0m \x1b[2m${this._msg}\x1b[0m `);
33
38
  }, 80);
34
39
  }
40
+
41
+ // Session mode — prints a one-time static status line, NO \r animation.
42
+ // readline owns the cursor; call rl.prompt(true) after this to show › .
43
+ startSession(msg) {
44
+ if (this._active) return;
45
+ if (msg) this._msg = msg;
46
+ this._active = true;
47
+ this._sessionMode = true;
48
+ process.stdout.write(` \x1b[2m⠋ ${this._msg}...\x1b[0m\n`);
49
+ }
50
+
35
51
  update(msg) { this._msg = msg; }
52
+
36
53
  stop(clearIt = true) {
37
54
  if (!this._active) return;
38
- clearInterval(this._timer); this._timer = null; this._active = false;
39
- if (clearIt) process.stdout.write('\r\x1b[2K');
55
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
56
+ const wasSession = this._sessionMode;
57
+ this._active = false;
58
+ this._sessionMode = false;
59
+ // Only clear the \r-based animation line; session mode output flows naturally
60
+ if (clearIt && !wasSession) {
61
+ process.stdout.write('\r\x1b[2K');
62
+ }
40
63
  }
64
+
41
65
  get active() { return this._active; }
42
66
 
43
- /**
44
- * Pause spinner, run fn() (which may print lines), then resume.
45
- * Prevents spinner from overwriting printed content.
46
- */
67
+ // Pause to print something cleanly, then resume.
47
68
  pauseFor(fn) {
48
- const wasActive = this._active;
49
- const savedMsg = this._msg;
50
- if (wasActive) this.stop(true);
69
+ const wasActive = this._active;
70
+ const wasSession = this._sessionMode;
71
+ const savedMsg = this._msg;
72
+ this.stop(!wasSession); // clear animated spinner; session mode: just deactivate
51
73
  fn();
52
- if (wasActive) this.start(savedMsg);
74
+ if (wasActive) {
75
+ if (wasSession) {
76
+ // Re-mark active without printing again — readline is showing the prompt
77
+ this._active = true;
78
+ this._sessionMode = true;
79
+ this._msg = savedMsg;
80
+ } else {
81
+ this.start(savedMsg);
82
+ }
83
+ }
53
84
  }
54
85
  }
55
86
 
@@ -192,13 +223,20 @@ function handleWsEvent(event) {
192
223
  case 'session.step': {
193
224
  spinner.stop();
194
225
  if (streaming) { process.stdout.write('\n'); streaming = false; }
226
+ // Clear current readline line, print step, then restore › prompt
227
+ process.stdout.write('\r\x1b[2K');
195
228
  console.log(` ${fmt(C.dim, '›')} ${event.step}`);
196
- spinner.start(event.step.slice(0, 50)); // resume with current step label
229
+ spinner.startSession(event.step.slice(0, 50));
230
+ rl.prompt(true); // restore › so user can keep typing
197
231
  break;
198
232
  }
199
233
  case 'session.token': {
200
234
  spinner.stop();
201
- if (!streaming) { process.stdout.write('\n '); streaming = true; }
235
+ if (!streaming) {
236
+ // Clear › prompt line before streaming response
237
+ process.stdout.write('\r\x1b[2K\n ');
238
+ streaming = true;
239
+ }
202
240
  process.stdout.write(event.token);
203
241
  lineBuffer += event.token;
204
242
  break;
@@ -383,9 +421,10 @@ async function runTask(input) {
383
421
  });
384
422
  const s = await res.json();
385
423
  sessionId = s.session_id ?? s.id;
386
- // Show affordance so user knows they can queue messages while agent works
387
- process.stdout.write(`\n \x1b[2m(type and press Enter to queue next message)\x1b[0m\n`);
388
- spinner.start('Thinking');
424
+ // Start session-mode status (no \r animation) then restore so user can type
425
+ process.stdout.write('\n');
426
+ spinner.startSession('Thinking');
427
+ rl.prompt(true); // keep › visible — user can queue next message while agent works
389
428
 
390
429
  // Polling fallback — runs concurrently with WS events.
391
430
  // Catches completion when WS is disconnected (e.g. daemon just restarted).
@@ -416,8 +455,10 @@ async function runTask(input) {
416
455
  const steps = session.steps ?? [];
417
456
  for (let j = lastPolledStep; j < steps.length; j++) {
418
457
  spinner.stop();
458
+ process.stdout.write('\r\x1b[2K');
419
459
  console.log(` \x1b[2m›\x1b[0m ${steps[j].description}`);
420
- spinner.start(steps[j].description.slice(0, 50));
460
+ spinner.startSession(steps[j].description.slice(0, 50));
461
+ rl.prompt(true);
421
462
  }
422
463
  lastPolledStep = steps.length;
423
464
 
@@ -968,14 +1009,16 @@ rl.on('line', async (input) => {
968
1009
  if (pendingResolve) {
969
1010
  messageQueue.push(line);
970
1011
  const qLen = messageQueue.length;
971
- spinner.pauseFor(() => {
972
- process.stdout.write(
973
- ` ${fmt(C.magenta, '')} ${fmt(C.bold, `[queued #${qLen}]`)} ${fmt(C.dim, line.slice(0, 70))}\n`
974
- );
975
- if (qLen > 1) {
976
- process.stdout.write(` ${fmt(C.dim, `${qLen} tasks waiting`)}\n`);
977
- }
978
- });
1012
+ // No spinner.pauseFor() needed — session mode has no \r animation
1013
+ // Just print the queued confirmation and restore the › prompt
1014
+ process.stdout.write('\r\x1b[2K');
1015
+ process.stdout.write(
1016
+ ` ${fmt(C.magenta, '↳')} ${fmt(C.bold, `[queued #${qLen}]`)} ${fmt(C.dim, line.slice(0, 70))}\n`
1017
+ );
1018
+ if (qLen > 1) {
1019
+ process.stdout.write(` ${fmt(C.dim, `${qLen} tasks waiting`)}\n`);
1020
+ }
1021
+ rl.prompt(true); // keep › visible
979
1022
  return;
980
1023
  }
981
1024
 
package/dist/daemon.mjs CHANGED
@@ -2406,14 +2406,14 @@ var init_FileCapability = __esm({
2406
2406
  "use strict";
2407
2407
  FileCapability = class {
2408
2408
  name = "file_op";
2409
- description = "Read, write, or list files. Scoped to working directory.";
2409
+ description = "Read, write, list files, or create directories. Scoped to working directory.";
2410
2410
  toolDefinition = {
2411
2411
  name: "file_op",
2412
- description: "Read, write, or list files in the working directory.",
2412
+ description: "Read, write, list files, or create directories in the working directory.",
2413
2413
  input_schema: {
2414
2414
  type: "object",
2415
2415
  properties: {
2416
- op: { type: "string", description: '"read", "write", or "list"' },
2416
+ op: { type: "string", description: '"read", "write", "list", or "mkdir"' },
2417
2417
  path: { type: "string", description: "File or directory path (relative to cwd)" },
2418
2418
  content: { type: "string", description: "Content for write operation" }
2419
2419
  },
@@ -2448,7 +2448,11 @@ var init_FileCapability = __esm({
2448
2448
  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");
2449
2449
  return { success: true, output: entries || "(empty)", duration_ms: Date.now() - start };
2450
2450
  }
2451
- return { success: false, output: `Unknown op: ${op}`, duration_ms: Date.now() - start };
2451
+ if (op === "mkdir") {
2452
+ mkdirSync(safe, { recursive: true });
2453
+ return { success: true, output: `Directory created: ${rel}`, duration_ms: Date.now() - start };
2454
+ }
2455
+ return { success: false, output: `Unknown op: ${op}. Use "read", "write", "list", or "mkdir"`, duration_ms: Date.now() - start };
2452
2456
  } catch (err) {
2453
2457
  return { success: false, output: `Error: ${err instanceof Error ? err.message : String(err)}`, duration_ms: Date.now() - start };
2454
2458
  }
@@ -2457,6 +2461,74 @@ var init_FileCapability = __esm({
2457
2461
  }
2458
2462
  });
2459
2463
 
2464
+ // packages/daemon/src/capabilities/MemoryCapability.ts
2465
+ var MemoryCapability;
2466
+ var init_MemoryCapability = __esm({
2467
+ "packages/daemon/src/capabilities/MemoryCapability.ts"() {
2468
+ "use strict";
2469
+ init_src();
2470
+ MemoryCapability = class {
2471
+ constructor(graph) {
2472
+ this.graph = graph;
2473
+ }
2474
+ name = "memory_write";
2475
+ description = "Persist a discovered fact to long-term memory so it survives across sessions.";
2476
+ toolDefinition = {
2477
+ name: "memory_write",
2478
+ description: 'Write an important fact to long-term memory. Call this whenever you discover something worth remembering: URLs (ngrok, live servers), file paths, port numbers, API endpoints, configuration values, decisions made, or task outcomes. Examples: memory_write({label:"ngrok_url", content:"https://abc.ngrok.io", type:"url"}) or memory_write({label:"project_port", content:"3000", type:"config"})',
2479
+ input_schema: {
2480
+ type: "object",
2481
+ properties: {
2482
+ label: { type: "string", description: 'Short name for this fact (e.g. "ngrok_url", "project_port")' },
2483
+ content: { type: "string", description: "The value to remember" },
2484
+ type: { type: "string", description: 'Category: "url", "path", "config", "note", "outcome"' }
2485
+ },
2486
+ required: ["label", "content"]
2487
+ }
2488
+ };
2489
+ async execute(input, _cwd) {
2490
+ const label = String(input.label ?? "").trim();
2491
+ const content = String(input.content ?? "").trim();
2492
+ const type = String(input.type ?? "note").trim();
2493
+ const start = Date.now();
2494
+ if (!label || !content) {
2495
+ return { success: false, output: "label and content are required", duration_ms: 0 };
2496
+ }
2497
+ try {
2498
+ const nodeId = `memory:${label.toLowerCase().replace(/[^a-z0-9_]/g, "_")}`;
2499
+ const existing = this.graph.getNode(nodeId);
2500
+ if (existing) {
2501
+ this.graph.updateNode(nodeId, {
2502
+ label,
2503
+ metadata: { ...existing.metadata, content, type, updated_at: (/* @__PURE__ */ new Date()).toISOString() }
2504
+ });
2505
+ } else {
2506
+ const node = createNode({
2507
+ id: nodeId,
2508
+ graph_id: "root",
2509
+ label,
2510
+ type: "context" /* CONTEXT */,
2511
+ metadata: { content, type, saved_at: (/* @__PURE__ */ new Date()).toISOString() }
2512
+ });
2513
+ this.graph.addNode(node);
2514
+ }
2515
+ return {
2516
+ success: true,
2517
+ output: `Remembered: "${label}" = ${content.slice(0, 120)}${content.length > 120 ? "\u2026" : ""}`,
2518
+ duration_ms: Date.now() - start
2519
+ };
2520
+ } catch (err) {
2521
+ return {
2522
+ success: false,
2523
+ output: `Memory write failed: ${err instanceof Error ? err.message : String(err)}`,
2524
+ duration_ms: Date.now() - start
2525
+ };
2526
+ }
2527
+ }
2528
+ };
2529
+ }
2530
+ });
2531
+
2460
2532
  // packages/daemon/src/capabilities/CodespaceBrowserCapability.ts
2461
2533
  var CodespaceBrowserCapability_exports = {};
2462
2534
  __export(CodespaceBrowserCapability_exports, {
@@ -2541,6 +2613,7 @@ var init_CapabilityRegistry = __esm({
2541
2613
  init_ScraperCapability();
2542
2614
  init_ShellCapability();
2543
2615
  init_FileCapability();
2616
+ init_MemoryCapability();
2544
2617
  CapabilityRegistry = class {
2545
2618
  capabilities = /* @__PURE__ */ new Map();
2546
2619
  /**
@@ -2553,7 +2626,7 @@ var init_CapabilityRegistry = __esm({
2553
2626
  * task_type: browser_task). The main agent does NOT have direct access
2554
2627
  * to browser_open without going through a subagent spawn.
2555
2628
  */
2556
- constructor(codespaceManager) {
2629
+ constructor(codespaceManager, graph) {
2557
2630
  this.register(new WebSearchCapability());
2558
2631
  if (codespaceManager) {
2559
2632
  try {
@@ -2568,6 +2641,9 @@ var init_CapabilityRegistry = __esm({
2568
2641
  this.register(new ScraperCapability());
2569
2642
  this.register(new ShellCapability());
2570
2643
  this.register(new FileCapability());
2644
+ if (graph) {
2645
+ this.register(new MemoryCapability(graph));
2646
+ }
2571
2647
  }
2572
2648
  register(cap) {
2573
2649
  this.capabilities.set(cap.name, cap);
@@ -2633,7 +2709,7 @@ var init_AgentExecutor = __esm({
2633
2709
  this.maxIterations = config.max_iterations ?? 20;
2634
2710
  this.maxCommandMs = config.max_command_ms ?? 3e4;
2635
2711
  this.agentRoot = config.agent_root;
2636
- this.registry = new CapabilityRegistry();
2712
+ this.registry = new CapabilityRegistry(void 0, config.graph);
2637
2713
  }
2638
2714
  cwd;
2639
2715
  maxIterations;
@@ -2658,23 +2734,39 @@ var init_AgentExecutor = __esm({
2658
2734
  for (let i = 0; i < this.maxIterations; i++) {
2659
2735
  this.onStep(i === 0 ? "Thinking\u2026" : "Continuing\u2026");
2660
2736
  let response;
2661
- try {
2662
- response = await this.llm.completeWithTools(
2663
- messages,
2664
- this.registry.getToolDefinitions(),
2665
- systemPrompt,
2666
- // Only stream tokens on the final (non-tool) turn
2667
- (token) => {
2668
- this.onToken(token);
2669
- finalOutput += token;
2737
+ let llmFailed = false;
2738
+ {
2739
+ let llmRetry = 0;
2740
+ while (true) {
2741
+ try {
2742
+ response = await this.llm.completeWithTools(
2743
+ messages,
2744
+ this.registry.getToolDefinitions(),
2745
+ systemPrompt,
2746
+ // Only stream tokens on the final (non-tool) turn
2747
+ (token) => {
2748
+ this.onToken(token);
2749
+ finalOutput += token;
2750
+ }
2751
+ );
2752
+ break;
2753
+ } catch (err) {
2754
+ const msg = err instanceof Error ? err.message : String(err);
2755
+ const isTimeout = /timeout|AbortError|aborted/i.test(msg);
2756
+ if (isTimeout && llmRetry < 2) {
2757
+ llmRetry++;
2758
+ this.onStep(`LLM timeout \u2014 retrying (${llmRetry}/2)\u2026`);
2759
+ await new Promise((r) => setTimeout(r, 2e3 * llmRetry));
2760
+ continue;
2761
+ }
2762
+ this.onStep(`LLM error: ${msg}`);
2763
+ finalOutput = `Error: ${msg}`;
2764
+ llmFailed = true;
2765
+ break;
2670
2766
  }
2671
- );
2672
- } catch (err) {
2673
- const msg = err instanceof Error ? err.message : String(err);
2674
- this.onStep(`LLM error: ${msg}`);
2675
- finalOutput = `Error: ${msg}`;
2676
- break;
2767
+ }
2677
2768
  }
2769
+ if (llmFailed) break;
2678
2770
  totalTokens += response.tokens_used;
2679
2771
  modelName = response.model;
2680
2772
  if (response.stop_reason === "end_turn" || !response.tool_calls?.length) {
@@ -2889,6 +2981,7 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
2889
2981
  }
2890
2982
  buildSystemPrompt(extra, task) {
2891
2983
  const isSelfMod = !!(task && SELF_MOD_PATTERN.test(task));
2984
+ const hasMemory = !!this.config.graph;
2892
2985
  const lines = [
2893
2986
  `You are 0agent, an AI software engineer. You can execute shell commands and manage files.`,
2894
2987
  `Working directory: ${this.cwd}`,
@@ -2899,12 +2992,24 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
2899
2992
  ` cmd > /tmp/0agent-server.log 2>&1 &`,
2900
2993
  ` Example: python3 -m http.server 3000 > /tmp/0agent-server.log 2>&1 &`,
2901
2994
  ` NEVER run background commands without redirecting output.`,
2902
- `- For npm/node projects: check package.json first with read_file or list_dir`,
2903
- `- After write_file, verify with read_file if needed`,
2995
+ `- To create a folder: use file_op with op="mkdir" and path="folder/name"`,
2996
+ `- To create a file (and its parent folders): use file_op with op="write" \u2014 parent dirs are created automatically`,
2997
+ `- For npm/node projects: check package.json first with file_op op="list"`,
2998
+ `- After writing files, verify with file_op op="read" if needed`,
2904
2999
  `- After shell_exec, check output for errors and retry if needed`,
2905
3000
  `- For research tasks: use web_search first, then scrape_url for full page content`,
2906
3001
  `- Use relative paths from the working directory`,
2907
- `- Be concise in your final response: state what was done and where to find it`
3002
+ `- Be concise in your final response: state what was done and where to find it`,
3003
+ ...hasMemory ? [
3004
+ ``,
3005
+ `Memory (IMPORTANT):`,
3006
+ `- ALWAYS call memory_write after discovering important facts:`,
3007
+ ` \xB7 Live URLs (ngrok, deployed apps, APIs): memory_write({label:"ngrok_url", content:"https://...", type:"url"})`,
3008
+ ` \xB7 Server ports: memory_write({label:"dev_server_port", content:"3000", type:"config"})`,
3009
+ ` \xB7 File paths of created projects: memory_write({label:"project_path", content:"/path/to/project", type:"path"})`,
3010
+ ` \xB7 Task outcomes: memory_write({label:"last_task_result", content:"...", type:"outcome"})`,
3011
+ `- Call memory_write immediately when you find the value, not just at the end`
3012
+ ] : []
2908
3013
  ];
2909
3014
  if (isSelfMod && this.agentRoot) {
2910
3015
  lines.push(
@@ -4597,7 +4702,7 @@ var SessionManager = class {
4597
4702
  if (activeLLM?.isConfigured) {
4598
4703
  const executor = new AgentExecutor(
4599
4704
  activeLLM,
4600
- { cwd: this.cwd, agent_root: this.agentRoot },
4705
+ { cwd: this.cwd, agent_root: this.agentRoot, graph: this.graph },
4601
4706
  // step callback → emit session.step events
4602
4707
  (step) => this.addStep(sessionId, step),
4603
4708
  // token callback → emit session.token events
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
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",