@livx.cc/agentx 0.99.94 → 0.99.96
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/dist/cli.js +254 -65
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10308,6 +10308,37 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10308
10308
|
}
|
|
10309
10309
|
if (this.silentTurn && res?.finishReason === "error" && refusedCall(res.error)) this.emitHost({ kind: "text_delta", message: fallback ?? _DuplexAgent.FALLBACK_ERROR });
|
|
10310
10310
|
else if (this.silentTurn) await this.ackIfSilent(fallback);
|
|
10311
|
+
else if (this.hollowPromise) await this.followThrough();
|
|
10312
|
+
}
|
|
10313
|
+
/** The turn SAID it is on it ("got it — checking…") but called no tool and nothing is running: a promise nothing
|
|
10314
|
+
* keeps — the user waits on silence (live Para #798: a follow-up while the reflex believed a finished task still ran). */
|
|
10315
|
+
get hollowPromise() {
|
|
10316
|
+
if (!this.options.host || this.turnDispatched || this.leakHit || this.fabricationCut) return false;
|
|
10317
|
+
if (this.voice.transcript.slice(this.turnBase).some((m) => m.tool_calls?.length)) return false;
|
|
10318
|
+
if ([...this.tasks.values()].some((t) => t.status === "running")) return false;
|
|
10319
|
+
return _DuplexAgent.PROMISE_RE.test(this.turnSpoken);
|
|
10320
|
+
}
|
|
10321
|
+
static PROMISE_RE = /\b(?:checking|looking (?:into|it up)|let me (?:check|look|see|find|pull)|i(?:'ll| will) (?:check|look|find|pull)|on it|one (?:moment|sec(?:ond)?))\b/i;
|
|
10322
|
+
static FOLLOW_THROUGH = "[reminder] You told the user you are checking, but no task is running and you called no tool. Keep that promise now: if a finished task result above already answers it, give that answer; otherwise call Act with a clear brief. Do not acknowledge again.";
|
|
10323
|
+
/** Re-prompt the reflex ONCE, tools allowed, to keep its hollow promise; the reminder never stays in the transcript. */
|
|
10324
|
+
async followThrough() {
|
|
10325
|
+
const base = this.voice.transcript.length, reminder = _DuplexAgent.FOLLOW_THROUGH;
|
|
10326
|
+
this.notify("diag", "hollow_promise", { text: this.turnSpoken.trim().slice(0, 80) });
|
|
10327
|
+
this.turnSpoken = "";
|
|
10328
|
+
this.reflexBuf = "";
|
|
10329
|
+
this.reflexForwarded = 0;
|
|
10330
|
+
this.stepMarks = [];
|
|
10331
|
+
this.dropJudge();
|
|
10332
|
+
try {
|
|
10333
|
+
await this.voice.send(reminder);
|
|
10334
|
+
} catch (e) {
|
|
10335
|
+
log21.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
10336
|
+
} finally {
|
|
10337
|
+
const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
|
|
10338
|
+
if (i >= 0) this.voice.transcript.splice(i, 1);
|
|
10339
|
+
await this.drainReflex();
|
|
10340
|
+
this.scrubLeakFromTranscript();
|
|
10341
|
+
}
|
|
10311
10342
|
}
|
|
10312
10343
|
/** Rewrite the UNFORWARDED reflex text: drop complete stage-direction parentheticals (STAGE_DIRECTION_RE) and spoken
|
|
10313
10344
|
* junk (scrubSpokenJunk). Only the unforwarded region is touched — already-spoken audio can't be unsent, and
|
|
@@ -13414,6 +13445,11 @@ function bindSecondChannel(v, sc, live) {
|
|
|
13414
13445
|
sc.onBargeIn();
|
|
13415
13446
|
onBargeIn(phase);
|
|
13416
13447
|
};
|
|
13448
|
+
const speakDelta = v.speakDelta.bind(v);
|
|
13449
|
+
v.speakDelta = ((t, ...rest) => {
|
|
13450
|
+
if (live()) sc.noteReply(t);
|
|
13451
|
+
return speakDelta(t, ...rest);
|
|
13452
|
+
});
|
|
13417
13453
|
Object.assign(sc.options, {
|
|
13418
13454
|
aec: v.usingAec,
|
|
13419
13455
|
speak: (text, kind) => {
|
|
@@ -13626,6 +13662,9 @@ var JsonlDiagSink = class {
|
|
|
13626
13662
|
// src/voice/secondChannel.ts
|
|
13627
13663
|
init_logging();
|
|
13628
13664
|
|
|
13665
|
+
// src/voice/backchannelPlan.ts
|
|
13666
|
+
init_logging();
|
|
13667
|
+
|
|
13629
13668
|
// src/voice/channelLang.ts
|
|
13630
13669
|
init_logging();
|
|
13631
13670
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -13860,8 +13899,79 @@ var ChannelBanks = class {
|
|
|
13860
13899
|
}
|
|
13861
13900
|
};
|
|
13862
13901
|
|
|
13902
|
+
// src/voice/backchannelPlan.ts
|
|
13903
|
+
var log30 = forComponent("BackchannelPlan");
|
|
13904
|
+
var TURN_TYPES = ["story", "list", "complaint", "explanation", "short_command", "question", "other"];
|
|
13905
|
+
var DENSITIES = ["none", "low", "normal", "high"];
|
|
13906
|
+
var silentTurn = (p) => p.density === "none" || p.turn === "short_command" || p.turn === "question";
|
|
13907
|
+
var cueWords = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}'\s]+/gu, " ").split(/\s+/).filter(Boolean);
|
|
13908
|
+
function parsePlan(raw, lang, maxCues = 8) {
|
|
13909
|
+
const m = raw.match(/\{[\s\S]*\}/);
|
|
13910
|
+
if (!m) return null;
|
|
13911
|
+
let o;
|
|
13912
|
+
try {
|
|
13913
|
+
o = JSON.parse(m[0]);
|
|
13914
|
+
} catch {
|
|
13915
|
+
return null;
|
|
13916
|
+
}
|
|
13917
|
+
const turn = String(o?.turn ?? "").toLowerCase().replace(/[\s-]+/g, "_");
|
|
13918
|
+
const density = String(o?.density ?? "").toLowerCase();
|
|
13919
|
+
if (!TURN_TYPES.includes(turn) || !DENSITIES.includes(density)) return null;
|
|
13920
|
+
const cues = [];
|
|
13921
|
+
for (const c of Array.isArray(o.cues) ? o.cues : []) {
|
|
13922
|
+
const when = (Array.isArray(c?.when) ? c.when : [c?.when]).filter((w) => typeof w === "string").map((w) => cueWords(w).join(" ")).filter((w) => w && w.split(" ").length <= 4);
|
|
13923
|
+
const line = typeof c?.line === "string" ? cleanLine(c.line, lang, 4) : "";
|
|
13924
|
+
if (when.length && line) cues.push({ when: [...new Set(when)], line });
|
|
13925
|
+
if (cues.length >= maxCues) break;
|
|
13926
|
+
}
|
|
13927
|
+
return { turn, density, cues };
|
|
13928
|
+
}
|
|
13929
|
+
function matchCue(plan, heard, from = 0) {
|
|
13930
|
+
if (!plan || silentTurn(plan)) return null;
|
|
13931
|
+
const w = cueWords(heard);
|
|
13932
|
+
for (let i = Math.max(0, from); i < w.length; i++) {
|
|
13933
|
+
for (const c of plan.cues) {
|
|
13934
|
+
for (const p of c.when) {
|
|
13935
|
+
const pw = p.split(" ");
|
|
13936
|
+
if (i + pw.length <= w.length && pw.every((x, j) => w[i + j] === x)) return c;
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
return null;
|
|
13941
|
+
}
|
|
13942
|
+
var BackchannelPlannerOptions = class {
|
|
13943
|
+
/** Fast model (Groq gpt-oss / the reflex). */
|
|
13944
|
+
model = "groq/openai/gpt-oss-120b";
|
|
13945
|
+
maxTokens = 900;
|
|
13946
|
+
};
|
|
13947
|
+
var BackchannelPlanner = class {
|
|
13948
|
+
constructor(ai, options) {
|
|
13949
|
+
this.ai = ai;
|
|
13950
|
+
this.options = options;
|
|
13951
|
+
this.options = { ...new BackchannelPlannerOptions(), ...options };
|
|
13952
|
+
}
|
|
13953
|
+
ai;
|
|
13954
|
+
options;
|
|
13955
|
+
async plan(c, signal) {
|
|
13956
|
+
const o = this.options;
|
|
13957
|
+
const name = languageName(c.lang);
|
|
13958
|
+
const prompt = `You plan a live voice assistant's LISTENER sounds ("mm-hm", "right", "oh no") for the user's ${c.heard ? "CURRENT" : "NEXT"} spoken turn.
|
|
13959
|
+
Assistant just said: "${c.agent.slice(-400)}"
|
|
13960
|
+
` + (c.user ? `User's previous turn ended: "${c.user.slice(-200)}"
|
|
13961
|
+
` : "") + (c.heard ? `User so far in this turn: "${c.heard.slice(-300)}"
|
|
13962
|
+
` : "") + `Predict the turn: turn = story | list | complaint | explanation | short_command | question | other; density = none (short request or a question: never interrupt) | low | normal | high (a long story wants frequent cues).
|
|
13963
|
+
cues: 3-8 short words/phrases the user is likely to say at a clause boundary where a listener cue fits (list markers like "first", "and then", "also"; narrative markers; emotional words; names/topics from the context), each with ONE fitting listener line of 1-3 words in ${name} that reacts to it (sympathy for bad news, "okay" for a list step) \u2014 never a question, never an answer.
|
|
13964
|
+
Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"line":"..."}]}. Phrases in the user's language.`;
|
|
13965
|
+
const reasoning = /gpt-oss/.test(o.model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
13966
|
+
const r = await this.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: o.maxTokens, ...reasoning });
|
|
13967
|
+
const p = parsePlan(r.content ?? "", c.lang);
|
|
13968
|
+
if (!p) log30.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
|
|
13969
|
+
return p;
|
|
13970
|
+
}
|
|
13971
|
+
};
|
|
13972
|
+
|
|
13863
13973
|
// src/voice/secondChannel.ts
|
|
13864
|
-
var
|
|
13974
|
+
var log31 = forComponent("SecondChannel");
|
|
13865
13975
|
var SecondChannelOptions = class {
|
|
13866
13976
|
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides. */
|
|
13867
13977
|
decide;
|
|
@@ -13910,6 +14020,23 @@ var SecondChannelOptions = class {
|
|
|
13910
14020
|
fillerPauseMs = 700;
|
|
13911
14021
|
fillerMinWords = 4;
|
|
13912
14022
|
bridgeMinWords = 5;
|
|
14023
|
+
/** ANTICIPATE (backchannelPlan.ts): plan the user's next turn off the hot path (after the agent's reply / at turn start,
|
|
14024
|
+
* revised once at `reviseAtWords`); live, a backchannel fires only on a planned cue, at a `cuePauseMs` breath, with the
|
|
14025
|
+
* cue's line and no decider call. A silent plan (short command / question / density none) vetoes backchannels. No plan
|
|
14026
|
+
* yet / failed / late → the reactive path. Off by default (AGENTX_CHANNEL_ANTICIPATE=1). */
|
|
14027
|
+
anticipate = false;
|
|
14028
|
+
plan;
|
|
14029
|
+
planBudgetMs = 4e3;
|
|
14030
|
+
reviseAtWords = 7;
|
|
14031
|
+
/** A predicted moment needs only a short breath (the decision is already made). */
|
|
14032
|
+
cuePauseMs = 200;
|
|
14033
|
+
/** A cue counts only within the last N words (older matches were gated or passed). */
|
|
14034
|
+
cueWindow = 6;
|
|
14035
|
+
/** true: a plan fires on its cues only. false: a non-silent plan with density normal/high also lets the reactive breath
|
|
14036
|
+
* backchannel through (the plan then only vetoes short commands / questions / low density). */
|
|
14037
|
+
cueOnly = true;
|
|
14038
|
+
onPlan = () => {
|
|
14039
|
+
};
|
|
13913
14040
|
now = () => performance.now();
|
|
13914
14041
|
random = Math.random;
|
|
13915
14042
|
};
|
|
@@ -13918,12 +14045,12 @@ function withBudget(p, ms, what) {
|
|
|
13918
14045
|
let t;
|
|
13919
14046
|
const late = new Promise((r) => {
|
|
13920
14047
|
t = setTimeout(() => {
|
|
13921
|
-
|
|
14048
|
+
log31.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
13922
14049
|
r(null);
|
|
13923
14050
|
}, ms);
|
|
13924
14051
|
});
|
|
13925
14052
|
return Promise.race([p.catch((e) => {
|
|
13926
|
-
|
|
14053
|
+
log31.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
13927
14054
|
return null;
|
|
13928
14055
|
}), late]).finally(() => clearTimeout(t));
|
|
13929
14056
|
}
|
|
@@ -13960,6 +14087,18 @@ var SecondChannel = class {
|
|
|
13960
14087
|
startedAt;
|
|
13961
14088
|
lang = null;
|
|
13962
14089
|
pinned = false;
|
|
14090
|
+
// anticipate: the plan for the user's current/next turn (turnNo bumps per dispatched turn → a late plan is dropped)
|
|
14091
|
+
turnNo = 0;
|
|
14092
|
+
plan = null;
|
|
14093
|
+
planning = false;
|
|
14094
|
+
planTries = 0;
|
|
14095
|
+
// per turn: the reply-end plan + one turn-start retry, never a call per interim
|
|
14096
|
+
revised = false;
|
|
14097
|
+
cueFrom = 0;
|
|
14098
|
+
// word index after the last planned backchannel
|
|
14099
|
+
reply = "";
|
|
14100
|
+
// channel-1's text this turn (the plan's context)
|
|
14101
|
+
lastUser = "";
|
|
13963
14102
|
/** The language lines are spoken in (detected from the user's words, or set by the host). */
|
|
13964
14103
|
get language() {
|
|
13965
14104
|
return this.lang;
|
|
@@ -13971,7 +14110,7 @@ var SecondChannel = class {
|
|
|
13971
14110
|
if (host) this.pinned = true;
|
|
13972
14111
|
const lang = primaryLang(tag);
|
|
13973
14112
|
if (!lang || lang === this.lang) return;
|
|
13974
|
-
|
|
14113
|
+
log31.debug(`language ${this.lang} \u2192 ${lang}`);
|
|
13975
14114
|
this.lang = lang;
|
|
13976
14115
|
this.recent = [];
|
|
13977
14116
|
this.picks.clear();
|
|
@@ -14003,11 +14142,21 @@ var SecondChannel = class {
|
|
|
14003
14142
|
this.pauseTimer = null;
|
|
14004
14143
|
this.onPause(this.o.fillerPauseMs);
|
|
14005
14144
|
}, this.o.fillerPauseMs);
|
|
14145
|
+
let cue = null;
|
|
14146
|
+
if (this.o.anticipate) {
|
|
14147
|
+
const n = words2(t).length;
|
|
14148
|
+
if (!this.plan && !this.planning && this.planTries < 2) this.requestPlan();
|
|
14149
|
+
else if (this.plan && !this.revised && n >= this.o.reviseAtWords) {
|
|
14150
|
+
this.revised = true;
|
|
14151
|
+
this.requestPlan(t);
|
|
14152
|
+
}
|
|
14153
|
+
cue = matchCue(this.plan, t, Math.max(this.cueFrom, n - this.o.cueWindow));
|
|
14154
|
+
}
|
|
14006
14155
|
if (this.bcTimer) clearTimeout(this.bcTimer);
|
|
14007
14156
|
this.bcTimer = this.o.aec ? setTimeout(() => {
|
|
14008
14157
|
this.bcTimer = null;
|
|
14009
|
-
if (this.userSpeaking && this.heard === t) void this.consider("interim", 0, true);
|
|
14010
|
-
}, this.o.backchannelPauseMs) : null;
|
|
14158
|
+
if (this.userSpeaking && this.heard === t) void this.consider("interim", 0, true, cue);
|
|
14159
|
+
}, cue ? this.o.cuePauseMs : this.o.backchannelPauseMs) : null;
|
|
14011
14160
|
if (words2(t).length - this.decidedAt >= this.o.decideEveryWords) void this.consider("interim");
|
|
14012
14161
|
}
|
|
14013
14162
|
/** The user went quiet for `ms` (host VAD/STT pause, or the internal interim timer). */
|
|
@@ -14036,9 +14185,17 @@ var SecondChannel = class {
|
|
|
14036
14185
|
this.heard = "";
|
|
14037
14186
|
this.decidedAt = 0;
|
|
14038
14187
|
this.bridged = false;
|
|
14188
|
+
this.cueFrom = 0;
|
|
14189
|
+
if (this.o.anticipate && this.reply.trim() && !this.plan && !this.planning) this.requestPlan();
|
|
14039
14190
|
return;
|
|
14040
14191
|
}
|
|
14041
14192
|
if (s === "queued") {
|
|
14193
|
+
this.turnNo++;
|
|
14194
|
+
this.plan = null;
|
|
14195
|
+
this.planning = false;
|
|
14196
|
+
this.planTries = 0;
|
|
14197
|
+
this.revised = false;
|
|
14198
|
+
this.reply = "";
|
|
14042
14199
|
this.gen++;
|
|
14043
14200
|
this.userSpeaking = false;
|
|
14044
14201
|
this.bridged = false;
|
|
@@ -14046,24 +14203,31 @@ var SecondChannel = class {
|
|
|
14046
14203
|
if (this[k]) clearTimeout(this[k]);
|
|
14047
14204
|
this[k] = null;
|
|
14048
14205
|
}
|
|
14049
|
-
|
|
14206
|
+
const wait = Math.max(this.o.bridgeDelayMs, this.o.minGapMs - (this.o.now() - this.lastAt));
|
|
14207
|
+
if (wait > 0) this.bridgeTimer = setTimeout(() => {
|
|
14050
14208
|
this.bridgeTimer = null;
|
|
14051
14209
|
void this.consider("working");
|
|
14052
|
-
},
|
|
14210
|
+
}, wait);
|
|
14053
14211
|
else void this.consider("working");
|
|
14054
14212
|
}
|
|
14055
14213
|
}
|
|
14056
14214
|
/** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
|
|
14057
14215
|
onUserTurn(text) {
|
|
14058
14216
|
this.heard = text.trim();
|
|
14217
|
+
this.lastUser = this.heard;
|
|
14059
14218
|
this.detect(this.heard);
|
|
14060
14219
|
this.main = "idle";
|
|
14061
14220
|
this.onAgentState("queued");
|
|
14062
14221
|
}
|
|
14063
14222
|
/** The main answer's streamed text (before its audio): channel-1 is responding — no bridge from here on. */
|
|
14064
14223
|
onMainTokens(text) {
|
|
14224
|
+
this.noteReply(text);
|
|
14065
14225
|
if (text.trim() && (this.main === "queued" || this.main === "thinking")) this.onAgentState("streaming_text");
|
|
14066
14226
|
}
|
|
14227
|
+
/** Channel-1's reply text (a delta) — only the plan's context; no state change (hosts without onMainTokens). */
|
|
14228
|
+
noteReply(text) {
|
|
14229
|
+
this.reply += text;
|
|
14230
|
+
}
|
|
14067
14231
|
/** The user barged in: cut the in-flight line now and drop every pending decision. */
|
|
14068
14232
|
onBargeIn() {
|
|
14069
14233
|
this.gen++;
|
|
@@ -14073,7 +14237,7 @@ var SecondChannel = class {
|
|
|
14073
14237
|
try {
|
|
14074
14238
|
this.o.cancel();
|
|
14075
14239
|
} catch (e) {
|
|
14076
|
-
|
|
14240
|
+
log31.warn(`cancel threw: ${e.message}`);
|
|
14077
14241
|
}
|
|
14078
14242
|
}
|
|
14079
14243
|
}
|
|
@@ -14102,13 +14266,31 @@ var SecondChannel = class {
|
|
|
14102
14266
|
return (this.main === "queued" || this.main === "thinking") && !this.bridged;
|
|
14103
14267
|
}
|
|
14104
14268
|
/** 0ms rule: the candidate line for this trigger, or silence. */
|
|
14105
|
-
rule(trigger, pauseMs = 0, breath = false) {
|
|
14269
|
+
rule(trigger, pauseMs = 0, breath = false, cue = null) {
|
|
14106
14270
|
const w = words2(this.heard);
|
|
14107
14271
|
if (!this.lang) return "silence";
|
|
14108
14272
|
if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return "silence";
|
|
14109
|
-
const k = trigger === "interim" ? this.backchannelMoment(w.length, breath) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
14273
|
+
const k = trigger === "interim" ? this.backchannelMoment(w.length, breath) && (!this.o.anticipate || !this.plan || !silentTurn(this.plan) && (!!cue || !this.o.cueOnly && this.plan.density !== "low")) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
14110
14274
|
return k && this.allowed(k) ? k : "silence";
|
|
14111
14275
|
}
|
|
14276
|
+
/** Plan (or, with `heard`, revise) the backchannels of the user's turn — async, never on the hot path. */
|
|
14277
|
+
requestPlan(heard) {
|
|
14278
|
+
if (!this.o.plan || !this.lang) return;
|
|
14279
|
+
const turn = this.turnNo, t0 = this.o.now(), ac = new AbortController();
|
|
14280
|
+
if (!heard) {
|
|
14281
|
+
this.planning = true;
|
|
14282
|
+
this.planTries++;
|
|
14283
|
+
}
|
|
14284
|
+
const ctx = { agent: this.reply.trim(), user: words2(this.lastUser).slice(-30).join(" "), recent: [...this.recent], lang: this.lang, heard };
|
|
14285
|
+
void withBudget(this.o.plan(ctx, ac.signal), this.o.planBudgetMs, heard ? "plan revision" : "plan").then((p) => {
|
|
14286
|
+
if (p === null) ac.abort();
|
|
14287
|
+
if (turn !== this.turnNo) return;
|
|
14288
|
+
if (!heard) this.planning = false;
|
|
14289
|
+
if (!p) return;
|
|
14290
|
+
this.plan = p;
|
|
14291
|
+
this.o.onPlan(p, !!heard, Math.round(this.o.now() - t0));
|
|
14292
|
+
});
|
|
14293
|
+
}
|
|
14112
14294
|
/** A clause boundary mid-floor: a breath or a comma-like close. Not a question (it wants the answer, not "mm-hm"), and a
|
|
14113
14295
|
* sentence end only deep into a story — a short request's end is the end of their turn (replay: "mm-hm" right after
|
|
14114
14296
|
* "Check hacker news for recent news.", just before the final). */
|
|
@@ -14117,32 +14299,36 @@ var SecondChannel = class {
|
|
|
14117
14299
|
if (/[.!]$/.test(this.heard)) return n >= 2 * this.o.backchannelMinWords;
|
|
14118
14300
|
return breath || /[,;:…]$/.test(this.heard);
|
|
14119
14301
|
}
|
|
14120
|
-
async consider(trigger, pauseMs, breath = false) {
|
|
14302
|
+
async consider(trigger, pauseMs, breath = false, cue = null) {
|
|
14121
14303
|
if (this.busy) return;
|
|
14122
14304
|
const n0 = words2(this.heard).length;
|
|
14123
14305
|
this.decidedAt = n0;
|
|
14124
|
-
|
|
14306
|
+
if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
|
|
14307
|
+
const rule = this.rule(trigger, pauseMs, breath, cue);
|
|
14125
14308
|
if (rule === "silence") return;
|
|
14126
14309
|
this.busy = true;
|
|
14127
14310
|
const gen = this.gen, t0 = this.o.now();
|
|
14128
|
-
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...e });
|
|
14311
|
+
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...cue ? { cue: cue.line } : {}, ...e });
|
|
14129
14312
|
const stale = () => gen !== this.gen || words2(this.heard).length - n0 >= this.o.staleWords;
|
|
14130
14313
|
try {
|
|
14131
14314
|
let kind = rule;
|
|
14132
14315
|
let choice;
|
|
14133
|
-
if (this.o.decide) {
|
|
14316
|
+
if (this.o.decide && !cue) {
|
|
14134
14317
|
const d = await withBudget(this.o.decide(this.tail(), this.state(pauseMs)), this.o.decisionBudgetMs, "channel decision");
|
|
14135
14318
|
choice = d?.choice;
|
|
14136
|
-
if (choice && choice in CHANNEL_ACTIONS) kind = choice;
|
|
14319
|
+
if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
|
|
14320
|
+
else if (choice && choice in CHANNEL_ACTIONS) log31.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
|
|
14137
14321
|
}
|
|
14138
14322
|
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
14139
14323
|
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
14140
14324
|
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
14141
|
-
const text = await this.line(kind);
|
|
14325
|
+
const text = cue && !this.recent.some((r) => norm2(r) === norm2(cue.line)) ? cue.line : await this.line(kind);
|
|
14142
14326
|
if (!text) return ev({ choice, kind, acted: "no_bank" });
|
|
14143
14327
|
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
14144
14328
|
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
14145
|
-
|
|
14329
|
+
const said = this.say(kind, text);
|
|
14330
|
+
if (said && cue) this.cueFrom = words2(this.heard).length;
|
|
14331
|
+
ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
|
|
14146
14332
|
} finally {
|
|
14147
14333
|
this.busy = false;
|
|
14148
14334
|
}
|
|
@@ -14166,7 +14352,7 @@ var SecondChannel = class {
|
|
|
14166
14352
|
if (raw === null) ac.abort();
|
|
14167
14353
|
const text = cleanLine(raw ?? "", this.lang);
|
|
14168
14354
|
if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
|
|
14169
|
-
if (text)
|
|
14355
|
+
if (text) log31.debug(`generated "${text}" repeats a recent line \u2014 canned`);
|
|
14170
14356
|
}
|
|
14171
14357
|
return this.pick(kind);
|
|
14172
14358
|
}
|
|
@@ -14188,7 +14374,7 @@ var SecondChannel = class {
|
|
|
14188
14374
|
try {
|
|
14189
14375
|
r = this.o.speak(text, kind);
|
|
14190
14376
|
} catch (e) {
|
|
14191
|
-
|
|
14377
|
+
log31.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
14192
14378
|
return false;
|
|
14193
14379
|
}
|
|
14194
14380
|
if (r === false) return false;
|
|
@@ -14200,7 +14386,7 @@ var SecondChannel = class {
|
|
|
14200
14386
|
this.o.show(text, kind);
|
|
14201
14387
|
const p = Promise.resolve(r).then(() => {
|
|
14202
14388
|
}, (e) => {
|
|
14203
|
-
|
|
14389
|
+
log31.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
14204
14390
|
}).finally(() => {
|
|
14205
14391
|
if (this.inflight === p) this.inflight = null;
|
|
14206
14392
|
});
|
|
@@ -14372,7 +14558,7 @@ function applyOverlay(cfg, file) {
|
|
|
14372
14558
|
// cli/hooks-config.ts
|
|
14373
14559
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
14374
14560
|
init_platform();
|
|
14375
|
-
var
|
|
14561
|
+
var log32 = forComponent("hooks");
|
|
14376
14562
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
14377
14563
|
function ruleMatches(rule, toolName) {
|
|
14378
14564
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -14391,7 +14577,7 @@ function runCmd(rule, env) {
|
|
|
14391
14577
|
});
|
|
14392
14578
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
14393
14579
|
} catch (e) {
|
|
14394
|
-
|
|
14580
|
+
log32.debug(`hook command failed: ${rule.command}`, e);
|
|
14395
14581
|
return { code: 1, out: String(e?.message ?? e) };
|
|
14396
14582
|
}
|
|
14397
14583
|
}
|
|
@@ -14499,7 +14685,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
|
|
|
14499
14685
|
import { homedir as homedir6 } from "os";
|
|
14500
14686
|
import { join as join11 } from "path";
|
|
14501
14687
|
init_platform();
|
|
14502
|
-
var
|
|
14688
|
+
var log33 = forComponent("session");
|
|
14503
14689
|
var globalDir = () => join11(homedir6(), ".agent", "sessions");
|
|
14504
14690
|
function indexLink(target, link2) {
|
|
14505
14691
|
try {
|
|
@@ -14562,7 +14748,7 @@ var SessionStore = class {
|
|
|
14562
14748
|
}
|
|
14563
14749
|
load(id) {
|
|
14564
14750
|
if (!this.safeId(id)) {
|
|
14565
|
-
|
|
14751
|
+
log33.debug(`rejecting unsafe session id: ${id}`);
|
|
14566
14752
|
return void 0;
|
|
14567
14753
|
}
|
|
14568
14754
|
const path = join11(this.dir, `${id}.json`);
|
|
@@ -14570,7 +14756,7 @@ var SessionStore = class {
|
|
|
14570
14756
|
try {
|
|
14571
14757
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
14572
14758
|
} catch (e) {
|
|
14573
|
-
|
|
14759
|
+
log33.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
14574
14760
|
return void 0;
|
|
14575
14761
|
}
|
|
14576
14762
|
}
|
|
@@ -14583,7 +14769,7 @@ var SessionStore = class {
|
|
|
14583
14769
|
try {
|
|
14584
14770
|
metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
|
|
14585
14771
|
} catch (e) {
|
|
14586
|
-
|
|
14772
|
+
log33.debug(`skipping unreadable session file ${f}`, e);
|
|
14587
14773
|
}
|
|
14588
14774
|
}
|
|
14589
14775
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -14822,7 +15008,7 @@ import { promisify } from "util";
|
|
|
14822
15008
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
14823
15009
|
import { homedir as homedir7 } from "os";
|
|
14824
15010
|
import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
14825
|
-
var
|
|
15011
|
+
var log34 = forComponent("checkpoints");
|
|
14826
15012
|
var exec = promisify(execFile4);
|
|
14827
15013
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
14828
15014
|
function isBroadRoot(p) {
|
|
@@ -14868,7 +15054,7 @@ var ShadowRepo = class {
|
|
|
14868
15054
|
writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
14869
15055
|
this.ready = true;
|
|
14870
15056
|
} catch (e) {
|
|
14871
|
-
|
|
15057
|
+
log34.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
14872
15058
|
this.ready = false;
|
|
14873
15059
|
}
|
|
14874
15060
|
return this.ready;
|
|
@@ -14879,7 +15065,7 @@ var ShadowRepo = class {
|
|
|
14879
15065
|
}
|
|
14880
15066
|
async commit(label, forced = []) {
|
|
14881
15067
|
await this.run("add", "-A");
|
|
14882
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
15068
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-add failed: ${p}`, e));
|
|
14883
15069
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
14884
15070
|
}
|
|
14885
15071
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -14887,8 +15073,8 @@ var ShadowRepo = class {
|
|
|
14887
15073
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
14888
15074
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
14889
15075
|
async amendForced(paths) {
|
|
14890
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
14891
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
15076
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-capture failed: ${p}`, e));
|
|
15077
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log34.debug("amend failed", e));
|
|
14892
15078
|
}
|
|
14893
15079
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
14894
15080
|
async log(ref) {
|
|
@@ -14948,7 +15134,7 @@ var ShadowRepo = class {
|
|
|
14948
15134
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
14949
15135
|
});
|
|
14950
15136
|
} catch (e) {
|
|
14951
|
-
|
|
15137
|
+
log34.debug("checkpoint prune failed", e);
|
|
14952
15138
|
}
|
|
14953
15139
|
}
|
|
14954
15140
|
};
|
|
@@ -15016,7 +15202,7 @@ var GitCheckpoints = class {
|
|
|
15016
15202
|
use(sessionId) {
|
|
15017
15203
|
if (sessionId === this.session) return;
|
|
15018
15204
|
this.session = sessionId;
|
|
15019
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
15205
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log34.debug("re-point failed", e));
|
|
15020
15206
|
}
|
|
15021
15207
|
async begin(label) {
|
|
15022
15208
|
if (!await this.start()) return;
|
|
@@ -15036,7 +15222,7 @@ var GitCheckpoints = class {
|
|
|
15036
15222
|
process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
|
|
15037
15223
|
`);
|
|
15038
15224
|
} else {
|
|
15039
|
-
|
|
15225
|
+
log34.debug("checkpoint commit failed", e);
|
|
15040
15226
|
survivors.push(this.repos[i]);
|
|
15041
15227
|
survivorCaches.push(this.caches[i] ?? []);
|
|
15042
15228
|
}
|
|
@@ -15071,7 +15257,7 @@ var GitCheckpoints = class {
|
|
|
15071
15257
|
if (this.forced.has(abs)) continue;
|
|
15072
15258
|
this.forced.add(abs);
|
|
15073
15259
|
const i = this.repoIndexFor(abs);
|
|
15074
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
15260
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log34.debug("amendForced failed", e));
|
|
15075
15261
|
}
|
|
15076
15262
|
}
|
|
15077
15263
|
};
|
|
@@ -16856,7 +17042,7 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
16856
17042
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
16857
17043
|
import { homedir as homedir8 } from "os";
|
|
16858
17044
|
import { join as join14 } from "path";
|
|
16859
|
-
var
|
|
17045
|
+
var log35 = forComponent("os-sched");
|
|
16860
17046
|
var OsScheduler = class {
|
|
16861
17047
|
options;
|
|
16862
17048
|
constructor(options) {
|
|
@@ -16918,7 +17104,7 @@ var OsScheduler = class {
|
|
|
16918
17104
|
}
|
|
16919
17105
|
}
|
|
16920
17106
|
} catch (e) {
|
|
16921
|
-
|
|
17107
|
+
log35.debug(`cancel ${id}`, e);
|
|
16922
17108
|
}
|
|
16923
17109
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
16924
17110
|
try {
|
|
@@ -17036,7 +17222,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
|
|
|
17036
17222
|
import { homedir as homedir9 } from "os";
|
|
17037
17223
|
import { join as join15 } from "path";
|
|
17038
17224
|
init_platform();
|
|
17039
|
-
var
|
|
17225
|
+
var log36 = forComponent("remote-trigger");
|
|
17040
17226
|
var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
|
|
17041
17227
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
|
|
17042
17228
|
var TriggerServer = class {
|
|
@@ -17074,13 +17260,13 @@ var TriggerServer = class {
|
|
|
17074
17260
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
17075
17261
|
}
|
|
17076
17262
|
});
|
|
17077
|
-
conn.on("error", (e) =>
|
|
17263
|
+
conn.on("error", (e) => log36.debug("trigger conn error", e));
|
|
17078
17264
|
});
|
|
17079
|
-
this.server.on("error", (e) =>
|
|
17265
|
+
this.server.on("error", (e) => log36.debug("trigger server error", e));
|
|
17080
17266
|
this.server.listen(p);
|
|
17081
17267
|
this.path = p;
|
|
17082
17268
|
} catch (e) {
|
|
17083
|
-
|
|
17269
|
+
log36.debug("trigger server unavailable", e);
|
|
17084
17270
|
}
|
|
17085
17271
|
}
|
|
17086
17272
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -17212,7 +17398,7 @@ var strike = C("9");
|
|
|
17212
17398
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
17213
17399
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
17214
17400
|
var err = (s) => process.stderr.write(s);
|
|
17215
|
-
var
|
|
17401
|
+
var log37 = forComponent("cli");
|
|
17216
17402
|
var VERSION = (() => {
|
|
17217
17403
|
try {
|
|
17218
17404
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -18055,7 +18241,7 @@ function turnCost(model, usage) {
|
|
|
18055
18241
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
18056
18242
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
18057
18243
|
}
|
|
18058
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
18244
|
+
async function evaluateGoal(ai, condition, transcript, log38) {
|
|
18059
18245
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
18060
18246
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
18061
18247
|
return text.slice(0, 600);
|
|
@@ -18075,7 +18261,7 @@ ${recent}` }
|
|
|
18075
18261
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
18076
18262
|
if (match) return JSON.parse(match[0]);
|
|
18077
18263
|
} catch (e) {
|
|
18078
|
-
|
|
18264
|
+
log38(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
18079
18265
|
`));
|
|
18080
18266
|
}
|
|
18081
18267
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -18301,7 +18487,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
18301
18487
|
return tools;
|
|
18302
18488
|
}
|
|
18303
18489
|
async function closeMcp(mounted) {
|
|
18304
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
18490
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log37.debug("mcp close failed", e))));
|
|
18305
18491
|
}
|
|
18306
18492
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
18307
18493
|
function mentionRefs(line) {
|
|
@@ -18360,7 +18546,7 @@ async function expandMentions(fs, line) {
|
|
|
18360
18546
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
18361
18547
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
18362
18548
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
18363
|
-
|
|
18549
|
+
log37.debug("mcp mention resolve failed", e);
|
|
18364
18550
|
return null;
|
|
18365
18551
|
});
|
|
18366
18552
|
if (body != null) {
|
|
@@ -18451,7 +18637,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18451
18637
|
try {
|
|
18452
18638
|
store.save(session);
|
|
18453
18639
|
} catch (ex) {
|
|
18454
|
-
|
|
18640
|
+
log37.debug("mid-turn session flush failed", ex);
|
|
18455
18641
|
}
|
|
18456
18642
|
}
|
|
18457
18643
|
return origNotify(e);
|
|
@@ -18615,14 +18801,14 @@ var isCancelTeardown = (e) => {
|
|
|
18615
18801
|
function installCancelGuards(mounted) {
|
|
18616
18802
|
process.on("unhandledRejection", (e) => {
|
|
18617
18803
|
if (isCancelTeardown(e)) {
|
|
18618
|
-
|
|
18804
|
+
log37.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
18619
18805
|
return;
|
|
18620
18806
|
}
|
|
18621
|
-
|
|
18807
|
+
log37.error("unhandledRejection", e);
|
|
18622
18808
|
});
|
|
18623
18809
|
process.on("uncaughtException", (e) => {
|
|
18624
18810
|
if (isCancelTeardown(e)) {
|
|
18625
|
-
|
|
18811
|
+
log37.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
18626
18812
|
return;
|
|
18627
18813
|
}
|
|
18628
18814
|
console.error(e);
|
|
@@ -19049,7 +19235,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19049
19235
|
try {
|
|
19050
19236
|
mkdirSync12(dir, { recursive: true });
|
|
19051
19237
|
} catch (e) {
|
|
19052
|
-
|
|
19238
|
+
log37.debug("paste image dir create failed", e);
|
|
19053
19239
|
}
|
|
19054
19240
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
19055
19241
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -19162,7 +19348,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19162
19348
|
mkdirSync12(join16(cwd, ".agent"), { recursive: true });
|
|
19163
19349
|
appendFileSync(histPath, line + "\n");
|
|
19164
19350
|
} catch (e) {
|
|
19165
|
-
|
|
19351
|
+
log37.debug("history write failed", e);
|
|
19166
19352
|
}
|
|
19167
19353
|
};
|
|
19168
19354
|
const ago = (t) => {
|
|
@@ -19234,7 +19420,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19234
19420
|
try {
|
|
19235
19421
|
store.save(session);
|
|
19236
19422
|
} catch (e) {
|
|
19237
|
-
|
|
19423
|
+
log37.debug("session save after rewind failed", e);
|
|
19238
19424
|
}
|
|
19239
19425
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
19240
19426
|
`));
|
|
@@ -19269,7 +19455,7 @@ ${task}`;
|
|
|
19269
19455
|
bangContext.length = 0;
|
|
19270
19456
|
}
|
|
19271
19457
|
const delta = await refreshCatalogs().catch((e) => {
|
|
19272
|
-
|
|
19458
|
+
log37.debug("catalog refresh failed", e);
|
|
19273
19459
|
return "";
|
|
19274
19460
|
});
|
|
19275
19461
|
if (delta) {
|
|
@@ -19532,7 +19718,7 @@ ${task}`;
|
|
|
19532
19718
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
19533
19719
|
run: async () => {
|
|
19534
19720
|
await refreshCatalogs().catch((e) => {
|
|
19535
|
-
|
|
19721
|
+
log37.debug("catalog refresh failed", e);
|
|
19536
19722
|
});
|
|
19537
19723
|
face.reprepare();
|
|
19538
19724
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -20035,7 +20221,7 @@ ${task}`;
|
|
|
20035
20221
|
try {
|
|
20036
20222
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
20037
20223
|
} catch (e) {
|
|
20038
|
-
|
|
20224
|
+
log37.debug(`loadAgents(${d}) failed`, e);
|
|
20039
20225
|
}
|
|
20040
20226
|
}
|
|
20041
20227
|
if (!seen.size) {
|
|
@@ -20123,7 +20309,7 @@ ${task}`;
|
|
|
20123
20309
|
}
|
|
20124
20310
|
if (idx >= 0) {
|
|
20125
20311
|
const old = mounted.splice(idx, 1)[0];
|
|
20126
|
-
await old.client.close().catch((e) =>
|
|
20312
|
+
await old.client.close().catch((e) => log37.debug("mcp close failed", e));
|
|
20127
20313
|
}
|
|
20128
20314
|
try {
|
|
20129
20315
|
const m = await mountMcpServer(name, conf);
|
|
@@ -20151,7 +20337,7 @@ ${task}`;
|
|
|
20151
20337
|
}
|
|
20152
20338
|
const m = mounted.splice(idx, 1)[0];
|
|
20153
20339
|
remountMcpTools();
|
|
20154
|
-
await m.client.close().catch((e) =>
|
|
20340
|
+
await m.client.close().catch((e) => log37.debug("mcp close failed", e));
|
|
20155
20341
|
err(dim(` removed "${name}"
|
|
20156
20342
|
`));
|
|
20157
20343
|
return;
|
|
@@ -20354,7 +20540,7 @@ ${task}`;
|
|
|
20354
20540
|
try {
|
|
20355
20541
|
return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
20356
20542
|
} catch (e) {
|
|
20357
|
-
|
|
20543
|
+
log37.debug("completion readdir failed", absDir, e);
|
|
20358
20544
|
return null;
|
|
20359
20545
|
}
|
|
20360
20546
|
};
|
|
@@ -20491,7 +20677,7 @@ ${out}
|
|
|
20491
20677
|
return;
|
|
20492
20678
|
}
|
|
20493
20679
|
await refreshCatalogs().catch((e) => {
|
|
20494
|
-
|
|
20680
|
+
log37.debug("catalog refresh failed", e);
|
|
20495
20681
|
});
|
|
20496
20682
|
const sk = skills.find((x) => x.name === name);
|
|
20497
20683
|
if (sk) {
|
|
@@ -20609,7 +20795,7 @@ ${out}
|
|
|
20609
20795
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
20610
20796
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
20611
20797
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
20612
|
-
|
|
20798
|
+
log37.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
20613
20799
|
return void 0;
|
|
20614
20800
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
20615
20801
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -20971,12 +21157,15 @@ function makeSecondChannel(ai, reflexModel, opts) {
|
|
|
20971
21157
|
const decider = leakDecider();
|
|
20972
21158
|
const instinct = decider ? new Instinct({ decider }) : void 0;
|
|
20973
21159
|
const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
|
|
21160
|
+
const planner = new BackchannelPlanner(ai, { model });
|
|
20974
21161
|
return new SecondChannel({
|
|
20975
21162
|
// Banks in the user's language: generated once per language, cached + topped up across sessions (~/.agent/cache).
|
|
20976
21163
|
banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
|
|
20977
21164
|
decide: instinct ? (heard, state) => instinct.channel(heard, state) : void 0,
|
|
20978
21165
|
generate: fastLineGenerator(ai, model),
|
|
20979
|
-
|
|
21166
|
+
// Anticipated backchannels (off until the offline replay proves them — tests/voice-bc-anticipate-eval.ts)
|
|
21167
|
+
...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true, plan: (c, s) => planner.plan(c, s) } : {},
|
|
21168
|
+
onEvent: (e) => log37.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""}${e.cue ? " (planned cue)" : ""} ${e.ms}ms`),
|
|
20980
21169
|
...opts
|
|
20981
21170
|
});
|
|
20982
21171
|
}
|
|
@@ -21038,7 +21227,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21038
21227
|
try {
|
|
21039
21228
|
store.save(session);
|
|
21040
21229
|
} catch (e) {
|
|
21041
|
-
|
|
21230
|
+
log37.warn("duplex session save failed", e);
|
|
21042
21231
|
}
|
|
21043
21232
|
};
|
|
21044
21233
|
out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
|
|
@@ -21076,7 +21265,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21076
21265
|
done2();
|
|
21077
21266
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
21078
21267
|
}).catch((e) => {
|
|
21079
|
-
|
|
21268
|
+
log37.error("duplex turn failed", e);
|
|
21080
21269
|
done2();
|
|
21081
21270
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
21082
21271
|
});
|