agent.libx.js 0.93.18 → 0.93.20

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
@@ -77,7 +77,7 @@ agentx --resume <id> "…" # resume a specific session
77
77
  - **@-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).
78
78
  - **Tab-completion** — `Tab` completes `/<command>` names and `@<path>` file/dir references (descends subdirs, dotfiles hidden unless typed) straight from the working tree.
79
79
  - **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the dual-model engine driving turns: a fast voice model (`--voice-model`, default haiku) 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.
80
- - **MCP servers** — declare `mcpServers: { name: { command, args } | { url } }` in config and they're auto-mounted at startup: 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.
80
+ - **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.
81
81
 
82
82
  ## 🧬 It improves itself
83
83
 
package/cli/cli.ts CHANGED
@@ -1035,6 +1035,9 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
1035
1035
  let dx: DuplexAgent | undefined;
1036
1036
  let voiceIO: VoiceIO | undefined; // real voice I/O (--voice + keys): mic→STT in, text_delta→TTS out
1037
1037
  let editorRef: LineEditor | undefined; // bound once the line editor exists — async chrome repaints the prompt via it
1038
+ // During a turn the user's type-ahead lives on a "stash ›" line (no active editor to own it). Async
1039
+ // chrome (streamed deltas, task events) lands on top of it — repaint the stash below, so it survives.
1040
+ let repaintStash: () => void = () => {};
1038
1041
  let workerOptions: AgentOptions | undefined;
1039
1042
  // Worker UI verbosity: 'full' = ⚙ tool chrome per worker step; 'minimal' = task events only
1040
1043
  // (started/progress/⦿ done). Voice defaults minimal (chrome is noise next to speech); /workers toggles live.
@@ -1110,6 +1113,13 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
1110
1113
  if (e.kind === 'text_delta' && voiceIO) {
1111
1114
  voiceIO.speakDelta(e.message);
1112
1115
  editorRef?.suspend(); // no-op when already suspended
1116
+ } else if (e.kind === 'text_delta' && stashBuf) {
1117
+ // TEXT mode with type-ahead pending: lift the sacred stash line, let the markdown stream
1118
+ // land, then repaint the stash below it (else the streamed ack overwrites what's being typed).
1119
+ process.stdout.write('\r\x1b[K');
1120
+ base.notify!(e);
1121
+ repaintStash();
1122
+ return;
1113
1123
  }
1114
1124
  if (e.kind === 'hold_filler' && voiceIO) {
1115
1125
  voiceIO.speakFiller(e.message);
@@ -1135,6 +1145,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
1135
1145
  if (lines.length > shown.length) err(dim(` … (+${lines.length - shown.length} more lines)\n`));
1136
1146
  duplexAccount(e.data); // worker tokens/cost are real spend — fold into /cost
1137
1147
  editorRef?.redrawNow();
1148
+ repaintStash(); // type-ahead survives the async ⦿ landing
1138
1149
  return;
1139
1150
  }
1140
1151
  // Remaining task_* events (started/progress/cancelled/error) land ASYNC at a live prompt —
@@ -1145,6 +1156,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
1145
1156
  // asks are decisions, not chatter — make them stand out (the voice also speaks them)
1146
1157
  err('\r\x1b[0J' + (e.kind === 'task_ask' ? yellow(` ? ${e.message} — answer by voice or type yes/no\n`) : dim(` · ${e.message}\n`)));
1147
1158
  editorRef?.redrawNow();
1159
+ repaintStash(); // type-ahead survives the async task event landing
1148
1160
  return;
1149
1161
  }
1150
1162
  base.notify!(e); // makeHost always provides notify (the HostBridge type just marks it optional)
@@ -1921,6 +1933,8 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
1921
1933
  const q = inputStash.length ? dim(` [${inputStash.length} queued]`) : '';
1922
1934
  err(`\r\x1b[K${dim(' stash › ')}${stashBuf}${q}`);
1923
1935
  };
1936
+ repaintStash = renderStashBuf; // async chrome repaints the type-ahead line through this
1937
+
1924
1938
  process.stdin.on('keypress', (_s, key) => {
1925
1939
  if (!activeTurn) return;
1926
1940
  if (key?.ctrl && key?.name === 'o') { toggleVerbose(); return; }
@@ -2064,9 +2078,12 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
2064
2078
  while (true) {
2065
2079
  // Double-Esc fired during the just-finished turn → open the jump-back picker now (turn has unwound).
2066
2080
  if (pendingRewind) { pendingRewind = false; const t = await rewindToMessage(); if (t !== undefined) prefill = t; }
2067
- aborting = false; stashBuf = ''; // re-arm cancel for the next turn
2081
+ aborting = false;
2082
+ const carry = stashBuf; stashBuf = ''; // type-ahead typed (not Enter'd) during the turn → carry it forward
2068
2083
  err('\n'); // blank line before the prompt (the editor renders on one line)
2069
- const initial = prefill; prefill = undefined; // consume the pending jump-back text (once)
2084
+ // Consume the pending jump-back text (once); else fall back to un-submitted type-ahead so a line
2085
+ // typed while the turn ran isn't lost — it lands editable in the fresh prompt (CC parity).
2086
+ const initial = prefill ?? (carry || undefined); prefill = undefined;
2070
2087
  // Dim status footer under the prompt (context% · cost). Constant while typing one line (transcript is
2071
2088
  // fixed until submit), so compute once per iteration. Hidden on a fresh REPL (no usage yet) to stay clean.
2072
2089
  const ctxTok = estimateTranscriptTokens(face.transcript); // expensive: compute once per iteration (not per keystroke)
package/dist/cli.js CHANGED
@@ -8600,6 +8600,8 @@ async function repl(args, ai, cfg, cwd) {
8600
8600
  let dx;
8601
8601
  let voiceIO;
8602
8602
  let editorRef;
8603
+ let repaintStash = () => {
8604
+ };
8603
8605
  let workerOptions;
8604
8606
  let workerChrome = "full";
8605
8607
  let duplexPersist = () => {
@@ -8655,6 +8657,11 @@ async function repl(args, ai, cfg, cwd) {
8655
8657
  if (e.kind === "text_delta" && voiceIO) {
8656
8658
  voiceIO.speakDelta(e.message);
8657
8659
  editorRef?.suspend();
8660
+ } else if (e.kind === "text_delta" && stashBuf) {
8661
+ process.stdout.write("\r\x1B[K");
8662
+ base.notify(e);
8663
+ repaintStash();
8664
+ return;
8658
8665
  }
8659
8666
  if (e.kind === "hold_filler" && voiceIO) {
8660
8667
  voiceIO.speakFiller(e.message);
@@ -8679,6 +8686,7 @@ async function repl(args, ai, cfg, cwd) {
8679
8686
  `));
8680
8687
  duplexAccount(e.data);
8681
8688
  editorRef?.redrawNow();
8689
+ repaintStash();
8682
8690
  return;
8683
8691
  }
8684
8692
  if (typeof e.kind === "string" && e.kind.startsWith("task_")) {
@@ -8687,6 +8695,7 @@ async function repl(args, ai, cfg, cwd) {
8687
8695
  `) : dim(` \xB7 ${e.message}
8688
8696
  `)));
8689
8697
  editorRef?.redrawNow();
8698
+ repaintStash();
8690
8699
  return;
8691
8700
  }
8692
8701
  base.notify(e);
@@ -9681,6 +9690,7 @@ ${extra}` : body);
9681
9690
  const q2 = inputStash.length ? dim(` [${inputStash.length} queued]`) : "";
9682
9691
  err(`\r\x1B[K${dim(" stash \u203A ")}${stashBuf}${q2}`);
9683
9692
  };
9693
+ repaintStash = renderStashBuf;
9684
9694
  process.stdin.on("keypress", (_s, key) => {
9685
9695
  if (!activeTurn) return;
9686
9696
  if (key?.ctrl && key?.name === "o") {
@@ -9870,9 +9880,10 @@ ${extra}` : body);
9870
9880
  if (t !== void 0) prefill = t;
9871
9881
  }
9872
9882
  aborting = false;
9883
+ const carry = stashBuf;
9873
9884
  stashBuf = "";
9874
9885
  err("\n");
9875
- const initial = prefill;
9886
+ const initial = prefill ?? (carry || void 0);
9876
9887
  prefill = void 0;
9877
9888
  const ctxTok = estimateTranscriptTokens(face.transcript);
9878
9889
  const ctxCap = face.options.maxTokens || 2e5;