agent.libx.js 0.94.5 → 0.94.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,7 +111,7 @@ agentx --resume <id> "…" # resume a specific session
111
111
  - **Any provider** — set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `GROQ_API_KEY`; choose with `-m provider/model`.
112
112
  - **@-file mentions & headless JSON** — reference files inline in a prompt with `@path` (e.g. `explain @src/Agent.ts`); script with `-p --output-format json` to get one machine-readable result object on stdout (activity stays on stderr).
113
113
  - **Tab-completion** — `Tab` completes `/<command>` names and `@<path>` file/dir references (descends subdirs, dotfiles hidden unless typed) straight from the working tree.
114
- - **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the three-tier engine driving turns: a fast voice model (`--voice-model`, default `groq/openai/gpt-oss-120b`) answers every line instantly and delegates real work to background workers built with the same wiring as a normal run (fs mode, permissions, MCP); worker activity shows as dim chrome and results are re-voiced when ready. Switch any tier live with `/model` (opens a reflex/act/think picker), or the `/voice-model` · `/think-model` shortcuts. `/tasks` lists background tasks and cancels a running one from a picker (Esc mid-turn cancels the foreground turn; Esc again at the idle prompt cancels running workers).
114
+ - **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the three-tier engine driving turns: a fast voice model (`--voice-model`, default `groq/openai/gpt-oss-120b`) answers every line instantly and delegates real work to background workers built with the same wiring as a normal run (fs mode, permissions, MCP); worker activity shows as dim chrome and results are re-voiced when ready. Switch any tier live with `/model` (opens a reflex/act/think picker), or the `/voice-model` · `/think-model` shortcuts. `/tasks` lists background tasks, inspects a task's live output tail, and cancels a running one from a picker (Esc mid-turn cancels the foreground turn; Esc again at the idle prompt cancels running workers).
115
115
  - **MCP servers** — declare `mcpServers: { name: { command, args } | { url } }` in config and they're auto-mounted at startup (in parallel, with an optional `mountTimeoutMs` deadline so one slow/dead server never blocks the rest): the client does the JSON-RPC handshake (stdio or HTTP) + `tools/list`, and the discovered tools appear as `mcp__<name>__<tool>` in `/tools` (inspect with `/mcp`). A bad server is logged and skipped, never blocking the agent. For large tool sets, **deferred mode** (`makeMcpToolSearch` / `mountMcpDeferred`) exposes just two bounded tools (`ToolSearch` + `McpCall`) instead of N defs — dodging the provider tool-cap and improving selection accuracy. **`mountMcpCatalog`** goes further: a cached, hash-keyed catalog + lazy connect means a turn that uses no MCP tool opens **zero** connections, and one that uses a tool connects exactly that server — latency scales with tools-used, not servers-configured. A down server is **negative-cached** (`failureCooldownMs`) so it never re-floors a later turn at the deadline. For zero turn-path latency even on a cold process, call **`warmMcpCatalog`** at boot + on a timer (off-turn discovery) and mount with **`{ discover: 'cache-only' }`** — the turn then never synchronously connects: it serves the warmed catalog and discovers any miss in the background.
116
116
 
117
117
  ## 🧬 It improves itself
package/dist/cli.js CHANGED
@@ -4257,22 +4257,30 @@ ${recent}` : brief) + verify;
4257
4257
  const controller = new AbortController();
4258
4258
  const base = tierOpts?.hooks ?? o.actOptions?.hooks;
4259
4259
  const report = o.progressUpdates ? this.progressReporter(id) : void 0;
4260
- const hooks = report ? {
4260
+ const tail = [];
4261
+ const pushTail = (line) => {
4262
+ tail.push(line.slice(0, 200));
4263
+ if (tail.length > 120) tail.splice(0, tail.length - 120);
4264
+ };
4265
+ const hooks = {
4261
4266
  ...base,
4262
4267
  preToolUse: async (call, meta) => {
4263
4268
  const d = await base?.preToolUse?.(call, meta);
4264
- report.pre(call);
4269
+ pushTail(`\u2699 ${describeCall(call)}`);
4270
+ report?.pre(call);
4265
4271
  return d;
4266
4272
  },
4267
4273
  postToolUse: async (call, result, meta) => {
4268
4274
  await base?.postToolUse?.(call, result, meta);
4269
- report.post(call);
4275
+ const last = result?.trim().split("\n").filter(Boolean).pop();
4276
+ if (last) pushTail(` \u21B3 ${last}`);
4277
+ report?.post(call);
4270
4278
  },
4271
4279
  onToolOutput: (call, chunk, meta) => {
4272
4280
  base?.onToolOutput?.(call, chunk, meta);
4273
- report.output(chunk);
4281
+ report?.output(chunk);
4274
4282
  }
4275
- } : base;
4283
+ };
4276
4284
  const relayAsk = async (q2) => {
4277
4285
  const opts = q2.options?.length ? ` Options: ${q2.options.map((x) => x.label).join(", ")}.` : "";
4278
4286
  const a = await this.parkQuestion(id, `${q2.question}${opts}`);
@@ -4294,7 +4302,7 @@ ${recent}` : brief) + verify;
4294
4302
  // shared with the checker so a cancel tears down both
4295
4303
  };
4296
4304
  const promise = new Agent(agentOpts).run(briefText).then((res) => this.maybeVerify(id, briefText, res, tier, agentOpts)).then((res) => this.onWorkerSettled(id, res)).catch((err2) => this.onWorkerFailed(id, err2));
4297
- this.tasks.set(id, { id, label, status: "running", controller, promise });
4305
+ this.tasks.set(id, { id, label, status: "running", controller, promise, tail });
4298
4306
  }
4299
4307
  /** Fresh-context check of a successful Act task: a NEW agent (same model/fs/tools, but NO shared
4300
4308
  * conversation context) re-reads the file state against the brief and fixes any gap. The fix lands
@@ -4413,6 +4421,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4413
4421
  return this.failTask(rec, msg);
4414
4422
  }
4415
4423
  rec.status = "done";
4424
+ rec.result = res.text;
4416
4425
  log7.verbose(`task ${id} done (${res.steps} steps)`);
4417
4426
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
4418
4427
  id,
@@ -4430,6 +4439,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4430
4439
  failTask(rec, msg) {
4431
4440
  this.dropAsk(rec.id);
4432
4441
  rec.status = "error";
4442
+ rec.result = msg;
4433
4443
  log7.warn(`task ${rec.id} failed: ${msg}`);
4434
4444
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
4435
4445
  this.queueRevoice(`[task ${rec.id} failed] ${msg}`);
@@ -10077,7 +10087,7 @@ ${extra}` : body);
10077
10087
  `));
10078
10088
  }
10079
10089
  }, tasks: {
10080
- desc: "background tasks \u2014 /tasks [cancel <id>], or alone for a picker (\u21B5 cancels the selected running task)",
10090
+ desc: "background tasks \u2014 /tasks [cancel <id>], or alone for a picker (\u21B5 inspects output; running tasks can be cancelled)",
10081
10091
  run: async (a) => {
10082
10092
  const all = [...dx.tasks.values()];
10083
10093
  if (!all.length) {
@@ -10094,15 +10104,29 @@ ${extra}` : body);
10094
10104
  return;
10095
10105
  }
10096
10106
  const mark = (s) => s === "running" ? cyan("\u25D4 running") : s === "done" ? green("\u2713 done") : s === "cancelled" ? yellow("\u2298 cancelled") : red(`\u2717 ${s}`);
10097
- if (!process.stderr.isTTY || !process.stdin.isTTY) {
10098
- for (const t of all) err(` ${t.id} ${mark(t.status)} ${dim(t.label.slice(0, 60))}
10107
+ const inspect = (t2) => {
10108
+ err(` ${t2.id} ${mark(t2.status)} ${dim(t2.label)}
10099
10109
  `);
10110
+ for (const l of t2.tail.slice(-20)) err(dim(` ${l}
10111
+ `));
10112
+ if (t2.result) err(dim(` \u29BF ${t2.result.split("\n")[0].slice(0, 160)}
10113
+ `));
10114
+ if (!t2.tail.length && !t2.result) err(dim(" (no activity yet)\n"));
10115
+ };
10116
+ if (!process.stderr.isTTY || !process.stdin.isTTY) {
10117
+ for (const t2 of all) inspect(t2);
10100
10118
  return;
10101
10119
  }
10102
- const items = all.map((t) => ({ label: `${t.id} ${t.label.slice(0, 60)}`, value: t.id, desc: mark(t.status) + (t.status === "running" ? " \xB7 \u21B5 to cancel" : "") }));
10103
- const id = await selectMenu(process.stderr, { title: "Background tasks \xB7 \u21B5 cancel running \xB7 esc close", items });
10104
- if (id) err(dim(` ${dx.cancelTask(String(id))}
10120
+ const items = all.map((t2) => ({ label: `${t2.id} ${t2.label.slice(0, 60)}`, value: t2.id, desc: mark(t2.status) + " \xB7 \u21B5 inspect" }));
10121
+ const id = await selectMenu(process.stderr, { title: "Background tasks \xB7 \u21B5 inspect \xB7 esc close", items });
10122
+ if (!id) return;
10123
+ const t = dx.tasks.get(String(id));
10124
+ inspect(t);
10125
+ if (t.status === "running") {
10126
+ const v = await selectMenu(process.stderr, { title: `Cancel ${t.id}?`, items: [{ label: "Keep running", value: "keep" }, { label: "Cancel the task", value: "cancel" }], current: "keep" });
10127
+ if (v === "cancel") err(dim(` ${dx.cancelTask(t.id)}
10105
10128
  `));
10129
+ }
10106
10130
  }
10107
10131
  } } : {},
10108
10132
  reasoning: {