agent.libx.js 0.92.3 → 0.92.5
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/cli/cli.ts +44 -8
- package/dist/cli.js +41 -7
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/cli/cli.ts
CHANGED
|
@@ -358,27 +358,38 @@ function makeHost(format: 'text' | 'json' | 'stream-json' = 'text', opts?: { str
|
|
|
358
358
|
};
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
/** Hooks that render tool activity to stderr: a preToolUse header, and for edits a colorized diff.
|
|
362
|
-
|
|
361
|
+
/** Hooks that render tool activity to stderr: a preToolUse header, and for edits a colorized diff.
|
|
362
|
+
* `opts.background` marks the agent as a BACKGROUND one (duplex workers): its chrome lands async on
|
|
363
|
+
* top of a live prompt, so it must never drive the foreground spinner (the 'agentx › '/'⠹ thinking…'
|
|
364
|
+
* flicker), clears the prompt line before printing, and repaints via the callback after. `opts.gate`
|
|
365
|
+
* is evaluated per call — false renders nothing (minimal mode: task events only). */
|
|
366
|
+
function displayHooks(fs?: IFilesystem, opts?: { gate?: () => boolean; background?: () => void }): Hooks {
|
|
363
367
|
const EDIT = new Set(['Edit', 'MultiEdit', 'Write']);
|
|
364
368
|
const before = new Map<string, string>();
|
|
365
369
|
const MAX = 64 * 1024; // skip diffing very large files
|
|
370
|
+
const bg = opts?.background;
|
|
371
|
+
const on = () => !opts?.gate || opts.gate();
|
|
366
372
|
const read = async (p: unknown): Promise<string> => {
|
|
367
373
|
if (!fs || typeof p !== 'string') return '';
|
|
368
374
|
try { return await fs.readFile(p); } catch { return ''; }
|
|
369
375
|
};
|
|
370
376
|
return {
|
|
371
377
|
async preToolUse(call) {
|
|
372
|
-
|
|
378
|
+
if (!on()) return;
|
|
379
|
+
if (bg) err('\r\x1b[0J'); else spinner.stop(); // foreground: a tool is about to run → stop "thinking…"
|
|
373
380
|
err(cyan(`\n ⚙ ${call.name}`) + dim(' ' + summarizeCall(call.name, call.args)) + '\n');
|
|
374
381
|
if (EDIT.has(call.name)) before.set(String(call.args?.path), await read(call.args?.path));
|
|
382
|
+
bg?.();
|
|
375
383
|
},
|
|
376
384
|
onToolOutput(_call, chunk) {
|
|
377
|
-
if (!verboseOutput) return; // Ctrl+O verbose: live-tail streaming tool output (default chrome stays calm)
|
|
385
|
+
if (!verboseOutput || !on()) return; // Ctrl+O verbose: live-tail streaming tool output (default chrome stays calm)
|
|
386
|
+
if (bg) err('\r\x1b[0J');
|
|
378
387
|
for (const ln of String(chunk).split('\n')) if (ln.trim()) err(dim(` ⋮ ${ln.length > 200 ? ln.slice(0, 200) + '…' : ln}\n`));
|
|
388
|
+
bg?.();
|
|
379
389
|
},
|
|
380
390
|
async postToolUse(call, result) {
|
|
381
|
-
|
|
391
|
+
if (!on()) return;
|
|
392
|
+
if (bg) err('\r\x1b[0J'); else spinner.stop();
|
|
382
393
|
try {
|
|
383
394
|
if (EDIT.has(call.name)) {
|
|
384
395
|
const path = String(call.args?.path);
|
|
@@ -405,7 +416,8 @@ function displayHooks(fs?: IFilesystem): Hooks {
|
|
|
405
416
|
err(dim(' ⎿ (no output)\n')); // empty result → confirm completion so it doesn't look hung
|
|
406
417
|
}
|
|
407
418
|
} finally {
|
|
408
|
-
|
|
419
|
+
if (bg) bg(); // background: repaint the live prompt below the chrome — NEVER the spinner
|
|
420
|
+
else spinner.start(); // foreground: tool done → the model is thinking about the next step
|
|
409
421
|
}
|
|
410
422
|
},
|
|
411
423
|
};
|
|
@@ -965,13 +977,17 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
965
977
|
let voiceIO: VoiceIO | undefined; // real voice I/O (--voice + keys): mic→STT in, text_delta→TTS out
|
|
966
978
|
let editorRef: LineEditor | undefined; // bound once the line editor exists — async chrome repaints the prompt via it
|
|
967
979
|
let workerOptions: AgentOptions | undefined;
|
|
980
|
+
// Worker UI verbosity: 'full' = ⚙ tool chrome per worker step; 'minimal' = task events only
|
|
981
|
+
// (started/progress/⦿ done). Voice defaults minimal (chrome is noise next to speech); /workers toggles live.
|
|
982
|
+
let workerChrome: 'full' | 'minimal' = 'full';
|
|
968
983
|
let duplexPersist: () => void = () => {}; // bound once the session exists (re-voice fires async)
|
|
969
984
|
let duplexAccount: (data: any) => void = () => {}; // worker cost → session meta (bound below)
|
|
970
985
|
// Workers are non-interactive: a permission 'ask' can't pop a menu mid-conversation (it would fight
|
|
971
986
|
// the line editor for raw stdin). Auto-deny with a narratable reason — the worker adapts or reports
|
|
972
987
|
// back and the voice narrates it. Allow/deny rules (config, --allowedTools, --yes) apply as-is.
|
|
973
988
|
const duplexAsk = async (call: ToolUse): Promise<{ decision: 'allow' | 'deny' }> => {
|
|
974
|
-
err(yellow(` ⊘ worker asked to run ${call.name} — auto-denied (no interactive approval in duplex; use --yes or --allowedTools)\n`));
|
|
989
|
+
err('\r\x1b[0J' + yellow(` ⊘ worker asked to run ${call.name} — auto-denied (no interactive approval in duplex; use --yes or --allowedTools)\n`));
|
|
990
|
+
editorRef?.redrawNow(); // background event at a live prompt — repaint below the notice
|
|
975
991
|
return { decision: 'deny' };
|
|
976
992
|
};
|
|
977
993
|
if (duplex) {
|
|
@@ -982,6 +998,11 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
982
998
|
if (workerOptions.permissions)
|
|
983
999
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: undefined, ask: duplexAsk });
|
|
984
1000
|
workerOptions.planMode = false; // a worker's plan could never be approved (no host) — it would stall to maxSteps
|
|
1001
|
+
// Workers are BACKGROUND agents: rebuild their display hooks so chrome never drives the foreground
|
|
1002
|
+
// spinner (the 'agentx › '/'⠹ thinking…' flicker) and repaints the live prompt after each print.
|
|
1003
|
+
workerChrome = args.voice ? 'minimal' : (cfg.workerChrome ?? 'full');
|
|
1004
|
+
const workerDisplay = displayHooks(agent.options.fs, { gate: () => workerChrome === 'full', background: () => editorRef?.redrawNow() });
|
|
1005
|
+
workerOptions.hooks = cfg.hooks ? composeHooks(workerDisplay, hooksFromConfig(cfg.hooks)) : workerDisplay;
|
|
985
1006
|
// The single voice: markdown-rendered deltas on stdout = the SPOKEN channel. Everything else
|
|
986
1007
|
// (⚙ tool chrome, task events, worker results) is VISUAL chrome on stderr — when wired to a
|
|
987
1008
|
// real voice API, only text_delta becomes speech; the rest feeds the screen.
|
|
@@ -1019,6 +1040,15 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1019
1040
|
editorRef?.redrawNow();
|
|
1020
1041
|
return;
|
|
1021
1042
|
}
|
|
1043
|
+
// Remaining task_* events (started/progress/cancelled/error) land ASYNC at a live prompt —
|
|
1044
|
+
// same treatment as task_done: clear the prompt line, print, repaint (bare err() leaves
|
|
1045
|
+
// residue glued to 'agentx › '). task_done returned above; everything else falls through.
|
|
1046
|
+
if (typeof e.kind === 'string' && e.kind.startsWith('task_')) {
|
|
1047
|
+
spinner.stop();
|
|
1048
|
+
err('\r\x1b[0J' + dim(` · ${e.message}\n`));
|
|
1049
|
+
editorRef?.redrawNow();
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1022
1052
|
base.notify!(e); // makeHost always provides notify (the HostBridge type just marks it optional)
|
|
1023
1053
|
},
|
|
1024
1054
|
};
|
|
@@ -1399,7 +1429,13 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1399
1429
|
else err(dim(' ' + (duplex ? `voice ${dx!.options.voiceModel} · worker ${work.model}` : work.model) + '\n'));
|
|
1400
1430
|
},
|
|
1401
1431
|
},
|
|
1402
|
-
...(duplex ? {
|
|
1432
|
+
...(duplex ? { workers: {
|
|
1433
|
+
desc: 'duplex worker chrome — /workers <full|minimal>: per-step ⚙ tool activity vs task events only',
|
|
1434
|
+
run: async (a: string[]) => {
|
|
1435
|
+
if (a[0] === 'full' || a[0] === 'minimal') { workerChrome = a[0]; err(green(` ✓ worker chrome → ${a[0]}\n`)); return; }
|
|
1436
|
+
err(dim(` worker chrome: ${workerChrome} (use /workers full|minimal)\n`));
|
|
1437
|
+
},
|
|
1438
|
+
}, 'voice-model': {
|
|
1403
1439
|
desc: 'switch the duplex voice (fast) model — /voice-model <id>, or alone for a picker',
|
|
1404
1440
|
run: async (a: string[]) => {
|
|
1405
1441
|
const apply = (id: string) => {
|
package/dist/cli.js
CHANGED
|
@@ -7128,10 +7128,12 @@ function makeHost(format = "text", opts) {
|
|
|
7128
7128
|
}
|
|
7129
7129
|
};
|
|
7130
7130
|
}
|
|
7131
|
-
function displayHooks(fs) {
|
|
7131
|
+
function displayHooks(fs, opts) {
|
|
7132
7132
|
const EDIT = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write"]);
|
|
7133
7133
|
const before = /* @__PURE__ */ new Map();
|
|
7134
7134
|
const MAX = 64 * 1024;
|
|
7135
|
+
const bg = opts?.background;
|
|
7136
|
+
const on = () => !opts?.gate || opts.gate();
|
|
7135
7137
|
const read = async (p) => {
|
|
7136
7138
|
if (!fs || typeof p !== "string") return "";
|
|
7137
7139
|
try {
|
|
@@ -7142,18 +7144,25 @@ function displayHooks(fs) {
|
|
|
7142
7144
|
};
|
|
7143
7145
|
return {
|
|
7144
7146
|
async preToolUse(call) {
|
|
7145
|
-
|
|
7147
|
+
if (!on()) return;
|
|
7148
|
+
if (bg) err("\r\x1B[0J");
|
|
7149
|
+
else spinner.stop();
|
|
7146
7150
|
err(cyan(`
|
|
7147
7151
|
\u2699 ${call.name}`) + dim(" " + summarizeCall(call.name, call.args)) + "\n");
|
|
7148
7152
|
if (EDIT.has(call.name)) before.set(String(call.args?.path), await read(call.args?.path));
|
|
7153
|
+
bg?.();
|
|
7149
7154
|
},
|
|
7150
7155
|
onToolOutput(_call, chunk) {
|
|
7151
|
-
if (!verboseOutput) return;
|
|
7156
|
+
if (!verboseOutput || !on()) return;
|
|
7157
|
+
if (bg) err("\r\x1B[0J");
|
|
7152
7158
|
for (const ln of String(chunk).split("\n")) if (ln.trim()) err(dim(` \u22EE ${ln.length > 200 ? ln.slice(0, 200) + "\u2026" : ln}
|
|
7153
7159
|
`));
|
|
7160
|
+
bg?.();
|
|
7154
7161
|
},
|
|
7155
7162
|
async postToolUse(call, result) {
|
|
7156
|
-
|
|
7163
|
+
if (!on()) return;
|
|
7164
|
+
if (bg) err("\r\x1B[0J");
|
|
7165
|
+
else spinner.stop();
|
|
7157
7166
|
try {
|
|
7158
7167
|
if (EDIT.has(call.name)) {
|
|
7159
7168
|
const path = String(call.args?.path);
|
|
@@ -7182,7 +7191,8 @@ function displayHooks(fs) {
|
|
|
7182
7191
|
err(dim(" \u23BF (no output)\n"));
|
|
7183
7192
|
}
|
|
7184
7193
|
} finally {
|
|
7185
|
-
|
|
7194
|
+
if (bg) bg();
|
|
7195
|
+
else spinner.start();
|
|
7186
7196
|
}
|
|
7187
7197
|
}
|
|
7188
7198
|
};
|
|
@@ -7694,13 +7704,15 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7694
7704
|
let voiceIO;
|
|
7695
7705
|
let editorRef;
|
|
7696
7706
|
let workerOptions;
|
|
7707
|
+
let workerChrome = "full";
|
|
7697
7708
|
let duplexPersist = () => {
|
|
7698
7709
|
};
|
|
7699
7710
|
let duplexAccount = () => {
|
|
7700
7711
|
};
|
|
7701
7712
|
const duplexAsk = async (call) => {
|
|
7702
|
-
err(yellow(` \u2298 worker asked to run ${call.name} \u2014 auto-denied (no interactive approval in duplex; use --yes or --allowedTools)
|
|
7713
|
+
err("\r\x1B[0J" + yellow(` \u2298 worker asked to run ${call.name} \u2014 auto-denied (no interactive approval in duplex; use --yes or --allowedTools)
|
|
7703
7714
|
`));
|
|
7715
|
+
editorRef?.redrawNow();
|
|
7704
7716
|
return { decision: "deny" };
|
|
7705
7717
|
};
|
|
7706
7718
|
if (duplex) {
|
|
@@ -7709,6 +7721,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7709
7721
|
if (workerOptions.permissions)
|
|
7710
7722
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: void 0, ask: duplexAsk });
|
|
7711
7723
|
workerOptions.planMode = false;
|
|
7724
|
+
workerChrome = args.voice ? "minimal" : cfg.workerChrome ?? "full";
|
|
7725
|
+
const workerDisplay = displayHooks(agent.options.fs, { gate: () => workerChrome === "full", background: () => editorRef?.redrawNow() });
|
|
7726
|
+
workerOptions.hooks = cfg.hooks ? composeHooks(workerDisplay, hooksFromConfig(cfg.hooks)) : workerDisplay;
|
|
7712
7727
|
const base = makeHost("text", { stream: true });
|
|
7713
7728
|
const host = {
|
|
7714
7729
|
...base,
|
|
@@ -7738,6 +7753,13 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7738
7753
|
editorRef?.redrawNow();
|
|
7739
7754
|
return;
|
|
7740
7755
|
}
|
|
7756
|
+
if (typeof e.kind === "string" && e.kind.startsWith("task_")) {
|
|
7757
|
+
spinner.stop();
|
|
7758
|
+
err("\r\x1B[0J" + dim(` \xB7 ${e.message}
|
|
7759
|
+
`));
|
|
7760
|
+
editorRef?.redrawNow();
|
|
7761
|
+
return;
|
|
7762
|
+
}
|
|
7741
7763
|
base.notify(e);
|
|
7742
7764
|
}
|
|
7743
7765
|
};
|
|
@@ -8167,7 +8189,19 @@ ${extra}` : body);
|
|
|
8167
8189
|
else err(dim(" " + (duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model) + "\n"));
|
|
8168
8190
|
}
|
|
8169
8191
|
},
|
|
8170
|
-
...duplex ? {
|
|
8192
|
+
...duplex ? { workers: {
|
|
8193
|
+
desc: "duplex worker chrome \u2014 /workers <full|minimal>: per-step \u2699 tool activity vs task events only",
|
|
8194
|
+
run: async (a) => {
|
|
8195
|
+
if (a[0] === "full" || a[0] === "minimal") {
|
|
8196
|
+
workerChrome = a[0];
|
|
8197
|
+
err(green(` \u2713 worker chrome \u2192 ${a[0]}
|
|
8198
|
+
`));
|
|
8199
|
+
return;
|
|
8200
|
+
}
|
|
8201
|
+
err(dim(` worker chrome: ${workerChrome} (use /workers full|minimal)
|
|
8202
|
+
`));
|
|
8203
|
+
}
|
|
8204
|
+
}, "voice-model": {
|
|
8171
8205
|
desc: "switch the duplex voice (fast) model \u2014 /voice-model <id>, or alone for a picker",
|
|
8172
8206
|
run: async (a) => {
|
|
8173
8207
|
const apply = (id) => {
|