agent.libx.js 0.92.3 → 0.92.4
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 +33 -7
- package/dist/cli.js +32 -6
- 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,6 +977,9 @@ 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
|
|
@@ -982,6 +997,11 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
982
997
|
if (workerOptions.permissions)
|
|
983
998
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: undefined, ask: duplexAsk });
|
|
984
999
|
workerOptions.planMode = false; // a worker's plan could never be approved (no host) — it would stall to maxSteps
|
|
1000
|
+
// Workers are BACKGROUND agents: rebuild their display hooks so chrome never drives the foreground
|
|
1001
|
+
// spinner (the 'agentx › '/'⠹ thinking…' flicker) and repaints the live prompt after each print.
|
|
1002
|
+
workerChrome = args.voice ? 'minimal' : (cfg.workerChrome ?? 'full');
|
|
1003
|
+
const workerDisplay = displayHooks(agent.options.fs, { gate: () => workerChrome === 'full', background: () => editorRef?.redrawNow() });
|
|
1004
|
+
workerOptions.hooks = cfg.hooks ? composeHooks(workerDisplay, hooksFromConfig(cfg.hooks)) : workerDisplay;
|
|
985
1005
|
// The single voice: markdown-rendered deltas on stdout = the SPOKEN channel. Everything else
|
|
986
1006
|
// (⚙ tool chrome, task events, worker results) is VISUAL chrome on stderr — when wired to a
|
|
987
1007
|
// real voice API, only text_delta becomes speech; the rest feeds the screen.
|
|
@@ -1399,7 +1419,13 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1399
1419
|
else err(dim(' ' + (duplex ? `voice ${dx!.options.voiceModel} · worker ${work.model}` : work.model) + '\n'));
|
|
1400
1420
|
},
|
|
1401
1421
|
},
|
|
1402
|
-
...(duplex ? {
|
|
1422
|
+
...(duplex ? { workers: {
|
|
1423
|
+
desc: 'duplex worker chrome — /workers <full|minimal>: per-step ⚙ tool activity vs task events only',
|
|
1424
|
+
run: async (a: string[]) => {
|
|
1425
|
+
if (a[0] === 'full' || a[0] === 'minimal') { workerChrome = a[0]; err(green(` ✓ worker chrome → ${a[0]}\n`)); return; }
|
|
1426
|
+
err(dim(` worker chrome: ${workerChrome} (use /workers full|minimal)\n`));
|
|
1427
|
+
},
|
|
1428
|
+
}, 'voice-model': {
|
|
1403
1429
|
desc: 'switch the duplex voice (fast) model — /voice-model <id>, or alone for a picker',
|
|
1404
1430
|
run: async (a: string[]) => {
|
|
1405
1431
|
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,6 +7704,7 @@ 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 = () => {
|
|
@@ -7709,6 +7720,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7709
7720
|
if (workerOptions.permissions)
|
|
7710
7721
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: void 0, ask: duplexAsk });
|
|
7711
7722
|
workerOptions.planMode = false;
|
|
7723
|
+
workerChrome = args.voice ? "minimal" : cfg.workerChrome ?? "full";
|
|
7724
|
+
const workerDisplay = displayHooks(agent.options.fs, { gate: () => workerChrome === "full", background: () => editorRef?.redrawNow() });
|
|
7725
|
+
workerOptions.hooks = cfg.hooks ? composeHooks(workerDisplay, hooksFromConfig(cfg.hooks)) : workerDisplay;
|
|
7712
7726
|
const base = makeHost("text", { stream: true });
|
|
7713
7727
|
const host = {
|
|
7714
7728
|
...base,
|
|
@@ -8167,7 +8181,19 @@ ${extra}` : body);
|
|
|
8167
8181
|
else err(dim(" " + (duplex ? `voice ${dx.options.voiceModel} \xB7 worker ${work.model}` : work.model) + "\n"));
|
|
8168
8182
|
}
|
|
8169
8183
|
},
|
|
8170
|
-
...duplex ? {
|
|
8184
|
+
...duplex ? { workers: {
|
|
8185
|
+
desc: "duplex worker chrome \u2014 /workers <full|minimal>: per-step \u2699 tool activity vs task events only",
|
|
8186
|
+
run: async (a) => {
|
|
8187
|
+
if (a[0] === "full" || a[0] === "minimal") {
|
|
8188
|
+
workerChrome = a[0];
|
|
8189
|
+
err(green(` \u2713 worker chrome \u2192 ${a[0]}
|
|
8190
|
+
`));
|
|
8191
|
+
return;
|
|
8192
|
+
}
|
|
8193
|
+
err(dim(` worker chrome: ${workerChrome} (use /workers full|minimal)
|
|
8194
|
+
`));
|
|
8195
|
+
}
|
|
8196
|
+
}, "voice-model": {
|
|
8171
8197
|
desc: "switch the duplex voice (fast) model \u2014 /voice-model <id>, or alone for a picker",
|
|
8172
8198
|
run: async (a) => {
|
|
8173
8199
|
const apply = (id) => {
|