agent.libx.js 0.92.1 → 0.92.3
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 +5 -1
- package/dist/{Agent-BzwprwHr.d.ts → Agent-QwBA0wu6.d.ts} +10 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +111 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +16 -6
- package/dist/index.js +108 -2
- package/dist/index.js.map +1 -1
- package/dist/{mcp-Bn5TlRbV.d.ts → mcp-wwgXyhbi.d.ts} +1 -1
- package/dist/mcp.client.d.ts +2 -2
- package/dist/{tools-CeK5AquG.d.ts → tools-GPWp7oXq.d.ts} +4 -0
- package/dist/tools.shell.d.ts +1 -1
- package/dist/tools.shell.js +21 -1
- package/dist/tools.shell.js.map +1 -1
- package/package.json +1 -1
package/cli/cli.ts
CHANGED
|
@@ -373,6 +373,10 @@ function displayHooks(fs?: IFilesystem): Hooks {
|
|
|
373
373
|
err(cyan(`\n ⚙ ${call.name}`) + dim(' ' + summarizeCall(call.name, call.args)) + '\n');
|
|
374
374
|
if (EDIT.has(call.name)) before.set(String(call.args?.path), await read(call.args?.path));
|
|
375
375
|
},
|
|
376
|
+
onToolOutput(_call, chunk) {
|
|
377
|
+
if (!verboseOutput) return; // Ctrl+O verbose: live-tail streaming tool output (default chrome stays calm)
|
|
378
|
+
for (const ln of String(chunk).split('\n')) if (ln.trim()) err(dim(` ⋮ ${ln.length > 200 ? ln.slice(0, 200) + '…' : ln}\n`));
|
|
379
|
+
},
|
|
376
380
|
async postToolUse(call, result) {
|
|
377
381
|
spinner.stop();
|
|
378
382
|
try {
|
|
@@ -1039,7 +1043,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1039
1043
|
workerModel: agent.options.model,
|
|
1040
1044
|
workerOptions,
|
|
1041
1045
|
host,
|
|
1042
|
-
...(args.voice ? { voiceStyle: 'conversational' as const } : {}),
|
|
1046
|
+
...(args.voice ? { voiceStyle: 'conversational' as const, progressUpdates: true } : {}), // voice: narrate throttled worker progress (dead air is worse than a short aside)
|
|
1043
1047
|
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
1044
1048
|
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
1045
1049
|
onTaskStart: async (_id, label) => { await checkpoints.begin(label); },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
2
|
-
import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, e as MessageContent } from './tools-
|
|
2
|
+
import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, e as MessageContent } from './tools-GPWp7oXq.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Hooks — deterministic interception points around tool execution, run by the
|
|
@@ -31,6 +31,9 @@ interface Hooks {
|
|
|
31
31
|
preToolUse?(call: ToolUse, meta?: ToolUseMeta): Promise<PreToolUseDecision | void> | PreToolUseDecision | void;
|
|
32
32
|
/** Observe a tool's result after it ran (audit, metrics, side-channel). */
|
|
33
33
|
postToolUse?(call: ToolUse, result: string, meta?: ToolUseMeta): void | Promise<void>;
|
|
34
|
+
/** Observe a tool's INCREMENTAL output while it runs (only tools that stream, e.g. the real
|
|
35
|
+
* Shell). Fire-and-forget — sync, never awaited, never alters the result. */
|
|
36
|
+
onToolOutput?(call: ToolUse, chunk: string, meta?: ToolUseMeta): void;
|
|
34
37
|
/** Fired once when the agent loop stops cleanly with the model's final text. */
|
|
35
38
|
onStop?(finalText: string): void;
|
|
36
39
|
/** Fired once at session start (a fresh `run()`, or the first `send()`). Return a string to inject
|
|
@@ -59,11 +62,17 @@ declare class RecordingHooks implements Hooks {
|
|
|
59
62
|
result: string;
|
|
60
63
|
meta?: ToolUseMeta;
|
|
61
64
|
}>;
|
|
65
|
+
outputs: Array<{
|
|
66
|
+
call: ToolUse;
|
|
67
|
+
chunk: string;
|
|
68
|
+
meta?: ToolUseMeta;
|
|
69
|
+
}>;
|
|
62
70
|
stops: string[];
|
|
63
71
|
/** tool name -> reason; a matching preToolUse call is blocked with that reason. */
|
|
64
72
|
constructor(blocks?: Record<string, string>);
|
|
65
73
|
preToolUse(call: ToolUse, meta?: ToolUseMeta): PreToolUseDecision | void;
|
|
66
74
|
postToolUse(call: ToolUse, result: string, meta?: ToolUseMeta): void;
|
|
75
|
+
onToolOutput(call: ToolUse, chunk: string, meta?: ToolUseMeta): void;
|
|
67
76
|
onStop(finalText: string): void;
|
|
68
77
|
}
|
|
69
78
|
/** Recording lifecycle hooks for tests: capture session-start/prompt-submit/pre-compact + script transforms. */
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { h as RunResult, R as ReasoningEffort } from './Agent-
|
|
2
|
+
import { h as RunResult, R as ReasoningEffort } from './Agent-QwBA0wu6.js';
|
|
3
3
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
4
|
-
import { M as Message, c as ContentPart } from './tools-
|
|
4
|
+
import { M as Message, c as ContentPart } from './tools-GPWp7oXq.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* On-disk session store for the CLI: each conversation is one JSON file at
|
package/dist/cli.js
CHANGED
|
@@ -1386,6 +1386,18 @@ function makeRealShellTool(options) {
|
|
|
1386
1386
|
timedOut = true;
|
|
1387
1387
|
ctl.abort();
|
|
1388
1388
|
}, timeoutMs);
|
|
1389
|
+
let pend = "";
|
|
1390
|
+
let flushTimer = null;
|
|
1391
|
+
const flushEmit = (ctx2) => {
|
|
1392
|
+
if (flushTimer) {
|
|
1393
|
+
clearTimeout(flushTimer);
|
|
1394
|
+
flushTimer = null;
|
|
1395
|
+
}
|
|
1396
|
+
if (pend) {
|
|
1397
|
+
ctx2.emit?.(redactSecrets(pend));
|
|
1398
|
+
pend = "";
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1389
1401
|
try {
|
|
1390
1402
|
return await new Promise((resolve4) => {
|
|
1391
1403
|
let out = "";
|
|
@@ -1402,7 +1414,13 @@ function makeRealShellTool(options) {
|
|
|
1402
1414
|
return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);
|
|
1403
1415
|
}
|
|
1404
1416
|
const collect = (chunk) => {
|
|
1405
|
-
|
|
1417
|
+
const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
|
|
1418
|
+
out += s;
|
|
1419
|
+
if (ctx.emit && !settled) {
|
|
1420
|
+
pend += s;
|
|
1421
|
+
if (pend.length >= 1024) flushEmit(ctx);
|
|
1422
|
+
else flushTimer ??= setTimeout(() => flushEmit(ctx), 250);
|
|
1423
|
+
}
|
|
1406
1424
|
};
|
|
1407
1425
|
proc.stdout?.on("data", collect);
|
|
1408
1426
|
proc.stderr?.on("data", collect);
|
|
@@ -1412,6 +1430,7 @@ function makeRealShellTool(options) {
|
|
|
1412
1430
|
finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
|
|
1413
1431
|
});
|
|
1414
1432
|
proc.on("close", (code) => {
|
|
1433
|
+
flushEmit(ctx);
|
|
1415
1434
|
if (ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
1416
1435
|
const body = clean(out);
|
|
1417
1436
|
if (code && code !== 0) return finish(`[exit ${code}]${body ? "\n" + body : ""}`);
|
|
@@ -1420,6 +1439,7 @@ function makeRealShellTool(options) {
|
|
|
1420
1439
|
});
|
|
1421
1440
|
} finally {
|
|
1422
1441
|
clearTimeout(timer);
|
|
1442
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
1423
1443
|
ctx.signal?.removeEventListener("abort", onAbort);
|
|
1424
1444
|
}
|
|
1425
1445
|
}
|
|
@@ -2409,6 +2429,9 @@ function composeHooks(...list) {
|
|
|
2409
2429
|
async postToolUse(call, result, meta) {
|
|
2410
2430
|
for (const h of hooks) await h.postToolUse?.(call, result, meta);
|
|
2411
2431
|
},
|
|
2432
|
+
onToolOutput(call, chunk, meta) {
|
|
2433
|
+
for (const h of hooks) h.onToolOutput?.(call, chunk, meta);
|
|
2434
|
+
},
|
|
2412
2435
|
onStop(text) {
|
|
2413
2436
|
for (const h of hooks) h.onStop?.(text);
|
|
2414
2437
|
},
|
|
@@ -2919,6 +2942,13 @@ var Agent = class _Agent {
|
|
|
2919
2942
|
let threw = false;
|
|
2920
2943
|
try {
|
|
2921
2944
|
log3.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
2945
|
+
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
2946
|
+
try {
|
|
2947
|
+
hooks.onToolOutput(call, chunk, meta);
|
|
2948
|
+
} catch (e) {
|
|
2949
|
+
log3.debug(`onToolOutput hook error: ${e}`);
|
|
2950
|
+
}
|
|
2951
|
+
} : void 0;
|
|
2922
2952
|
const raw = await tool.run(args, this.ctx);
|
|
2923
2953
|
if (typeof raw === "string") {
|
|
2924
2954
|
result = raw;
|
|
@@ -2931,6 +2961,8 @@ var Agent = class _Agent {
|
|
|
2931
2961
|
log3.debug(`${tc.function.name} -> error: ${msg}`);
|
|
2932
2962
|
result = `Error: ${msg}`;
|
|
2933
2963
|
threw = true;
|
|
2964
|
+
} finally {
|
|
2965
|
+
this.ctx.emit = void 0;
|
|
2934
2966
|
}
|
|
2935
2967
|
if (!threw) result = await this.maybeAutoTest(tc.function.name, result);
|
|
2936
2968
|
await hooks?.postToolUse?.(call, result, meta);
|
|
@@ -3383,6 +3415,11 @@ function digestRun(messages, maxChars) {
|
|
|
3383
3415
|
import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
|
|
3384
3416
|
init_logging();
|
|
3385
3417
|
var log6 = forComponent("DuplexAgent");
|
|
3418
|
+
function describeCall(call) {
|
|
3419
|
+
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
3420
|
+
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
3421
|
+
return `${call.name}${hint}`;
|
|
3422
|
+
}
|
|
3386
3423
|
var DuplexAgentOptions = class {
|
|
3387
3424
|
/** Any ai.libx.js AIClient — shared by the voice and worker agents (routed by model). */
|
|
3388
3425
|
ai;
|
|
@@ -3403,11 +3440,16 @@ var DuplexAgentOptions = class {
|
|
|
3403
3440
|
/** Awaited BEFORE a delegated worker spawns — open a per-task checkpoint frame, audit, etc.
|
|
3404
3441
|
* (post-spawn would race the worker's first edits). */
|
|
3405
3442
|
onTaskStart;
|
|
3443
|
+
/** Re-voice throttled worker progress asides ('[task t1 progress] …') so long tasks aren't dead
|
|
3444
|
+
* air. Off by default — each update costs a voice turn (LLM call + speech). */
|
|
3445
|
+
progressUpdates = false;
|
|
3446
|
+
/** Min ms between progress re-voices per task. */
|
|
3447
|
+
progressIntervalMs = 25e3;
|
|
3406
3448
|
/** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
|
|
3407
3449
|
* (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
|
|
3408
3450
|
quickLook;
|
|
3409
3451
|
};
|
|
3410
|
-
var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou work in a pair: you talk, and a background worker with FULL access to the user\'s environment (files, shell, web) does the hands-on work. You can find out or do ANYTHING by calling `Delegate` with a clear, self-contained brief \u2014 so NEVER tell the user you can\'t see, access, or do something. Delegate and find out. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), delegate IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nAfter calling Delegate, tell the user you are on it in one short sentence, then end your turn. Do not wait for the result.\nResults arrive later as events like "[task t1 completed] \u2026" or "[task t1 failed] \u2026". When one arrives, summarize it for the ear in one or two short sentences. Never read raw file paths, diffs, or code aloud verbatim.\nDo not fire a second Delegate for work already in flight \u2014 check `TaskStatus` first. Use `CancelTask` when the user asks to stop something.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not delegate, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file \u2014 use `QuickLook` (instant, no task). Anything requiring searching, reasoning, running commands, or editing still goes through `Delegate`.\nNEVER claim to have stored, saved, or remembered something durably \u2014 you cannot. Anything the user wants persisted (their name, preferences, notes) must be Delegated so a worker writes it to memory.\nUser messages may arrive via speech-to-text and can carry transcription artifacts \u2014 odd words, cut-offs, homophones ("for you" vs "folder"). Read for INTENT, not surface text. If a message seems garbled or surprising, briefly confirm what they meant ("did you mean\u2026?") instead of answering the literal words.';
|
|
3452
|
+
var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou work in a pair: you talk, and a background worker with FULL access to the user\'s environment (files, shell, web) does the hands-on work. You can find out or do ANYTHING by calling `Delegate` with a clear, self-contained brief \u2014 so NEVER tell the user you can\'t see, access, or do something. Delegate and find out. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), delegate IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nAfter calling Delegate, tell the user you are on it in one short sentence, then end your turn. Do not wait for the result.\nResults arrive later as events like "[task t1 completed] \u2026" or "[task t1 failed] \u2026". When one arrives, summarize it for the ear in one or two short sentences. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 give at most a half-sentence aside ("still on it \u2014 running tests now") and end your turn. Never present progress as a finished result.\nNever read raw file paths, diffs, or code aloud verbatim.\nDo not fire a second Delegate for work already in flight \u2014 check `TaskStatus` first. Use `CancelTask` when the user asks to stop something.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not delegate, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file \u2014 use `QuickLook` (instant, no task). Anything requiring searching, reasoning, running commands, or editing still goes through `Delegate`.\nNEVER claim to have stored, saved, or remembered something durably \u2014 you cannot. Anything the user wants persisted (their name, preferences, notes) must be Delegated so a worker writes it to memory.\nUser messages may arrive via speech-to-text and can carry transcription artifacts \u2014 odd words, cut-offs, homophones ("for you" vs "folder"). Read for INTENT, not surface text. If a message seems garbled or surprising, briefly confirm what they meant ("did you mean\u2026?") instead of answering the literal words.';
|
|
3411
3453
|
var VOICE_STYLE_CONVERSATIONAL = `Speak like a person in a live conversation, not an assistant reading a script. React first, then deliver: a quick impulsive beat ("oh nice", "hmm, hold on", "ah, got it") before the substance. Use contractions always. Vary sentence length \u2014 some very short. Light fillers and backchannels are fine ("mm-hm", "right", "let's see") but at most one per reply \u2014 never stack them. When you delegate, say it like a human would ("hang on, let me actually dig into that \u2014 gimme a minute") instead of announcing a task. When a result comes back, react to it like you just found out ("okay so \u2014 turns out\u2026"). Match the user's energy: a quick question gets a quick answer \u2014 a few words is a perfectly good turn. Prefer a short answer plus an offer ("want the details?") over covering everything. Never narrate your own mechanics (no "I will now delegate", no task ids out loud).`;
|
|
3412
3454
|
var DuplexAgent = class {
|
|
3413
3455
|
options;
|
|
@@ -3492,18 +3534,78 @@ ${recent}` : brief;
|
|
|
3492
3534
|
spawnWorker(id, label, briefText) {
|
|
3493
3535
|
const o = this.options;
|
|
3494
3536
|
const controller = new AbortController();
|
|
3537
|
+
const base = o.workerOptions?.hooks;
|
|
3538
|
+
const report = o.progressUpdates ? this.progressReporter(id) : void 0;
|
|
3539
|
+
const hooks = report ? {
|
|
3540
|
+
...base,
|
|
3541
|
+
preToolUse: async (call, meta) => {
|
|
3542
|
+
const d = await base?.preToolUse?.(call, meta);
|
|
3543
|
+
report.pre(call);
|
|
3544
|
+
return d;
|
|
3545
|
+
},
|
|
3546
|
+
postToolUse: async (call, result, meta) => {
|
|
3547
|
+
await base?.postToolUse?.(call, result, meta);
|
|
3548
|
+
report.post(call);
|
|
3549
|
+
},
|
|
3550
|
+
onToolOutput: (call, chunk, meta) => {
|
|
3551
|
+
base?.onToolOutput?.(call, chunk, meta);
|
|
3552
|
+
report.output(chunk);
|
|
3553
|
+
}
|
|
3554
|
+
} : base;
|
|
3495
3555
|
const worker = new Agent({
|
|
3496
3556
|
ai: o.ai,
|
|
3497
3557
|
fs: o.fs,
|
|
3498
3558
|
model: o.workerModel,
|
|
3499
3559
|
...o.workerOptions,
|
|
3500
3560
|
// may override ai/fs/model/tools/… —
|
|
3561
|
+
...hooks ? { hooks } : {},
|
|
3501
3562
|
signal: controller.signal
|
|
3502
3563
|
// …but never the per-task cancellation signal
|
|
3503
3564
|
});
|
|
3504
3565
|
const promise = worker.run(briefText).then((res) => this.onWorkerSettled(id, res)).catch((err2) => this.onWorkerFailed(id, err2));
|
|
3505
3566
|
this.tasks.set(id, { id, label, status: "running", controller, promise });
|
|
3506
3567
|
}
|
|
3568
|
+
/** Throttled per-task progress: worker tool calls → at most one progress re-voice per interval.
|
|
3569
|
+
* Two sources, one throttle: completed steps (post) and a heartbeat for a SINGLE long tool call
|
|
3570
|
+
* (pre records the in-flight call; a self-cleaning timer narrates "still inside Bash — 70s").
|
|
3571
|
+
* Completion supersedes: nothing is emitted once the task has settled. */
|
|
3572
|
+
progressReporter(id) {
|
|
3573
|
+
let lastAt = Date.now();
|
|
3574
|
+
let steps = 0;
|
|
3575
|
+
let inflight = null;
|
|
3576
|
+
const due = () => {
|
|
3577
|
+
const rec = this.tasks.get(id);
|
|
3578
|
+
return rec && rec.status === "running" && Date.now() - lastAt >= this.options.progressIntervalMs ? rec : void 0;
|
|
3579
|
+
};
|
|
3580
|
+
const emit = (rec, line, call) => {
|
|
3581
|
+
lastAt = Date.now();
|
|
3582
|
+
this.notify("task_progress", `task ${id} (${rec.label}): ${line}`, { id, steps, call: call.name });
|
|
3583
|
+
this.queueRevoice(`[task ${id} progress] ${line}`);
|
|
3584
|
+
};
|
|
3585
|
+
const timer = setInterval(() => {
|
|
3586
|
+
const rec = this.tasks.get(id);
|
|
3587
|
+
if (!rec || rec.status !== "running") return clearInterval(timer);
|
|
3588
|
+
if (!inflight || !due()) return;
|
|
3589
|
+
const last = inflight.tail.trim().split("\n").filter(Boolean).pop()?.slice(-80);
|
|
3590
|
+
emit(rec, `still inside ${describeCall(inflight.call)} \u2014 ${Math.round((Date.now() - inflight.at) / 1e3)}s on this step${last ? `, last output: ${last}` : ""}`, inflight.call);
|
|
3591
|
+
}, Math.max(this.options.progressIntervalMs, 250));
|
|
3592
|
+
timer.unref?.();
|
|
3593
|
+
return {
|
|
3594
|
+
pre: (call) => {
|
|
3595
|
+
inflight = { call, at: Date.now(), tail: "" };
|
|
3596
|
+
},
|
|
3597
|
+
output: (chunk) => {
|
|
3598
|
+
if (inflight) inflight.tail = (inflight.tail + chunk).slice(-500);
|
|
3599
|
+
},
|
|
3600
|
+
// digest only — NEVER re-voices directly
|
|
3601
|
+
post: (call) => {
|
|
3602
|
+
steps++;
|
|
3603
|
+
inflight = null;
|
|
3604
|
+
const rec = due();
|
|
3605
|
+
if (rec) emit(rec, `still running \u2014 ${steps} steps so far, now: ${describeCall(call)}`, call);
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3507
3609
|
onWorkerSettled(id, res) {
|
|
3508
3610
|
const rec = this.tasks.get(id);
|
|
3509
3611
|
if (res.finishReason === "aborted" || rec.status === "cancelled") {
|
|
@@ -7045,6 +7147,11 @@ function displayHooks(fs) {
|
|
|
7045
7147
|
\u2699 ${call.name}`) + dim(" " + summarizeCall(call.name, call.args)) + "\n");
|
|
7046
7148
|
if (EDIT.has(call.name)) before.set(String(call.args?.path), await read(call.args?.path));
|
|
7047
7149
|
},
|
|
7150
|
+
onToolOutput(_call, chunk) {
|
|
7151
|
+
if (!verboseOutput) return;
|
|
7152
|
+
for (const ln of String(chunk).split("\n")) if (ln.trim()) err(dim(` \u22EE ${ln.length > 200 ? ln.slice(0, 200) + "\u2026" : ln}
|
|
7153
|
+
`));
|
|
7154
|
+
},
|
|
7048
7155
|
async postToolUse(call, result) {
|
|
7049
7156
|
spinner.stop();
|
|
7050
7157
|
try {
|
|
@@ -7654,7 +7761,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7654
7761
|
workerModel: agent.options.model,
|
|
7655
7762
|
workerOptions,
|
|
7656
7763
|
host,
|
|
7657
|
-
...args.voice ? { voiceStyle: "conversational" } : {},
|
|
7764
|
+
...args.voice ? { voiceStyle: "conversational", progressUpdates: true } : {},
|
|
7765
|
+
// voice: narrate throttled worker progress (dead air is worse than a short aside)
|
|
7658
7766
|
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
7659
7767
|
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
7660
7768
|
onTaskStart: async (_id, label) => {
|