agent.libx.js 0.92.7 → 0.92.9
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 +27 -4
- package/dist/cli.js +97 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +19 -0
- package/dist/index.js +74 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/cli/cli.ts
CHANGED
|
@@ -983,9 +983,31 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
983
983
|
let duplexPersist: () => void = () => {}; // bound once the session exists (re-voice fires async)
|
|
984
984
|
let duplexAccount: (data: any) => void = () => {}; // worker cost → session meta (bound below)
|
|
985
985
|
// Workers are non-interactive: a permission 'ask' can't pop a menu mid-conversation (it would fight
|
|
986
|
-
// the line editor for raw stdin).
|
|
987
|
-
//
|
|
986
|
+
// the line editor for raw stdin). VOICE mode relays the ask through the conversation (park →
|
|
987
|
+
// '[task asks]' re-voice → spoken yes/no via AnswerTask; timeout → deny). Text duplex (and the
|
|
988
|
+
// relay's timeout path) auto-denies with a narratable reason — the worker adapts or reports back.
|
|
989
|
+
let permSeq = 0;
|
|
988
990
|
const duplexAsk = async (call: ToolUse): Promise<{ decision: 'allow' | 'deny' }> => {
|
|
991
|
+
if (args.voice && dx) {
|
|
992
|
+
const hint = summarizeCall(call.name, call.args).slice(0, 80);
|
|
993
|
+
// 'menu' mode: approve like a normal session — suspend the editor, pop the picker.
|
|
994
|
+
if ((cfg as any).voiceAskUi === 'menu') {
|
|
995
|
+
editorRef?.suspend();
|
|
996
|
+
const v = await selectMenu(process.stderr, { title: `? background worker asks to run ${call.name} ${hint}`, items: [{ label: 'Allow', value: 'y' }, { label: 'Deny', value: 'n' }], current: 'n' });
|
|
997
|
+
editorRef?.resume();
|
|
998
|
+
editorRef?.redrawNow();
|
|
999
|
+
return { decision: v === 'y' ? 'allow' : 'deny' };
|
|
1000
|
+
}
|
|
1001
|
+
// NB: perm asks are keyed perm-N (PermissionPolicy.ask carries no task identity), so a
|
|
1002
|
+
// cancelled task can't clean its parked perm question — bounded by askTimeoutMs → deny.
|
|
1003
|
+
// (Chrome prints once via the task_ask notify below — no extra line here.)
|
|
1004
|
+
const id = `perm-${++permSeq}`;
|
|
1005
|
+
const a = await dx.parkQuestion(id, `Permission: may the background worker run ${call.name}${hint ? ` (${hint})` : ''}? Answer yes or no (you can also type it).`);
|
|
1006
|
+
const allow = /^\s*(y(es|ep|eah)?|sure|ok(ay)?|allow|go|approved?|do it)\b/i.test(a);
|
|
1007
|
+
err('\r\x1b[0J' + (allow ? green(` ✓ allowed ${call.name}`) : yellow(` ⊘ denied ${call.name}`)) + dim(` (${a.trim() || 'no answer'})\n`));
|
|
1008
|
+
editorRef?.redrawNow();
|
|
1009
|
+
return { decision: allow ? 'allow' : 'deny' };
|
|
1010
|
+
}
|
|
989
1011
|
err('\r\x1b[0J' + yellow(` ⊘ worker asked to run ${call.name} — auto-denied (no interactive approval in duplex; use --yes or --allowedTools)\n`));
|
|
990
1012
|
editorRef?.redrawNow(); // background event at a live prompt — repaint below the notice
|
|
991
1013
|
return { decision: 'deny' };
|
|
@@ -1045,7 +1067,8 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1045
1067
|
// residue glued to 'agentx › '). task_done returned above; everything else falls through.
|
|
1046
1068
|
if (typeof e.kind === 'string' && e.kind.startsWith('task_')) {
|
|
1047
1069
|
spinner.stop();
|
|
1048
|
-
|
|
1070
|
+
// asks are decisions, not chatter — make them stand out (the voice also speaks them)
|
|
1071
|
+
err('\r\x1b[0J' + (e.kind === 'task_ask' ? yellow(` ? ${e.message} — answer by voice or type yes/no\n`) : dim(` · ${e.message}\n`)));
|
|
1049
1072
|
editorRef?.redrawNow();
|
|
1050
1073
|
return;
|
|
1051
1074
|
}
|
|
@@ -1073,7 +1096,7 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
|
|
|
1073
1096
|
workerModel: agent.options.model,
|
|
1074
1097
|
workerOptions,
|
|
1075
1098
|
host,
|
|
1076
|
-
...(args.voice ? { voiceStyle: 'conversational' as const, progressUpdates: true } : {}), // voice:
|
|
1099
|
+
...(args.voice ? { voiceStyle: 'conversational' as const, progressUpdates: true, askRelay: true } : {}), // voice: progress asides + worker questions relayed through the conversation
|
|
1077
1100
|
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
1078
1101
|
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
1079
1102
|
onTaskStart: async (_id, label) => { await checkpoints.begin(label); },
|
package/dist/cli.js
CHANGED
|
@@ -3445,11 +3445,17 @@ var DuplexAgentOptions = class {
|
|
|
3445
3445
|
progressUpdates = false;
|
|
3446
3446
|
/** Min ms between progress re-voices per task. */
|
|
3447
3447
|
progressIntervalMs = 25e3;
|
|
3448
|
+
/** Relay worker questions (AskUserQuestion + permission asks via parkQuestion) through the VOICE:
|
|
3449
|
+
* the question re-voices as '[task <id> asks] …', the user answers conversationally, and the
|
|
3450
|
+
* voice model resolves it with the AnswerTask tool. Off → host.ask passthrough (text menus). */
|
|
3451
|
+
askRelay = false;
|
|
3452
|
+
/** Parked questions auto-resolve empty after this long (callers map '' to deny/best-judgment). */
|
|
3453
|
+
askTimeoutMs = 12e4;
|
|
3448
3454
|
/** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
|
|
3449
3455
|
* (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
|
|
3450
3456
|
quickLook;
|
|
3451
3457
|
};
|
|
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.';
|
|
3458
|
+
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.\n"[task t1 asks] \u2026" events are QUESTIONS from a background task \u2014 relay to the user in your own words, short, then end your turn. When the user answers, call `AnswerTask` with that id and their answer. NEVER answer on the user\'s behalf for permissions or risky operations; if their reply is ambiguous, confirm first.\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.';
|
|
3453
3459
|
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).`;
|
|
3454
3460
|
var DuplexAgent = class {
|
|
3455
3461
|
options;
|
|
@@ -3459,6 +3465,8 @@ var DuplexAgent = class {
|
|
|
3459
3465
|
seq = 0;
|
|
3460
3466
|
pendingEvents = [];
|
|
3461
3467
|
flushQueued = false;
|
|
3468
|
+
/** Parked worker questions awaiting a (voice-relayed) user answer, keyed by ask id. */
|
|
3469
|
+
pendingAsks = /* @__PURE__ */ new Map();
|
|
3462
3470
|
constructor(options) {
|
|
3463
3471
|
this.options = { ...new DuplexAgentOptions(), ...options };
|
|
3464
3472
|
const o = this.options;
|
|
@@ -3481,7 +3489,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`,
|
|
|
3481
3489
|
// no defaultTools() — the voice can only Delegate, never touch files itself. Set AFTER the
|
|
3482
3490
|
// voiceOptions spread (addTools() would be clobbered by the first prepare()); extra voice
|
|
3483
3491
|
// tools come in via voiceOptions.tools and are merged here.
|
|
3484
|
-
tools: [...o.voiceOptions?.tools ?? [], this.delegateTool(), this.taskStatusTool(), this.cancelTaskTool(), this.quickLookTool()]
|
|
3492
|
+
tools: [...o.voiceOptions?.tools ?? [], this.delegateTool(), this.taskStatusTool(), this.cancelTaskTool(), this.quickLookTool(), this.answerTaskTool()]
|
|
3485
3493
|
});
|
|
3486
3494
|
}
|
|
3487
3495
|
/** One user turn: the voice agent streams the reply (and may Delegate). Serialized with re-voice turns. */
|
|
@@ -3552,7 +3560,12 @@ ${recent}` : brief;
|
|
|
3552
3560
|
report.output(chunk);
|
|
3553
3561
|
}
|
|
3554
3562
|
} : base;
|
|
3555
|
-
const
|
|
3563
|
+
const relayAsk = async (q2) => {
|
|
3564
|
+
const opts = q2.options?.length ? ` Options: ${q2.options.map((x) => x.label).join(", ")}.` : "";
|
|
3565
|
+
const a = await this.parkQuestion(id, `${q2.question}${opts}`);
|
|
3566
|
+
return a || "(no answer from the user \u2014 use your best judgment and note the assumption)";
|
|
3567
|
+
};
|
|
3568
|
+
const workerHost = o.askRelay ? { ask: relayAsk } : o.host?.ask ? { ask: (q2) => o.host.ask(q2) } : void 0;
|
|
3556
3569
|
const worker = new Agent({
|
|
3557
3570
|
ai: o.ai,
|
|
3558
3571
|
fs: o.fs,
|
|
@@ -3576,6 +3589,7 @@ ${recent}` : brief;
|
|
|
3576
3589
|
let steps = 0;
|
|
3577
3590
|
let inflight = null;
|
|
3578
3591
|
const due = () => {
|
|
3592
|
+
if (this.pendingAsks.size) return void 0;
|
|
3579
3593
|
const rec = this.tasks.get(id);
|
|
3580
3594
|
return rec && rec.status === "running" && Date.now() - lastAt >= this.options.progressIntervalMs ? rec : void 0;
|
|
3581
3595
|
};
|
|
@@ -3608,7 +3622,35 @@ ${recent}` : brief;
|
|
|
3608
3622
|
}
|
|
3609
3623
|
};
|
|
3610
3624
|
}
|
|
3625
|
+
/** Park a question under `askId` (a task id, or any unique key for permission asks): re-voices
|
|
3626
|
+
* '[task <id> asks] …' and resolves with the user's answer via AnswerTask — or '' on timeout/
|
|
3627
|
+
* task settle (callers map '' to deny / best-judgment). Workers never block forever. */
|
|
3628
|
+
parkQuestion(askId, question) {
|
|
3629
|
+
return new Promise((resolve4) => {
|
|
3630
|
+
let settled = false;
|
|
3631
|
+
const finish = (answer) => {
|
|
3632
|
+
if (settled) return;
|
|
3633
|
+
settled = true;
|
|
3634
|
+
clearTimeout(timer);
|
|
3635
|
+
this.pendingAsks.delete(askId);
|
|
3636
|
+
resolve4(answer);
|
|
3637
|
+
};
|
|
3638
|
+
const timer = setTimeout(() => {
|
|
3639
|
+
this.notify("task_ask_timeout", `task ${askId}: question timed out \u2014 proceeding without an answer`);
|
|
3640
|
+
finish("");
|
|
3641
|
+
}, this.options.askTimeoutMs);
|
|
3642
|
+
this.pendingAsks.set(askId, { question, resolve: finish });
|
|
3643
|
+
this.notify("task_ask", `task ${askId} asks: ${question}`, { id: askId, question });
|
|
3644
|
+
this.queueRevoice(`[task ${askId} asks] ${question}
|
|
3645
|
+
(Relay this to the user in your own words. When they answer, call AnswerTask with id "${askId}" and their answer.)`);
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
/** Resolve any question a settling/cancelled task left parked (its answer can no longer matter). */
|
|
3649
|
+
dropAsk(id) {
|
|
3650
|
+
this.pendingAsks.get(id)?.resolve("");
|
|
3651
|
+
}
|
|
3611
3652
|
onWorkerSettled(id, res) {
|
|
3653
|
+
this.dropAsk(id);
|
|
3612
3654
|
const rec = this.tasks.get(id);
|
|
3613
3655
|
if (res.finishReason === "aborted" || rec.status === "cancelled") {
|
|
3614
3656
|
rec.status = "cancelled";
|
|
@@ -3628,6 +3670,7 @@ ${recent}` : brief;
|
|
|
3628
3670
|
this.failTask(this.tasks.get(id), err2 instanceof Error ? err2.message : String(err2));
|
|
3629
3671
|
}
|
|
3630
3672
|
failTask(rec, msg) {
|
|
3673
|
+
this.dropAsk(rec.id);
|
|
3631
3674
|
rec.status = "error";
|
|
3632
3675
|
log6.warn(`task ${rec.id} failed: ${msg}`);
|
|
3633
3676
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
@@ -3720,6 +3763,23 @@ ${recent}` : brief;
|
|
|
3720
3763
|
}
|
|
3721
3764
|
};
|
|
3722
3765
|
}
|
|
3766
|
+
answerTaskTool() {
|
|
3767
|
+
return {
|
|
3768
|
+
name: "AnswerTask",
|
|
3769
|
+
description: `Relay the user's answer to a pending question from a background task (the "[task <id> asks]" events). Pass the id from the event and the user's answer.`,
|
|
3770
|
+
parameters: {
|
|
3771
|
+
type: "object",
|
|
3772
|
+
required: ["id", "answer"],
|
|
3773
|
+
properties: { id: { type: "string" }, answer: { type: "string", description: "the user's answer, verbatim or faithfully summarized" } }
|
|
3774
|
+
},
|
|
3775
|
+
run: async ({ id, answer }) => {
|
|
3776
|
+
const ask = this.pendingAsks.get(String(id));
|
|
3777
|
+
if (!ask) return `No pending question for '${id}' \u2014 it may have been answered already or timed out.`;
|
|
3778
|
+
ask.resolve(String(answer ?? ""));
|
|
3779
|
+
return `Answer relayed \u2014 task ${id} resumes.`;
|
|
3780
|
+
}
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3723
3783
|
cancelTaskTool() {
|
|
3724
3784
|
return {
|
|
3725
3785
|
name: "CancelTask",
|
|
@@ -3813,7 +3873,7 @@ var VoiceEngineOptions = class {
|
|
|
3813
3873
|
* vocabulary) resume from the precise sample and are dropped. false disables. */
|
|
3814
3874
|
overlapPause = true;
|
|
3815
3875
|
/** sustained overlap ≥ this → cede the turn */
|
|
3816
|
-
overlapSustainMs =
|
|
3876
|
+
overlapSustainMs = 450;
|
|
3817
3877
|
/** quiet for this long while paused → resume, drop the interjection */
|
|
3818
3878
|
overlapResumeMs = 700;
|
|
3819
3879
|
/** energy floor for "overlap candidate" — must sit ABOVE typical room ambient (~110 rms measured;
|
|
@@ -3855,6 +3915,8 @@ var VoiceEngine = class {
|
|
|
3855
3915
|
// loud chunks since pause (sustain must be real sound, not two clicks)
|
|
3856
3916
|
overlapLastLoudAt = 0;
|
|
3857
3917
|
// continuity guard: a gap re-arms the onset (sparse noise ≠ sustained speech)
|
|
3918
|
+
loudTimes = [];
|
|
3919
|
+
// recent loud-chunk timestamps (sliding onset window)
|
|
3858
3920
|
resumeTimer = null;
|
|
3859
3921
|
constructor(options) {
|
|
3860
3922
|
this.options = { ...new VoiceEngineOptions(), ...options };
|
|
@@ -4074,14 +4136,18 @@ var VoiceEngine = class {
|
|
|
4074
4136
|
if (rms < o.overlapRms) return;
|
|
4075
4137
|
const t = now();
|
|
4076
4138
|
if (!this.pausedAt) {
|
|
4139
|
+
this.loudTimes = this.loudTimes.filter((x) => t - x < 400);
|
|
4140
|
+
this.loudTimes.push(t);
|
|
4141
|
+
if (this.loudTimes.length < 2) return;
|
|
4142
|
+
this.loudTimes = [];
|
|
4077
4143
|
this.pausedAt = t;
|
|
4078
|
-
this.overlapLoud =
|
|
4144
|
+
this.overlapLoud = 2;
|
|
4079
4145
|
this.overlapLastLoudAt = t;
|
|
4080
4146
|
this.player.pause();
|
|
4081
4147
|
this.armResume();
|
|
4082
4148
|
return;
|
|
4083
4149
|
}
|
|
4084
|
-
if (t - this.overlapLastLoudAt >
|
|
4150
|
+
if (t - this.overlapLastLoudAt > 450) {
|
|
4085
4151
|
this.pausedAt = t;
|
|
4086
4152
|
this.overlapLoud = 1;
|
|
4087
4153
|
this.overlapLastLoudAt = t;
|
|
@@ -4090,7 +4156,7 @@ var VoiceEngine = class {
|
|
|
4090
4156
|
}
|
|
4091
4157
|
this.overlapLastLoudAt = t;
|
|
4092
4158
|
this.overlapLoud++;
|
|
4093
|
-
if (t - this.pausedAt >= o.overlapSustainMs && this.overlapLoud >=
|
|
4159
|
+
if (t - this.pausedAt >= o.overlapSustainMs && this.overlapLoud >= 3) {
|
|
4094
4160
|
const phase = this.ctxOpen ? "speaking" : "drain";
|
|
4095
4161
|
this.interrupt();
|
|
4096
4162
|
this.options.onBargeIn(phase);
|
|
@@ -4114,6 +4180,7 @@ var VoiceEngine = class {
|
|
|
4114
4180
|
if (this.pausedAt && resume) this.player.resume?.();
|
|
4115
4181
|
this.pausedAt = 0;
|
|
4116
4182
|
this.overlapLoud = 0;
|
|
4183
|
+
this.loudTimes = [];
|
|
4117
4184
|
}
|
|
4118
4185
|
/** energy two-stage barge-in (heuristic tier only): spike over echo baseline → pause + confirm via STT */
|
|
4119
4186
|
handleLevel(rms) {
|
|
@@ -7902,7 +7969,25 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7902
7969
|
};
|
|
7903
7970
|
let duplexAccount = () => {
|
|
7904
7971
|
};
|
|
7972
|
+
let permSeq = 0;
|
|
7905
7973
|
const duplexAsk = async (call) => {
|
|
7974
|
+
if (args.voice && dx) {
|
|
7975
|
+
const hint = summarizeCall(call.name, call.args).slice(0, 80);
|
|
7976
|
+
if (cfg.voiceAskUi === "menu") {
|
|
7977
|
+
editorRef?.suspend();
|
|
7978
|
+
const v = await selectMenu(process.stderr, { title: `? background worker asks to run ${call.name} ${hint}`, items: [{ label: "Allow", value: "y" }, { label: "Deny", value: "n" }], current: "n" });
|
|
7979
|
+
editorRef?.resume();
|
|
7980
|
+
editorRef?.redrawNow();
|
|
7981
|
+
return { decision: v === "y" ? "allow" : "deny" };
|
|
7982
|
+
}
|
|
7983
|
+
const id = `perm-${++permSeq}`;
|
|
7984
|
+
const a = await dx.parkQuestion(id, `Permission: may the background worker run ${call.name}${hint ? ` (${hint})` : ""}? Answer yes or no (you can also type it).`);
|
|
7985
|
+
const allow = /^\s*(y(es|ep|eah)?|sure|ok(ay)?|allow|go|approved?|do it)\b/i.test(a);
|
|
7986
|
+
err("\r\x1B[0J" + (allow ? green(` \u2713 allowed ${call.name}`) : yellow(` \u2298 denied ${call.name}`)) + dim(` (${a.trim() || "no answer"})
|
|
7987
|
+
`));
|
|
7988
|
+
editorRef?.redrawNow();
|
|
7989
|
+
return { decision: allow ? "allow" : "deny" };
|
|
7990
|
+
}
|
|
7906
7991
|
err("\r\x1B[0J" + yellow(` \u2298 worker asked to run ${call.name} \u2014 auto-denied (no interactive approval in duplex; use --yes or --allowedTools)
|
|
7907
7992
|
`));
|
|
7908
7993
|
editorRef?.redrawNow();
|
|
@@ -7948,8 +8033,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7948
8033
|
}
|
|
7949
8034
|
if (typeof e.kind === "string" && e.kind.startsWith("task_")) {
|
|
7950
8035
|
spinner.stop();
|
|
7951
|
-
err("\r\x1B[0J" +
|
|
7952
|
-
`)
|
|
8036
|
+
err("\r\x1B[0J" + (e.kind === "task_ask" ? yellow(` ? ${e.message} \u2014 answer by voice or type yes/no
|
|
8037
|
+
`) : dim(` \xB7 ${e.message}
|
|
8038
|
+
`)));
|
|
7953
8039
|
editorRef?.redrawNow();
|
|
7954
8040
|
return;
|
|
7955
8041
|
}
|
|
@@ -7976,8 +8062,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7976
8062
|
workerModel: agent.options.model,
|
|
7977
8063
|
workerOptions,
|
|
7978
8064
|
host,
|
|
7979
|
-
...args.voice ? { voiceStyle: "conversational", progressUpdates: true } : {},
|
|
7980
|
-
// voice:
|
|
8065
|
+
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true } : {},
|
|
8066
|
+
// voice: progress asides + worker questions relayed through the conversation
|
|
7981
8067
|
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
7982
8068
|
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
7983
8069
|
onTaskStart: async (_id, label) => {
|