@livx.cc/agentx 0.99.84 → 0.99.86
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/README.md +2 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +567 -117
- package/dist/cli.js.map +1 -1
- package/dist/directives.js +2 -12
- package/dist/directives.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +63 -53
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +7 -11
- package/dist/models.js +8 -12
- package/dist/models.js.map +1 -1
- package/dist/tools.shell.js +10 -1
- package/dist/tools.shell.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -709,16 +709,16 @@ ${entries.map((s) => " " + s).join("\n")}`);
|
|
|
709
709
|
return blocks.length ? blocks.join("\n") : `(no ${label} found)`;
|
|
710
710
|
}
|
|
711
711
|
function fuzzyLineReplace(content, oldStr, newStr) {
|
|
712
|
-
const
|
|
712
|
+
const norm3 = (s) => s.trim();
|
|
713
713
|
const cl = content.split("\n");
|
|
714
|
-
const ol = oldStr.split("\n").map(
|
|
714
|
+
const ol = oldStr.split("\n").map(norm3);
|
|
715
715
|
while (ol.length && ol[ol.length - 1] === "") ol.pop();
|
|
716
716
|
while (ol.length && ol[0] === "") ol.shift();
|
|
717
717
|
if (!ol.length) return null;
|
|
718
718
|
const matches = [];
|
|
719
719
|
for (let i2 = 0; i2 + ol.length <= cl.length; i2++) {
|
|
720
720
|
let ok = true;
|
|
721
|
-
for (let j = 0; j < ol.length; j++) if (
|
|
721
|
+
for (let j = 0; j < ol.length; j++) if (norm3(cl[i2 + j]) !== ol[j]) {
|
|
722
722
|
ok = false;
|
|
723
723
|
break;
|
|
724
724
|
}
|
|
@@ -1000,6 +1000,34 @@ var init_todo = __esm({
|
|
|
1000
1000
|
}
|
|
1001
1001
|
});
|
|
1002
1002
|
|
|
1003
|
+
// src/models.ts
|
|
1004
|
+
import { claudeModelAliases } from "ai.libx.js";
|
|
1005
|
+
function latestClaude(family, provider = "anthropic") {
|
|
1006
|
+
const id = MODEL_ALIASES[family];
|
|
1007
|
+
if (!id) throw new Error(`ai.libx.js catalog has no Claude ${family} model`);
|
|
1008
|
+
return `${provider}/${id}`;
|
|
1009
|
+
}
|
|
1010
|
+
function resolveModelAlias(input) {
|
|
1011
|
+
const slash = input.indexOf("/");
|
|
1012
|
+
const prefix = slash === -1 ? "" : input.slice(0, slash + 1);
|
|
1013
|
+
const rest = slash === -1 ? input : input.slice(slash + 1);
|
|
1014
|
+
if (prefix === "claude-code/") return input;
|
|
1015
|
+
return prefix + (MODEL_ALIASES[rest.toLowerCase()] ?? rest);
|
|
1016
|
+
}
|
|
1017
|
+
function modelProvider(model) {
|
|
1018
|
+
const i = model.indexOf("/");
|
|
1019
|
+
if (i !== -1) return model.slice(0, i);
|
|
1020
|
+
if (/^claude-/i.test(model)) return "anthropic";
|
|
1021
|
+
return `?${model}`;
|
|
1022
|
+
}
|
|
1023
|
+
var MODEL_ALIASES;
|
|
1024
|
+
var init_models = __esm({
|
|
1025
|
+
"src/models.ts"() {
|
|
1026
|
+
"use strict";
|
|
1027
|
+
MODEL_ALIASES = { ...claudeModelAliases };
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1003
1031
|
// src/tools.web.ts
|
|
1004
1032
|
function htmlToText(html) {
|
|
1005
1033
|
let s = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<title[\s\S]*?<\/title>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ").replace(/<textarea[\s\S]*?<\/textarea>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ");
|
|
@@ -1221,7 +1249,7 @@ function makeWebSearchTool(options = {}) {
|
|
|
1221
1249
|
if (provider === "anthropic") {
|
|
1222
1250
|
const akey = options.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
1223
1251
|
if (!akey) return "Error: WebSearchAnthropic requires ANTHROPIC_API_KEY (set in env)";
|
|
1224
|
-
return await anthropicSearch(q2, { key: akey, model: options.model ?? "
|
|
1252
|
+
return await anthropicSearch(q2, { key: akey, model: options.model ?? latestClaude("haiku").slice("anthropic/".length), fetch: doFetch, signal: ctl.signal });
|
|
1225
1253
|
}
|
|
1226
1254
|
if (useFirecrawl) {
|
|
1227
1255
|
if (!fcKey) return "Error: Firecrawl provider selected but FIRECRAWL_API_KEY is not set";
|
|
@@ -1263,6 +1291,7 @@ var init_tools_web = __esm({
|
|
|
1263
1291
|
"src/tools.web.ts"() {
|
|
1264
1292
|
"use strict";
|
|
1265
1293
|
init_logging();
|
|
1294
|
+
init_models();
|
|
1266
1295
|
log4 = forComponent("web");
|
|
1267
1296
|
webFetchTool = makeWebFetchTool();
|
|
1268
1297
|
webSearchTool = makeWebSearchTool();
|
|
@@ -3983,36 +4012,10 @@ ${sections.join("\n\n---\n\n")}`;
|
|
|
3983
4012
|
|
|
3984
4013
|
// src/subagent.ts
|
|
3985
4014
|
init_tools();
|
|
4015
|
+
init_models();
|
|
3986
4016
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3987
4017
|
import { join as join4 } from "path";
|
|
3988
4018
|
|
|
3989
|
-
// src/models.ts
|
|
3990
|
-
var MODEL_ALIASES = {
|
|
3991
|
-
fable: "claude-fable-5",
|
|
3992
|
-
"fable-5": "claude-fable-5",
|
|
3993
|
-
opus: "claude-opus-5-5",
|
|
3994
|
-
"opus-5-5": "claude-opus-5-5",
|
|
3995
|
-
"opus-5": "claude-opus-5",
|
|
3996
|
-
"opus-4-8": "claude-opus-4-8",
|
|
3997
|
-
"opus-4-7": "claude-opus-4-7",
|
|
3998
|
-
"opus-4-6": "claude-opus-4-6",
|
|
3999
|
-
sonnet: "claude-sonnet-4-6",
|
|
4000
|
-
haiku: "claude-haiku-4-5-20251001"
|
|
4001
|
-
};
|
|
4002
|
-
function resolveModelAlias(input) {
|
|
4003
|
-
const slash = input.indexOf("/");
|
|
4004
|
-
const prefix = slash === -1 ? "" : input.slice(0, slash + 1);
|
|
4005
|
-
const rest = slash === -1 ? input : input.slice(slash + 1);
|
|
4006
|
-
if (prefix === "claude-code/") return input;
|
|
4007
|
-
return prefix + (MODEL_ALIASES[rest.toLowerCase()] ?? rest);
|
|
4008
|
-
}
|
|
4009
|
-
function modelProvider(model) {
|
|
4010
|
-
const i = model.indexOf("/");
|
|
4011
|
-
if (i !== -1) return model.slice(0, i);
|
|
4012
|
-
if (/^claude-/i.test(model)) return "anthropic";
|
|
4013
|
-
return `?${model}`;
|
|
4014
|
-
}
|
|
4015
|
-
|
|
4016
4019
|
// src/permissions.ts
|
|
4017
4020
|
init_tools_structured();
|
|
4018
4021
|
function commandMatches(glob, cmd) {
|
|
@@ -4500,6 +4503,9 @@ ${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}${extra}`);
|
|
|
4500
4503
|
};
|
|
4501
4504
|
}
|
|
4502
4505
|
|
|
4506
|
+
// src/Agent.ts
|
|
4507
|
+
init_models();
|
|
4508
|
+
|
|
4503
4509
|
// src/agents.ts
|
|
4504
4510
|
function parseAgentFrontmatter(md) {
|
|
4505
4511
|
const m = md.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
@@ -4635,7 +4641,7 @@ var AgentOptions = class {
|
|
|
4635
4641
|
* browser build (which always passes its own fs) never pulls in `node:fs`. Pass `fs` explicitly to
|
|
4636
4642
|
* use any other backend (and to keep full control of the jail). */
|
|
4637
4643
|
fs;
|
|
4638
|
-
model = "
|
|
4644
|
+
model = latestClaude("sonnet");
|
|
4639
4645
|
systemPrompt = "You are a coding agent operating on a virtual filesystem. Use the `bash` tool to explore (ls, grep, find, cat) and the `Read`/`Edit` tools to view and modify files. Always Read a file before Editing it \u2014 and before overwriting it with `Write` or a whole-file `ApplyEdits`: Write replaces the ENTIRE file, so blindly writing over an existing one destroys its unseen contents. If a file already exists, Read it first, then merge your changes (or Edit the specific part). When multiple tool calls are independent (e.g. several Reads or Greps with no dependency between them), issue them together in a single turn rather than one at a time. When the task is complete, reply with a short summary and make no further tool calls.\nRecovery: every turn's file changes are checkpointed. If the user asks to undo/revert/recover edits (including a file you overwrote), tell them to run `/rewind` (or `/undo` for the last turn) to restore the working tree \u2014 checkpoints cover files you edited even in .gitignored dirs. You cannot run `/rewind` yourself; surface it as the recovery path.\nWhen working with a knowledge base or document workspace: use `RepoMap` with scope \"docs\" to see all document headings and summaries (orient first); use `MemorySearch` to find memories by content when you don't know the exact slug; use `Recall` with multiple slugs to batch-load related facts in one call. Prefer targeted loads over reading everything \u2014 orient \u2192 search \u2192 load \u2192 act.";
|
|
4640
4646
|
tools = defaultTools();
|
|
4641
4647
|
/** claude-code/* only: which agentx tools reach it as the in-process `agentx` MCP server (ccBridge.ts). 'memory' =
|
|
@@ -6040,14 +6046,14 @@ function summarize(messages, focus) {
|
|
|
6040
6046
|
if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
|
|
6041
6047
|
if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
|
|
6042
6048
|
if (focus?.trim()) {
|
|
6043
|
-
const
|
|
6049
|
+
const words3 = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
6044
6050
|
const kept = [];
|
|
6045
6051
|
const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
|
|
6046
6052
|
outer: for (const msg of ordered) {
|
|
6047
6053
|
if (msg.role === "tool") continue;
|
|
6048
6054
|
for (const line of contentText(msg.content).split("\n")) {
|
|
6049
6055
|
const l = line.toLowerCase();
|
|
6050
|
-
if (line.trim() &&
|
|
6056
|
+
if (line.trim() && words3.some((w) => l.includes(w))) {
|
|
6051
6057
|
kept.push(line.trim().slice(0, 200));
|
|
6052
6058
|
if (kept.length >= 12) break outer;
|
|
6053
6059
|
}
|
|
@@ -6070,6 +6076,7 @@ function toolKey(name) {
|
|
|
6070
6076
|
}
|
|
6071
6077
|
|
|
6072
6078
|
// src/index.ts
|
|
6079
|
+
init_models();
|
|
6073
6080
|
init_NodeDiskFilesystem();
|
|
6074
6081
|
|
|
6075
6082
|
// src/BodDbFilesystem.ts
|
|
@@ -6778,6 +6785,7 @@ function digestRun(messages, maxChars) {
|
|
|
6778
6785
|
import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
|
|
6779
6786
|
init_llm();
|
|
6780
6787
|
init_logging();
|
|
6788
|
+
init_models();
|
|
6781
6789
|
|
|
6782
6790
|
// node_modules/@bod.ee/voice/src/core/logging.ts
|
|
6783
6791
|
function consoleForComponent(name) {
|
|
@@ -7093,13 +7101,13 @@ function isTranscriptComplete(text) {
|
|
|
7093
7101
|
const trimmed = text.trim();
|
|
7094
7102
|
if (!trimmed) return false;
|
|
7095
7103
|
if (/[.?!]$/.test(trimmed)) return true;
|
|
7096
|
-
const
|
|
7097
|
-
const lastWord =
|
|
7104
|
+
const words3 = trimmed.toLowerCase().split(/\s+/);
|
|
7105
|
+
const lastWord = words3[words3.length - 1];
|
|
7098
7106
|
if (COMPLETE_ENDINGS.has(lastWord)) return true;
|
|
7099
7107
|
if (INCOMPLETE_ENDINGS.has(lastWord)) return false;
|
|
7100
7108
|
const lower = trimmed.toLowerCase();
|
|
7101
7109
|
for (const marker of CONTINUATION_MARKERS) if (lower.endsWith(marker)) return false;
|
|
7102
|
-
if (
|
|
7110
|
+
if (words3.length <= 2) return false;
|
|
7103
7111
|
return true;
|
|
7104
7112
|
}
|
|
7105
7113
|
var MAX_HOLD_MS = 2e3;
|
|
@@ -7500,13 +7508,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7500
7508
|
};
|
|
7501
7509
|
if (this.options.revealMode === "word" || this.options.heardTimestamps) {
|
|
7502
7510
|
this.tts.wantTimestamps = true;
|
|
7503
|
-
this.tts.onTimestamps = (
|
|
7511
|
+
this.tts.onTimestamps = (words3, start) => {
|
|
7504
7512
|
if (this.interrupted) return;
|
|
7505
7513
|
start.forEach((s, i) => {
|
|
7506
|
-
if (this.wordAnchorPending) this.wordPending.push([
|
|
7514
|
+
if (this.wordAnchorPending) this.wordPending.push([words3[i] ?? "", s]);
|
|
7507
7515
|
else {
|
|
7508
7516
|
this.wordStarts.push(this.wordOrigin + s);
|
|
7509
|
-
this.wordTexts.push(
|
|
7517
|
+
this.wordTexts.push(words3[i] ?? "");
|
|
7510
7518
|
}
|
|
7511
7519
|
});
|
|
7512
7520
|
};
|
|
@@ -7654,9 +7662,9 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7654
7662
|
this.ackProbe = null;
|
|
7655
7663
|
this.adaptiveAckFired = false;
|
|
7656
7664
|
const lead = m ? m[1] : bare;
|
|
7657
|
-
const
|
|
7665
|
+
const norm3 = lead.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7658
7666
|
const same = this.ackPickedFired ? bare.match(_VoiceEngine.leadingPhrase(this.lastAck)) : null;
|
|
7659
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7667
|
+
if (_VoiceEngine.ACK_LEADING.test(norm3)) {
|
|
7660
7668
|
const rest = m ? m[2] : "";
|
|
7661
7669
|
this.diag("reflex_ack_suppressed", { lead: lead.trim() });
|
|
7662
7670
|
if (!rest.trim()) return "";
|
|
@@ -7742,8 +7750,8 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7742
7750
|
while (n < this.wordStarts.length && this.wordStarts[n] <= playedSec) n++;
|
|
7743
7751
|
if (n !== this.revealedN) {
|
|
7744
7752
|
this.revealedN = n;
|
|
7745
|
-
const
|
|
7746
|
-
this.options.onReveal(
|
|
7753
|
+
const words3 = this.revealText.trim().split(/\s+/).filter(Boolean);
|
|
7754
|
+
this.options.onReveal(words3.slice(0, n).join(" "));
|
|
7747
7755
|
}
|
|
7748
7756
|
this.revealPoll = this.clock.setTimeout(tick, 60);
|
|
7749
7757
|
};
|
|
@@ -7770,8 +7778,8 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7770
7778
|
this.adaptiveAckFired = false;
|
|
7771
7779
|
this.ackProbe = null;
|
|
7772
7780
|
if (heldProbe) {
|
|
7773
|
-
const
|
|
7774
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7781
|
+
const norm3 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7782
|
+
if (_VoiceEngine.ACK_LEADING.test(norm3)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
|
|
7775
7783
|
else this.speakDelta(heldProbe);
|
|
7776
7784
|
}
|
|
7777
7785
|
if (!this.speaking) return;
|
|
@@ -8309,14 +8317,14 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8309
8317
|
if (!prev) return next;
|
|
8310
8318
|
if (!next) return prev;
|
|
8311
8319
|
const pw = prev.split(/\s+/), nw = next.split(/\s+/);
|
|
8312
|
-
const
|
|
8320
|
+
const norm3 = _VoiceEngine.normWord;
|
|
8313
8321
|
if (pw.length >= 2 && pw.length <= nw.length) {
|
|
8314
8322
|
let pfx = true;
|
|
8315
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8323
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) {
|
|
8316
8324
|
pfx = false;
|
|
8317
8325
|
break;
|
|
8318
8326
|
}
|
|
8319
|
-
if (pfx &&
|
|
8327
|
+
if (pfx && norm3(nw[pw.length - 1]).startsWith(norm3(pw[pw.length - 1]))) return next;
|
|
8320
8328
|
}
|
|
8321
8329
|
if (_VoiceEngine.restates(prev, next)) {
|
|
8322
8330
|
this.diag("merge_restated", { dropped: prev.slice(0, 60) });
|
|
@@ -8325,7 +8333,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8325
8333
|
const max = Math.min(pw.length, nw.length);
|
|
8326
8334
|
for (let k = max; k > 0; k--) {
|
|
8327
8335
|
let match = true;
|
|
8328
|
-
for (let i = 0; i < k; i++) if (
|
|
8336
|
+
for (let i = 0; i < k; i++) if (norm3(pw[pw.length - k + i]) !== norm3(nw[i])) {
|
|
8329
8337
|
match = false;
|
|
8330
8338
|
break;
|
|
8331
8339
|
}
|
|
@@ -8377,11 +8385,11 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8377
8385
|
* race), never worse. Fillers ("mhm") deliberately don't count as replies — see speakFiller. */
|
|
8378
8386
|
refinalizes(next) {
|
|
8379
8387
|
const pw = this.lastDispatchWords, nw = next.trim().split(/\s+/);
|
|
8380
|
-
const
|
|
8388
|
+
const norm3 = _VoiceEngine.normWord;
|
|
8381
8389
|
if (pw.length < 3 || nw.length < pw.length) return false;
|
|
8382
8390
|
if (this.clock.now() - this.lastDispatchAt >= _VoiceEngine.DUP_FINAL_MS) return false;
|
|
8383
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8384
|
-
const pLast =
|
|
8391
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) return false;
|
|
8392
|
+
const pLast = norm3(pw[pw.length - 1]), nSame = norm3(nw[pw.length - 1]);
|
|
8385
8393
|
if (!nSame.startsWith(pLast)) return false;
|
|
8386
8394
|
return nw.length > pw.length || nSame.length > pLast.length;
|
|
8387
8395
|
}
|
|
@@ -9277,6 +9285,17 @@ var ACK_BANK = {
|
|
|
9277
9285
|
steer: ["Ah, okay, switching\u2014", "Ah, got it, so\u2014", "Oh, gotcha, sure\u2014"],
|
|
9278
9286
|
resume: ["Right, so\u2014", "Anyway, so\u2014", "So, as I was saying\u2014"]
|
|
9279
9287
|
};
|
|
9288
|
+
var CHANNEL_ACTIONS = {
|
|
9289
|
+
silence: 'the default \u2014 nothing to add. ALWAYS for a greeting, thanks, a yes/no, or any short complete turn the real reply will answer instantly ("hey", "how are you?", "thanks"); also mid-word or mid-clause',
|
|
9290
|
+
backchannel: 'they are mid-way through a longer story or explanation (several clauses) and just closed a clause: a tiny "mm-hm"/"right" shows you follow, without taking the turn',
|
|
9291
|
+
filler: 'they paused mid-thought, searching for words, and a soft "hmm" keeps the line warm',
|
|
9292
|
+
bridge: 'they finished a substantial request or hard question and the real answer takes a moment: a short "let me check\u2014" lead-in'
|
|
9293
|
+
};
|
|
9294
|
+
var CHANNEL_BANK = {
|
|
9295
|
+
backchannel: ["Mm-hm.", "Right.", "Mm, yeah.", "Uh-huh.", "I see.", "Mm."],
|
|
9296
|
+
filler: ["Hmm\u2026", "Mm\u2026", "Hm, mm\u2026", "Ahh\u2026"],
|
|
9297
|
+
bridge: ["Let me check\u2014", "One sec, looking\u2014", "Hm, let me see\u2014", "Okay, give me a moment\u2014", "Let me think on that\u2014"]
|
|
9298
|
+
};
|
|
9280
9299
|
var INTENT_ACK_OVER = {
|
|
9281
9300
|
stop: Object.keys(ACK_SITUATIONS).filter((s) => s !== "silent"),
|
|
9282
9301
|
steer: ["command"],
|
|
@@ -9494,6 +9513,17 @@ var Instinct = class {
|
|
|
9494
9513
|
if (pick?.text !== text.trim()) return null;
|
|
9495
9514
|
return Promise.race([pick.p.then((p) => p ? pick.spoken : null), pick.spoken]);
|
|
9496
9515
|
}
|
|
9516
|
+
/** Duplex v2 second channel: which (if any) listener line fits THIS instant — one Choice over CHANNEL_ACTIONS.
|
|
9517
|
+
* `heard` is the micro-context tail only (never history). Never rejects: a failed call → null (the rule decides). */
|
|
9518
|
+
channel(heard, state) {
|
|
9519
|
+
return this.ask(
|
|
9520
|
+
"channel",
|
|
9521
|
+
heard,
|
|
9522
|
+
{ heard_so_far: heard, ...state },
|
|
9523
|
+
FRAME + "You are the quick side-voice of the conversation, NOT the one who answers. Pick the tiny sound a person would make right now, or none. Silence is the default: most moments call for nothing.",
|
|
9524
|
+
CHANNEL_ACTIONS
|
|
9525
|
+
);
|
|
9526
|
+
}
|
|
9497
9527
|
/** A random phrase from `pool`, never the one said last (no back-to-back repeat across keys). */
|
|
9498
9528
|
rotate(pool) {
|
|
9499
9529
|
const fresh = pool.filter((x) => x !== this.lastAckSaid);
|
|
@@ -9650,9 +9680,9 @@ var DuplexAgentOptions = class {
|
|
|
9650
9680
|
// mislabels gpt-oss harmony channels under load, leaking raw analysis into the spoken `final` channel
|
|
9651
9681
|
// (and misfiring Hold). 120b is the same price tier (~$0.15/$0.60) — the quality/cost trade is free.
|
|
9652
9682
|
reflexModel = "groq/openai/gpt-oss-120b";
|
|
9653
|
-
actModel = "
|
|
9683
|
+
actModel = latestClaude("sonnet");
|
|
9654
9684
|
/** Premium reasoning model. Set to `false` to disable the Think tier entirely. */
|
|
9655
|
-
thinkModel = "
|
|
9685
|
+
thinkModel = latestClaude("opus");
|
|
9656
9686
|
/** Per-worker providerOptions, derived from the worker's actual model at spawn time (IoC — keeps duplex
|
|
9657
9687
|
* provider-agnostic). Workers override the reflex/main model, so provider-specific options (e.g. cursor's
|
|
9658
9688
|
* cwd/cursorSession) must be recomputed for the worker's model, never inherited from the main template —
|
|
@@ -11230,11 +11260,12 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11230
11260
|
// src/modelRouter.ts
|
|
11231
11261
|
init_llm();
|
|
11232
11262
|
init_logging();
|
|
11263
|
+
init_models();
|
|
11233
11264
|
var log22 = forComponent("ModelRouter");
|
|
11234
11265
|
var ModelRouterOptions = class {
|
|
11235
11266
|
classifier = jevClassifier();
|
|
11236
11267
|
/** Capability tiers → model id (any provider). basic/standard run the main session; premium is only ever an offload. */
|
|
11237
|
-
models = { basic: "
|
|
11268
|
+
models = { basic: latestClaude("haiku"), standard: latestClaude("sonnet"), premium: latestClaude("opus") };
|
|
11238
11269
|
/** Escalate to a stronger tier when it holds at least this much probability mass (upward bias). */
|
|
11239
11270
|
threshold = 0.25;
|
|
11240
11271
|
/** P(same thread) at or above which the previous premium side-conversation is resumed. */
|
|
@@ -12533,7 +12564,7 @@ Reference files in them by their mount path (the left side).`;
|
|
|
12533
12564
|
ai: o.ai,
|
|
12534
12565
|
jobs: jobRegistry,
|
|
12535
12566
|
fs,
|
|
12536
|
-
model: o.model ?? "
|
|
12567
|
+
model: o.model ?? latestClaude("sonnet"),
|
|
12537
12568
|
// PDF reads (disk mode only — VFS paths aren't real files): poppler's pdftotext when installed.
|
|
12538
12569
|
...!virtual ? { pdfText: pdfTextViaPoppler } : {},
|
|
12539
12570
|
// Anchor cursor to the launch dir (its adapter defaults to TMPDIR otherwise) and forward the
|
|
@@ -12584,7 +12615,7 @@ Windows paths: file tools accept ${cwd} style paths and the same path written ${
|
|
|
12584
12615
|
if (o.anthropicWebSearch !== false && o.anthropicKey && !requested.includes("WebSearchAnthropic")) {
|
|
12585
12616
|
tail.push(makeWebSearchAnthropicTool({ anthropicApiKey: o.anthropicKey }));
|
|
12586
12617
|
}
|
|
12587
|
-
if (scratch) tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? "
|
|
12618
|
+
if (scratch) tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? latestClaude("sonnet"), dir: scratchDir }));
|
|
12588
12619
|
tail.push(makeNotifyTool());
|
|
12589
12620
|
if (!realShell.length) return [...base, ...tail];
|
|
12590
12621
|
const filtered = base.filter((t) => t.name !== "bash");
|
|
@@ -13013,8 +13044,8 @@ var CutInAbsorber = class {
|
|
|
13013
13044
|
const cut = this.cut || (meta?.supersedes !== void 0 && meta.supersedes === this.absorbed?.text ? this.absorbed.cut : "");
|
|
13014
13045
|
this.cut = "";
|
|
13015
13046
|
this.absorbed = void 0;
|
|
13016
|
-
const
|
|
13017
|
-
if (!cut ||
|
|
13047
|
+
const words3 = (s) => s.trim().split(/\s+/).filter(Boolean).length;
|
|
13048
|
+
if (!cut || words3(text) - words3(cut) >= 3) return false;
|
|
13018
13049
|
this.absorbed = { text, cut };
|
|
13019
13050
|
return true;
|
|
13020
13051
|
}
|
|
@@ -13073,6 +13104,77 @@ function bindAckInstinct(v, instinct) {
|
|
|
13073
13104
|
onDiag(ev);
|
|
13074
13105
|
};
|
|
13075
13106
|
}
|
|
13107
|
+
function bindSecondChannel(v, sc, live) {
|
|
13108
|
+
const o = v.options;
|
|
13109
|
+
const { onPartial, onUtterance, onState, onBargeIn, onBackchannel } = o;
|
|
13110
|
+
const { show } = sc.options;
|
|
13111
|
+
let own = false;
|
|
13112
|
+
let blip = "";
|
|
13113
|
+
o.onBackchannel = (p) => {
|
|
13114
|
+
blip = p;
|
|
13115
|
+
onBackchannel(p);
|
|
13116
|
+
};
|
|
13117
|
+
const take = v.takeInterruptedReply.bind(v);
|
|
13118
|
+
v.takeInterruptedReply = () => {
|
|
13119
|
+
const r = take();
|
|
13120
|
+
if (!r || !sc.spoke(r.full)) return r;
|
|
13121
|
+
return r.queued.length ? { ...r, full: "", heard: "" } : null;
|
|
13122
|
+
};
|
|
13123
|
+
o.onPartial = (t) => {
|
|
13124
|
+
if (live() && !v.audible) sc.onInterim(t);
|
|
13125
|
+
onPartial(t);
|
|
13126
|
+
};
|
|
13127
|
+
o.onUtterance = ((t, meta) => {
|
|
13128
|
+
if (live() && t.trim()) sc.onUserTurn(t);
|
|
13129
|
+
onUtterance(t, meta);
|
|
13130
|
+
});
|
|
13131
|
+
o.onState = (s) => {
|
|
13132
|
+
if (live() && !own) {
|
|
13133
|
+
if (s === "speaking") sc.onAgentState("first_audio");
|
|
13134
|
+
else if (s === "listening") sc.onAgentState("idle");
|
|
13135
|
+
}
|
|
13136
|
+
onState(s);
|
|
13137
|
+
};
|
|
13138
|
+
o.onBargeIn = (phase) => {
|
|
13139
|
+
sc.onBargeIn();
|
|
13140
|
+
onBargeIn(phase);
|
|
13141
|
+
};
|
|
13142
|
+
Object.assign(sc.options, {
|
|
13143
|
+
aec: v.usingAec,
|
|
13144
|
+
speak: (text, kind) => {
|
|
13145
|
+
if (!live() || v.audible) return false;
|
|
13146
|
+
if (kind === "backchannel") {
|
|
13147
|
+
blip = "";
|
|
13148
|
+
v.fireBackchannel();
|
|
13149
|
+
return;
|
|
13150
|
+
}
|
|
13151
|
+
own = true;
|
|
13152
|
+
v.speakFiller(text);
|
|
13153
|
+
return v.awaitIdle().finally(() => {
|
|
13154
|
+
own = false;
|
|
13155
|
+
});
|
|
13156
|
+
},
|
|
13157
|
+
show: (text, kind) => show(kind === "backchannel" && blip ? blip : text, kind)
|
|
13158
|
+
});
|
|
13159
|
+
}
|
|
13160
|
+
function channelInput(sc, m) {
|
|
13161
|
+
if (m.type === "interim") {
|
|
13162
|
+
if (typeof m.aec === "boolean") sc.options.aec = m.aec;
|
|
13163
|
+
if (typeof m.text === "string") sc.onInterim(m.text);
|
|
13164
|
+
return true;
|
|
13165
|
+
}
|
|
13166
|
+
if (m.type === "pause") {
|
|
13167
|
+
sc.onPause(Number(m.ms) || 0);
|
|
13168
|
+
return true;
|
|
13169
|
+
}
|
|
13170
|
+
if (m.type === "agent_state") {
|
|
13171
|
+
if (typeof m.state === "string" && MAIN_STATES.has(m.state)) sc.onAgentState(m.state);
|
|
13172
|
+
else log27.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
|
|
13173
|
+
return true;
|
|
13174
|
+
}
|
|
13175
|
+
return false;
|
|
13176
|
+
}
|
|
13177
|
+
var MAIN_STATES = /* @__PURE__ */ new Set(["idle", "queued", "thinking", "streaming_text", "first_audio", "speaking"]);
|
|
13076
13178
|
var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
13077
13179
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
13078
13180
|
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
@@ -13242,6 +13344,316 @@ var JsonlDiagSink = class {
|
|
|
13242
13344
|
}
|
|
13243
13345
|
};
|
|
13244
13346
|
|
|
13347
|
+
// src/voice/secondChannel.ts
|
|
13348
|
+
init_logging();
|
|
13349
|
+
var log29 = forComponent("SecondChannel");
|
|
13350
|
+
var SecondChannelOptions = class {
|
|
13351
|
+
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides. */
|
|
13352
|
+
decide;
|
|
13353
|
+
/** Fast-model line writer (see fastLineGenerator). Absent or late → the canned bank. */
|
|
13354
|
+
generate;
|
|
13355
|
+
/** Kinds worth generating (content-conditioned); the rest are canned sounds. */
|
|
13356
|
+
generateFor = ["backchannel", "bridge"];
|
|
13357
|
+
/** Host TTS on the second channel; resolves when the line finished playing. `false` = the host could not play it. */
|
|
13358
|
+
speak = () => {
|
|
13359
|
+
};
|
|
13360
|
+
/** Host UI (caption / muted line) — never history. */
|
|
13361
|
+
show = () => {
|
|
13362
|
+
};
|
|
13363
|
+
/** Stop the in-flight line's audio now (barge-in only). */
|
|
13364
|
+
cancel = () => {
|
|
13365
|
+
};
|
|
13366
|
+
onEvent = () => {
|
|
13367
|
+
};
|
|
13368
|
+
/** Echo cancellation on: backchannels may play while the user speaks. Off (half-duplex) → fillers/bridges in silences only. */
|
|
13369
|
+
aec = false;
|
|
13370
|
+
decideEveryWords = 3;
|
|
13371
|
+
decisionBudgetMs = 300;
|
|
13372
|
+
generateBudgetMs = 500;
|
|
13373
|
+
minGapMs = 2500;
|
|
13374
|
+
/** Backchannel after backchannel needs this gap (v1 Instinct's cadence, ≤12/min). */
|
|
13375
|
+
backchannelGapMs = 5e3;
|
|
13376
|
+
/** A decision whose partial grew by this many words while in flight is stale → dropped. */
|
|
13377
|
+
staleWords = 3;
|
|
13378
|
+
microWords = 30;
|
|
13379
|
+
avoidLines = 4;
|
|
13380
|
+
/** Canned picks exclude the last N picks of the same kind. */
|
|
13381
|
+
bankExclude = 2;
|
|
13382
|
+
backchannelMinWords = 8;
|
|
13383
|
+
/** No new interim for this long → a pause (hosts may also call onPause). */
|
|
13384
|
+
fillerPauseMs = 700;
|
|
13385
|
+
fillerMinWords = 4;
|
|
13386
|
+
bridgeMinWords = 5;
|
|
13387
|
+
now = () => performance.now();
|
|
13388
|
+
random = Math.random;
|
|
13389
|
+
};
|
|
13390
|
+
var words2 = (s) => s.trim() ? s.trim().split(/\s+/) : [];
|
|
13391
|
+
var QUICK = /^(hey|hi|hello|yo|hiya|morning|good (morning|evening|afternoon)|thanks|thank you|ok(ay)?|yes|yeah|yep|no|nope|sure|bye|goodbye|how are you|what'?s up|can you hear me)\b/i;
|
|
13392
|
+
function withBudget(p, ms, what) {
|
|
13393
|
+
let t;
|
|
13394
|
+
const late = new Promise((r) => {
|
|
13395
|
+
t = setTimeout(() => {
|
|
13396
|
+
log29.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
13397
|
+
r(null);
|
|
13398
|
+
}, ms);
|
|
13399
|
+
});
|
|
13400
|
+
return Promise.race([p.catch((e) => {
|
|
13401
|
+
log29.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
13402
|
+
return null;
|
|
13403
|
+
}), late]).finally(() => clearTimeout(t));
|
|
13404
|
+
}
|
|
13405
|
+
var SecondChannel = class {
|
|
13406
|
+
constructor(options) {
|
|
13407
|
+
this.options = options;
|
|
13408
|
+
this.options = { ...new SecondChannelOptions(), ...options };
|
|
13409
|
+
this.startedAt = this.o.now();
|
|
13410
|
+
}
|
|
13411
|
+
options;
|
|
13412
|
+
heard = "";
|
|
13413
|
+
userSpeaking = false;
|
|
13414
|
+
main = "idle";
|
|
13415
|
+
lead = "";
|
|
13416
|
+
gen = 0;
|
|
13417
|
+
// bumped by barge-in / a new user turn → in-flight decisions drop
|
|
13418
|
+
decidedAt = 0;
|
|
13419
|
+
// word count at the last decision
|
|
13420
|
+
busy = false;
|
|
13421
|
+
pauseTimer = null;
|
|
13422
|
+
lastAt = -Infinity;
|
|
13423
|
+
lastKind = null;
|
|
13424
|
+
live = null;
|
|
13425
|
+
// our last line, until channel-1 audio takes the floor
|
|
13426
|
+
cutLine = null;
|
|
13427
|
+
// the line the last barge actually cut (spoke())
|
|
13428
|
+
bridged = false;
|
|
13429
|
+
// one bridge per main turn
|
|
13430
|
+
recent = [];
|
|
13431
|
+
picks = /* @__PURE__ */ new Map();
|
|
13432
|
+
inflight = null;
|
|
13433
|
+
startedAt;
|
|
13434
|
+
get o() {
|
|
13435
|
+
return this.options;
|
|
13436
|
+
}
|
|
13437
|
+
/** The user's live partial for the current turn (the whole partial, not a delta). */
|
|
13438
|
+
onInterim(text) {
|
|
13439
|
+
const t = text.trim();
|
|
13440
|
+
if (!t || t === this.heard) return;
|
|
13441
|
+
if (this.main !== "idle") this.onAgentState("idle");
|
|
13442
|
+
this.heard = t;
|
|
13443
|
+
this.userSpeaking = true;
|
|
13444
|
+
if (this.pauseTimer) clearTimeout(this.pauseTimer);
|
|
13445
|
+
this.pauseTimer = setTimeout(() => {
|
|
13446
|
+
this.pauseTimer = null;
|
|
13447
|
+
this.onPause(this.o.fillerPauseMs);
|
|
13448
|
+
}, this.o.fillerPauseMs);
|
|
13449
|
+
if (words2(t).length - this.decidedAt >= this.o.decideEveryWords) void this.consider("interim");
|
|
13450
|
+
}
|
|
13451
|
+
/** The user went quiet for `ms` (host VAD/STT pause, or the internal interim timer). */
|
|
13452
|
+
onPause(ms) {
|
|
13453
|
+
if (this.pauseTimer) {
|
|
13454
|
+
clearTimeout(this.pauseTimer);
|
|
13455
|
+
this.pauseTimer = null;
|
|
13456
|
+
}
|
|
13457
|
+
this.userSpeaking = false;
|
|
13458
|
+
void this.consider("pause", ms);
|
|
13459
|
+
}
|
|
13460
|
+
/** Channel-1 lifecycle. `queued` = the user's turn was dispatched to the main agent. */
|
|
13461
|
+
onAgentState(s) {
|
|
13462
|
+
if (s === this.main) return;
|
|
13463
|
+
this.main = s;
|
|
13464
|
+
if (s === "first_audio" || s === "speaking") this.live = null;
|
|
13465
|
+
if (s === "idle") {
|
|
13466
|
+
this.heard = "";
|
|
13467
|
+
this.decidedAt = 0;
|
|
13468
|
+
this.lead = "";
|
|
13469
|
+
this.bridged = false;
|
|
13470
|
+
return;
|
|
13471
|
+
}
|
|
13472
|
+
if (s === "queued") {
|
|
13473
|
+
this.gen++;
|
|
13474
|
+
this.userSpeaking = false;
|
|
13475
|
+
this.lead = "";
|
|
13476
|
+
this.bridged = false;
|
|
13477
|
+
if (this.pauseTimer) clearTimeout(this.pauseTimer);
|
|
13478
|
+
this.pauseTimer = null;
|
|
13479
|
+
}
|
|
13480
|
+
if (s === "queued" || s === "thinking" || s === "streaming_text") void this.consider("working");
|
|
13481
|
+
}
|
|
13482
|
+
/** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
|
|
13483
|
+
onUserTurn(text) {
|
|
13484
|
+
this.heard = text.trim();
|
|
13485
|
+
this.main = "idle";
|
|
13486
|
+
this.onAgentState("queued");
|
|
13487
|
+
}
|
|
13488
|
+
/** The main answer's streamed text (before its audio): a bridge can lead into it. */
|
|
13489
|
+
onMainTokens(text) {
|
|
13490
|
+
if (this.lead.length < 120) this.lead += text;
|
|
13491
|
+
if (this.main === "queued" || this.main === "thinking") this.onAgentState("streaming_text");
|
|
13492
|
+
}
|
|
13493
|
+
/** The user barged in: cut the in-flight line now and drop every pending decision. */
|
|
13494
|
+
onBargeIn() {
|
|
13495
|
+
this.gen++;
|
|
13496
|
+
this.cutLine = this.inflight || this.live ? this.live : null;
|
|
13497
|
+
if (this.inflight) {
|
|
13498
|
+
this.inflight = null;
|
|
13499
|
+
try {
|
|
13500
|
+
this.o.cancel();
|
|
13501
|
+
} catch (e) {
|
|
13502
|
+
log29.warn(`cancel threw: ${e.message}`);
|
|
13503
|
+
}
|
|
13504
|
+
}
|
|
13505
|
+
}
|
|
13506
|
+
/** Is `text` the line the last barge-in cut (ours, still holding the floor)? Hosts strip it from the cut reply — it is
|
|
13507
|
+
* not the agent's speech. By identity, not by text: a real reply that merely repeats a recent line is kept. Consumed once. */
|
|
13508
|
+
spoke(text) {
|
|
13509
|
+
const c = this.cutLine;
|
|
13510
|
+
this.cutLine = null;
|
|
13511
|
+
return !!c && c === text.trim();
|
|
13512
|
+
}
|
|
13513
|
+
/** Resolves when the in-flight line finished playing (immediately when none) — hosts enqueue channel-1 audio behind it. */
|
|
13514
|
+
drain() {
|
|
13515
|
+
return this.inflight ?? Promise.resolve();
|
|
13516
|
+
}
|
|
13517
|
+
get audioLocked() {
|
|
13518
|
+
return this.main === "first_audio" || this.main === "speaking";
|
|
13519
|
+
}
|
|
13520
|
+
get working() {
|
|
13521
|
+
return this.main === "queued" || this.main === "thinking" || this.main === "streaming_text";
|
|
13522
|
+
}
|
|
13523
|
+
tail() {
|
|
13524
|
+
return words2(this.heard).slice(-this.o.microWords).join(" ");
|
|
13525
|
+
}
|
|
13526
|
+
/** Hard gates every line passes, whoever proposed it. */
|
|
13527
|
+
allowed(k) {
|
|
13528
|
+
if (this.audioLocked || this.inflight || this.o.now() - this.lastAt < this.o.minGapMs || k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return false;
|
|
13529
|
+
if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
|
|
13530
|
+
if (k === "filler") return !this.userSpeaking && this.main === "idle";
|
|
13531
|
+
return this.working && !this.bridged;
|
|
13532
|
+
}
|
|
13533
|
+
/** 0ms rule: the candidate line for this trigger, or silence. */
|
|
13534
|
+
rule(trigger, pauseMs = 0) {
|
|
13535
|
+
const w = words2(this.heard);
|
|
13536
|
+
if (w.length <= 3 || QUICK.test(this.heard) && w.length <= 6) return "silence";
|
|
13537
|
+
const k = trigger === "interim" ? w.length >= this.o.backchannelMinWords && /[,.;:!?…]$/.test(this.heard) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
13538
|
+
return k && this.allowed(k) ? k : "silence";
|
|
13539
|
+
}
|
|
13540
|
+
async consider(trigger, pauseMs) {
|
|
13541
|
+
if (this.busy) return;
|
|
13542
|
+
const n0 = words2(this.heard).length;
|
|
13543
|
+
this.decidedAt = n0;
|
|
13544
|
+
const rule = this.rule(trigger, pauseMs);
|
|
13545
|
+
if (rule === "silence") return;
|
|
13546
|
+
this.busy = true;
|
|
13547
|
+
const gen = this.gen, t0 = this.o.now();
|
|
13548
|
+
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...e });
|
|
13549
|
+
const stale = () => gen !== this.gen || words2(this.heard).length - n0 >= this.o.staleWords;
|
|
13550
|
+
try {
|
|
13551
|
+
let kind = rule;
|
|
13552
|
+
let choice;
|
|
13553
|
+
if (this.o.decide) {
|
|
13554
|
+
const d = await withBudget(this.o.decide(this.tail(), this.state(pauseMs)), this.o.decisionBudgetMs, "channel decision");
|
|
13555
|
+
choice = d?.choice;
|
|
13556
|
+
if (choice && choice in CHANNEL_ACTIONS) kind = choice;
|
|
13557
|
+
}
|
|
13558
|
+
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
13559
|
+
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
13560
|
+
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
13561
|
+
const text = await this.line(kind);
|
|
13562
|
+
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
13563
|
+
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
13564
|
+
ev({ choice, kind, text, acted: this.say(kind, text) ? "spoken" : "skipped" });
|
|
13565
|
+
} finally {
|
|
13566
|
+
this.busy = false;
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
13569
|
+
state(pauseMs) {
|
|
13570
|
+
const now4 = this.o.now();
|
|
13571
|
+
return {
|
|
13572
|
+
user_speaking: this.userSpeaking,
|
|
13573
|
+
pause_ms: pauseMs ?? null,
|
|
13574
|
+
agent_state: this.main,
|
|
13575
|
+
last_line: this.recent.at(-1) ?? null,
|
|
13576
|
+
ms_since_last_line: Number.isFinite(this.lastAt) ? Math.round(now4 - this.lastAt) : null,
|
|
13577
|
+
call_ms: Math.round(now4 - this.startedAt)
|
|
13578
|
+
};
|
|
13579
|
+
}
|
|
13580
|
+
async line(kind) {
|
|
13581
|
+
if (this.o.generate && this.o.generateFor.includes(kind)) {
|
|
13582
|
+
const ac = new AbortController();
|
|
13583
|
+
const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], ...this.lead ? { lead: this.lead } : {} };
|
|
13584
|
+
const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
|
|
13585
|
+
if (raw === null) ac.abort();
|
|
13586
|
+
const text = cleanLine(raw ?? "");
|
|
13587
|
+
if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
|
|
13588
|
+
if (text) log29.debug(`generated "${text}" repeats a recent line \u2014 canned`);
|
|
13589
|
+
}
|
|
13590
|
+
return this.pick(kind);
|
|
13591
|
+
}
|
|
13592
|
+
/** Canned rotation: never one of the last `bankExclude` picks of this kind, nor a recent line. */
|
|
13593
|
+
pick(kind) {
|
|
13594
|
+
const pool = CHANNEL_BANK[kind];
|
|
13595
|
+
const hist = this.picks.get(kind) ?? [];
|
|
13596
|
+
const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm2));
|
|
13597
|
+
const fresh = pool.filter((x) => !recent.has(norm2(x)));
|
|
13598
|
+
const from = fresh.length ? fresh : pool;
|
|
13599
|
+
const p = from[Math.floor(this.o.random() * from.length)];
|
|
13600
|
+
this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
|
|
13601
|
+
return p;
|
|
13602
|
+
}
|
|
13603
|
+
say(kind, text) {
|
|
13604
|
+
let r;
|
|
13605
|
+
try {
|
|
13606
|
+
r = this.o.speak(text, kind);
|
|
13607
|
+
} catch (e) {
|
|
13608
|
+
log29.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
13609
|
+
return false;
|
|
13610
|
+
}
|
|
13611
|
+
if (r === false) return false;
|
|
13612
|
+
this.lastAt = this.o.now();
|
|
13613
|
+
this.lastKind = kind;
|
|
13614
|
+
if (kind === "bridge") this.bridged = true;
|
|
13615
|
+
this.recent = [...this.recent, text].slice(-this.o.avoidLines);
|
|
13616
|
+
this.live = text.trim();
|
|
13617
|
+
this.o.show(text, kind);
|
|
13618
|
+
const p = Promise.resolve(r).then(() => {
|
|
13619
|
+
}, (e) => {
|
|
13620
|
+
log29.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
13621
|
+
}).finally(() => {
|
|
13622
|
+
if (this.inflight === p) this.inflight = null;
|
|
13623
|
+
});
|
|
13624
|
+
this.inflight = p;
|
|
13625
|
+
return true;
|
|
13626
|
+
}
|
|
13627
|
+
};
|
|
13628
|
+
var norm2 = (s) => s.toLowerCase().replace(/[^a-z' ]/g, "").trim();
|
|
13629
|
+
function cleanLine(s) {
|
|
13630
|
+
const raw = s.trim().split("\n")[0];
|
|
13631
|
+
const l = raw.replace(/^["'“”]+|["'“”]+$/g, "").replace(/^\w+:\s*/, "").trim();
|
|
13632
|
+
if (isLeakSuspect(raw) || isLeakSuspect(l)) {
|
|
13633
|
+
if (l) log29.debug(`generated "${raw}" reads as meta \u2014 canned`);
|
|
13634
|
+
return "";
|
|
13635
|
+
}
|
|
13636
|
+
return words2(l).length > 6 ? "" : l;
|
|
13637
|
+
}
|
|
13638
|
+
var KIND_HINT = {
|
|
13639
|
+
backchannel: "a tiny listener cue while they keep talking \u2014 shows you follow THEIR point, does not take the turn",
|
|
13640
|
+
filler: "a soft thinking sound at their pause",
|
|
13641
|
+
bridge: "a quick lead-in before the real answer, while it is being prepared"
|
|
13642
|
+
};
|
|
13643
|
+
function fastLineGenerator(ai, model) {
|
|
13644
|
+
return async (c, signal) => {
|
|
13645
|
+
const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT[c.kind]}.
|
|
13646
|
+
The user just said: "${c.heard}"
|
|
13647
|
+
The assistant is: ${c.state}.
|
|
13648
|
+
` + (c.lead ? `The real answer begins: "${c.lead.trim()}" \u2014 lead naturally into it; do not repeat it.
|
|
13649
|
+
` : "") + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
|
|
13650
|
+
` : "") + "React to their actual words, casually. Output only the line.";
|
|
13651
|
+
const reasoning = /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
13652
|
+
const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: 150, ...reasoning });
|
|
13653
|
+
return r.content ?? "";
|
|
13654
|
+
};
|
|
13655
|
+
}
|
|
13656
|
+
|
|
13245
13657
|
// cli/config.ts
|
|
13246
13658
|
import { homedir as homedir5 } from "os";
|
|
13247
13659
|
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
|
|
@@ -13388,7 +13800,7 @@ function applyOverlay(cfg, file) {
|
|
|
13388
13800
|
// cli/hooks-config.ts
|
|
13389
13801
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13390
13802
|
init_platform();
|
|
13391
|
-
var
|
|
13803
|
+
var log30 = forComponent("hooks");
|
|
13392
13804
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
13393
13805
|
function ruleMatches(rule, toolName) {
|
|
13394
13806
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -13407,7 +13819,7 @@ function runCmd(rule, env) {
|
|
|
13407
13819
|
});
|
|
13408
13820
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
13409
13821
|
} catch (e) {
|
|
13410
|
-
|
|
13822
|
+
log30.debug(`hook command failed: ${rule.command}`, e);
|
|
13411
13823
|
return { code: 1, out: String(e?.message ?? e) };
|
|
13412
13824
|
}
|
|
13413
13825
|
}
|
|
@@ -13515,7 +13927,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
|
|
|
13515
13927
|
import { homedir as homedir6 } from "os";
|
|
13516
13928
|
import { join as join10 } from "path";
|
|
13517
13929
|
init_platform();
|
|
13518
|
-
var
|
|
13930
|
+
var log31 = forComponent("session");
|
|
13519
13931
|
var globalDir = () => join10(homedir6(), ".agent", "sessions");
|
|
13520
13932
|
function indexLink(target, link2) {
|
|
13521
13933
|
try {
|
|
@@ -13578,7 +13990,7 @@ var SessionStore = class {
|
|
|
13578
13990
|
}
|
|
13579
13991
|
load(id) {
|
|
13580
13992
|
if (!this.safeId(id)) {
|
|
13581
|
-
|
|
13993
|
+
log31.debug(`rejecting unsafe session id: ${id}`);
|
|
13582
13994
|
return void 0;
|
|
13583
13995
|
}
|
|
13584
13996
|
const path = join10(this.dir, `${id}.json`);
|
|
@@ -13586,7 +13998,7 @@ var SessionStore = class {
|
|
|
13586
13998
|
try {
|
|
13587
13999
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
13588
14000
|
} catch (e) {
|
|
13589
|
-
|
|
14001
|
+
log31.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
13590
14002
|
return void 0;
|
|
13591
14003
|
}
|
|
13592
14004
|
}
|
|
@@ -13599,7 +14011,7 @@ var SessionStore = class {
|
|
|
13599
14011
|
try {
|
|
13600
14012
|
metas.push(JSON.parse(readFileSync8(join10(this.dir, f), "utf8")).meta);
|
|
13601
14013
|
} catch (e) {
|
|
13602
|
-
|
|
14014
|
+
log31.debug(`skipping unreadable session file ${f}`, e);
|
|
13603
14015
|
}
|
|
13604
14016
|
}
|
|
13605
14017
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -13838,7 +14250,7 @@ import { promisify } from "util";
|
|
|
13838
14250
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
13839
14251
|
import { homedir as homedir7 } from "os";
|
|
13840
14252
|
import { join as join11, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
13841
|
-
var
|
|
14253
|
+
var log32 = forComponent("checkpoints");
|
|
13842
14254
|
var exec = promisify(execFile4);
|
|
13843
14255
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
13844
14256
|
function isBroadRoot(p) {
|
|
@@ -13884,7 +14296,7 @@ var ShadowRepo = class {
|
|
|
13884
14296
|
writeFileSync6(join11(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
13885
14297
|
this.ready = true;
|
|
13886
14298
|
} catch (e) {
|
|
13887
|
-
|
|
14299
|
+
log32.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
13888
14300
|
this.ready = false;
|
|
13889
14301
|
}
|
|
13890
14302
|
return this.ready;
|
|
@@ -13895,7 +14307,7 @@ var ShadowRepo = class {
|
|
|
13895
14307
|
}
|
|
13896
14308
|
async commit(label, forced = []) {
|
|
13897
14309
|
await this.run("add", "-A");
|
|
13898
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
14310
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log32.debug(`force-add failed: ${p}`, e));
|
|
13899
14311
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
13900
14312
|
}
|
|
13901
14313
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -13903,8 +14315,8 @@ var ShadowRepo = class {
|
|
|
13903
14315
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
13904
14316
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
13905
14317
|
async amendForced(paths) {
|
|
13906
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
13907
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
14318
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log32.debug(`force-capture failed: ${p}`, e));
|
|
14319
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log32.debug("amend failed", e));
|
|
13908
14320
|
}
|
|
13909
14321
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
13910
14322
|
async log(ref) {
|
|
@@ -13964,7 +14376,7 @@ var ShadowRepo = class {
|
|
|
13964
14376
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
13965
14377
|
});
|
|
13966
14378
|
} catch (e) {
|
|
13967
|
-
|
|
14379
|
+
log32.debug("checkpoint prune failed", e);
|
|
13968
14380
|
}
|
|
13969
14381
|
}
|
|
13970
14382
|
};
|
|
@@ -14032,7 +14444,7 @@ var GitCheckpoints = class {
|
|
|
14032
14444
|
use(sessionId) {
|
|
14033
14445
|
if (sessionId === this.session) return;
|
|
14034
14446
|
this.session = sessionId;
|
|
14035
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
14447
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log32.debug("re-point failed", e));
|
|
14036
14448
|
}
|
|
14037
14449
|
async begin(label) {
|
|
14038
14450
|
if (!await this.start()) return;
|
|
@@ -14052,7 +14464,7 @@ var GitCheckpoints = class {
|
|
|
14052
14464
|
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
|
|
14053
14465
|
`);
|
|
14054
14466
|
} else {
|
|
14055
|
-
|
|
14467
|
+
log32.debug("checkpoint commit failed", e);
|
|
14056
14468
|
survivors.push(this.repos[i]);
|
|
14057
14469
|
survivorCaches.push(this.caches[i] ?? []);
|
|
14058
14470
|
}
|
|
@@ -14087,7 +14499,7 @@ var GitCheckpoints = class {
|
|
|
14087
14499
|
if (this.forced.has(abs)) continue;
|
|
14088
14500
|
this.forced.add(abs);
|
|
14089
14501
|
const i = this.repoIndexFor(abs);
|
|
14090
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
14502
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log32.debug("amendForced failed", e));
|
|
14091
14503
|
}
|
|
14092
14504
|
}
|
|
14093
14505
|
};
|
|
@@ -15872,7 +16284,7 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
15872
16284
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
15873
16285
|
import { homedir as homedir8 } from "os";
|
|
15874
16286
|
import { join as join13 } from "path";
|
|
15875
|
-
var
|
|
16287
|
+
var log33 = forComponent("os-sched");
|
|
15876
16288
|
var OsScheduler = class {
|
|
15877
16289
|
options;
|
|
15878
16290
|
constructor(options) {
|
|
@@ -15934,7 +16346,7 @@ var OsScheduler = class {
|
|
|
15934
16346
|
}
|
|
15935
16347
|
}
|
|
15936
16348
|
} catch (e) {
|
|
15937
|
-
|
|
16349
|
+
log33.debug(`cancel ${id}`, e);
|
|
15938
16350
|
}
|
|
15939
16351
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
15940
16352
|
try {
|
|
@@ -16052,7 +16464,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
|
|
|
16052
16464
|
import { homedir as homedir9 } from "os";
|
|
16053
16465
|
import { join as join14 } from "path";
|
|
16054
16466
|
init_platform();
|
|
16055
|
-
var
|
|
16467
|
+
var log34 = forComponent("remote-trigger");
|
|
16056
16468
|
var TRIGGER_DIR = () => join14(homedir9(), ".agent", "triggers");
|
|
16057
16469
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join14(dir, `${sessionId}.sock`);
|
|
16058
16470
|
var TriggerServer = class {
|
|
@@ -16090,13 +16502,13 @@ var TriggerServer = class {
|
|
|
16090
16502
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
16091
16503
|
}
|
|
16092
16504
|
});
|
|
16093
|
-
conn.on("error", (e) =>
|
|
16505
|
+
conn.on("error", (e) => log34.debug("trigger conn error", e));
|
|
16094
16506
|
});
|
|
16095
|
-
this.server.on("error", (e) =>
|
|
16507
|
+
this.server.on("error", (e) => log34.debug("trigger server error", e));
|
|
16096
16508
|
this.server.listen(p);
|
|
16097
16509
|
this.path = p;
|
|
16098
16510
|
} catch (e) {
|
|
16099
|
-
|
|
16511
|
+
log34.debug("trigger server unavailable", e);
|
|
16100
16512
|
}
|
|
16101
16513
|
}
|
|
16102
16514
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -16228,7 +16640,7 @@ var strike = C("9");
|
|
|
16228
16640
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
16229
16641
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
16230
16642
|
var err = (s) => process.stderr.write(s);
|
|
16231
|
-
var
|
|
16643
|
+
var log35 = forComponent("cli");
|
|
16232
16644
|
var VERSION = (() => {
|
|
16233
16645
|
try {
|
|
16234
16646
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -16368,7 +16780,10 @@ function parseArgs(argv) {
|
|
|
16368
16780
|
a.hardenNet = true;
|
|
16369
16781
|
} else if (x === "--subagents") a.subagents = true;
|
|
16370
16782
|
else if (x === "--duplex") a.duplex = true;
|
|
16371
|
-
else if (x === "--
|
|
16783
|
+
else if (x === "--duplex-v2") {
|
|
16784
|
+
a.duplex = true;
|
|
16785
|
+
a.duplexV2 = true;
|
|
16786
|
+
} else if (x === "--conversational" || x === "--convo" || x === "--voice") {
|
|
16372
16787
|
a.voice = true;
|
|
16373
16788
|
a.duplex = true;
|
|
16374
16789
|
} else if (x === "--voice-model") a.voiceModel = val(++i, x);
|
|
@@ -16425,7 +16840,7 @@ var HELP = `agentx \u2014 agent runtime CLI
|
|
|
16425
16840
|
agentx -c "<task>" continue the most recent session
|
|
16426
16841
|
|
|
16427
16842
|
Flags:
|
|
16428
|
-
-m, --model <id> model (default auto: per-turn haiku/sonnet + opus offload when Jev is configured, else anthropic
|
|
16843
|
+
-m, --model <id> model (default auto: per-turn haiku/sonnet + opus offload when Jev is configured, else the newest anthropic Sonnet); bare -m or -m list = list all models
|
|
16429
16844
|
-C, --cwd <dir> working dir / project root (default .)
|
|
16430
16845
|
-c, --continue resume the most recent session in this dir
|
|
16431
16846
|
-r, --resume [id] resume a session \u2014 with an id, or bare for an interactive picker
|
|
@@ -16452,12 +16867,14 @@ Flags:
|
|
|
16452
16867
|
--append-system-prompt <t> extra instructions appended to the system prompt for this run
|
|
16453
16868
|
--duplex duplex mode: a fast voice model replies instantly and delegates real work
|
|
16454
16869
|
to a background worker agent (-m model); results are re-voiced when ready
|
|
16870
|
+
--duplex-v2 duplex + a second voice channel: backchannels, fillers and bridges that never
|
|
16871
|
+
enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
|
|
16455
16872
|
--conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
|
|
16456
16873
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
16457
16874
|
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
|
|
16458
16875
|
spoken replies out (echo-cancelled; speak over it to interrupt)
|
|
16459
16876
|
--voice-model <id> with --duplex: the fast voice model (default groq/openai/gpt-oss-120b)
|
|
16460
|
-
--think-model <id> with --duplex: the premium deep-reasoning model (default anthropic
|
|
16877
|
+
--think-model <id> with --duplex: the premium deep-reasoning model (default: the newest anthropic Opus)
|
|
16461
16878
|
--no-think with --duplex: disable the Think tier (Act handles everything)
|
|
16462
16879
|
--voice-system-prompt <text> with --duplex: appended to the voice (reflex) prompt only (--append-system-prompt \u2192 workers)
|
|
16463
16880
|
--add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
|
|
@@ -16508,8 +16925,8 @@ REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2
|
|
|
16508
16925
|
REPL steer/stash: type while a turn is running \u2192 Enter STEERS it (the text reaches the model at its next step, alongside the next tool result). Alt+S (or Ctrl+S) with text stashes it for AFTER the turn instead; on an empty prompt pops the next stashed entry for editing.
|
|
16509
16926
|
REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Ctrl-_ undo \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
|
|
16510
16927
|
REPL paste: large/multi-line pastes collapse to a [Pasted #N "\u2026" +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; Ctrl-V or /paste grabs a clipboard image (macOS).`;
|
|
16511
|
-
function newestModel() {
|
|
16512
|
-
return listModels().slice().sort((a, b) => (getModelInfo(b)?.releaseDate ?? "").localeCompare(getModelInfo(a)?.releaseDate ?? ""))[0] ?? "";
|
|
16928
|
+
function newestModel(provider) {
|
|
16929
|
+
return listModels(provider).slice().sort((a, b) => (getModelInfo(b)?.releaseDate ?? "").localeCompare(getModelInfo(a)?.releaseDate ?? ""))[0] ?? "";
|
|
16513
16930
|
}
|
|
16514
16931
|
function printModels() {
|
|
16515
16932
|
const byProvider = /* @__PURE__ */ new Map();
|
|
@@ -16533,7 +16950,8 @@ function resolveModelOrNewest(model) {
|
|
|
16533
16950
|
const want = model ?? new AgentOptions().model;
|
|
16534
16951
|
const resolved = resolveModel(want);
|
|
16535
16952
|
if (isModelSupported(resolved)) return resolved;
|
|
16536
|
-
const
|
|
16953
|
+
const provider = getProviderFromModel(want) ?? void 0;
|
|
16954
|
+
const fallback = newestModel(provider && listModels(provider).length ? provider : void 0);
|
|
16537
16955
|
if (!fallback) return want;
|
|
16538
16956
|
err(yellow(` \u26A0 model "${want}" not available \u2014 using ${fallback}
|
|
16539
16957
|
`));
|
|
@@ -16603,7 +17021,7 @@ function duplexModels(o) {
|
|
|
16603
17021
|
let reflexModel = asked ? resolveModelOrNewest(asked) : void 0;
|
|
16604
17022
|
const reflex = reflexModel ?? dflt.reflexModel;
|
|
16605
17023
|
if (!modelUsable(reflex, o.apiKeys)) {
|
|
16606
|
-
const to = anthropic ? "
|
|
17024
|
+
const to = anthropic ? latestClaude("haiku") : cc ? "claude-code/haiku" : o.actModel.startsWith("cursor/") ? void 0 : o.actModel;
|
|
16607
17025
|
if (to) reflexModel = to;
|
|
16608
17026
|
notes.push(`voice model ${reflex} can't run here (${why(reflex)})${to ? ` \u2014 using ${to}` : ""}`);
|
|
16609
17027
|
}
|
|
@@ -17053,14 +17471,14 @@ function turnCost(model, usage) {
|
|
|
17053
17471
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
17054
17472
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
17055
17473
|
}
|
|
17056
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
17474
|
+
async function evaluateGoal(ai, condition, transcript, log36) {
|
|
17057
17475
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
17058
17476
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
17059
17477
|
return text.slice(0, 600);
|
|
17060
17478
|
}).join("\n---\n");
|
|
17061
17479
|
try {
|
|
17062
17480
|
const r = await ai.chat({
|
|
17063
|
-
model: "
|
|
17481
|
+
model: latestClaude("haiku"),
|
|
17064
17482
|
stream: false,
|
|
17065
17483
|
messages: [
|
|
17066
17484
|
{ role: "system", content: 'You judge whether a goal condition has been met based on conversation transcript. Respond ONLY with JSON: {"met": boolean, "reason": "one sentence"}. Be strict \u2014 only met:true if there is clear evidence in the transcript.' },
|
|
@@ -17073,7 +17491,7 @@ ${recent}` }
|
|
|
17073
17491
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
17074
17492
|
if (match) return JSON.parse(match[0]);
|
|
17075
17493
|
} catch (e) {
|
|
17076
|
-
|
|
17494
|
+
log36(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
17077
17495
|
`));
|
|
17078
17496
|
}
|
|
17079
17497
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -17299,7 +17717,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
17299
17717
|
return tools;
|
|
17300
17718
|
}
|
|
17301
17719
|
async function closeMcp(mounted) {
|
|
17302
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
17720
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log35.debug("mcp close failed", e))));
|
|
17303
17721
|
}
|
|
17304
17722
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
17305
17723
|
function mentionRefs(line) {
|
|
@@ -17358,7 +17776,7 @@ async function expandMentions(fs, line) {
|
|
|
17358
17776
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
17359
17777
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
17360
17778
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
17361
|
-
|
|
17779
|
+
log35.debug("mcp mention resolve failed", e);
|
|
17362
17780
|
return null;
|
|
17363
17781
|
});
|
|
17364
17782
|
if (body != null) {
|
|
@@ -17449,7 +17867,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
17449
17867
|
try {
|
|
17450
17868
|
store.save(session);
|
|
17451
17869
|
} catch (ex) {
|
|
17452
|
-
|
|
17870
|
+
log35.debug("mid-turn session flush failed", ex);
|
|
17453
17871
|
}
|
|
17454
17872
|
}
|
|
17455
17873
|
return origNotify(e);
|
|
@@ -17598,14 +18016,14 @@ var isCancelTeardown = (e) => {
|
|
|
17598
18016
|
function installCancelGuards(mounted) {
|
|
17599
18017
|
process.on("unhandledRejection", (e) => {
|
|
17600
18018
|
if (isCancelTeardown(e)) {
|
|
17601
|
-
|
|
18019
|
+
log35.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
17602
18020
|
return;
|
|
17603
18021
|
}
|
|
17604
|
-
|
|
18022
|
+
log35.error("unhandledRejection", e);
|
|
17605
18023
|
});
|
|
17606
18024
|
process.on("uncaughtException", (e) => {
|
|
17607
18025
|
if (isCancelTeardown(e)) {
|
|
17608
|
-
|
|
18026
|
+
log35.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
17609
18027
|
return;
|
|
17610
18028
|
}
|
|
17611
18029
|
console.error(e);
|
|
@@ -18000,7 +18418,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
18000
18418
|
err(green(" \u2713 think tier disabled\n"));
|
|
18001
18419
|
return;
|
|
18002
18420
|
}
|
|
18003
|
-
const picked2 = await pickModel(dx.options.thinkModel === false ? "
|
|
18421
|
+
const picked2 = await pickModel(dx.options.thinkModel === false ? latestClaude("opus") : String(dx.options.thinkModel));
|
|
18004
18422
|
if (picked2) applyThinkModel(picked2);
|
|
18005
18423
|
return;
|
|
18006
18424
|
}
|
|
@@ -18030,7 +18448,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
18030
18448
|
try {
|
|
18031
18449
|
mkdirSync12(dir, { recursive: true });
|
|
18032
18450
|
} catch (e) {
|
|
18033
|
-
|
|
18451
|
+
log35.debug("paste image dir create failed", e);
|
|
18034
18452
|
}
|
|
18035
18453
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
18036
18454
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -18143,7 +18561,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
18143
18561
|
mkdirSync12(join15(cwd, ".agent"), { recursive: true });
|
|
18144
18562
|
appendFileSync(histPath, line + "\n");
|
|
18145
18563
|
} catch (e) {
|
|
18146
|
-
|
|
18564
|
+
log35.debug("history write failed", e);
|
|
18147
18565
|
}
|
|
18148
18566
|
};
|
|
18149
18567
|
const ago = (t) => {
|
|
@@ -18215,7 +18633,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
18215
18633
|
try {
|
|
18216
18634
|
store.save(session);
|
|
18217
18635
|
} catch (e) {
|
|
18218
|
-
|
|
18636
|
+
log35.debug("session save after rewind failed", e);
|
|
18219
18637
|
}
|
|
18220
18638
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
18221
18639
|
`));
|
|
@@ -18250,7 +18668,7 @@ ${task}`;
|
|
|
18250
18668
|
bangContext.length = 0;
|
|
18251
18669
|
}
|
|
18252
18670
|
const delta = await refreshCatalogs().catch((e) => {
|
|
18253
|
-
|
|
18671
|
+
log35.debug("catalog refresh failed", e);
|
|
18254
18672
|
return "";
|
|
18255
18673
|
});
|
|
18256
18674
|
if (delta) {
|
|
@@ -18513,7 +18931,7 @@ ${task}`;
|
|
|
18513
18931
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
18514
18932
|
run: async () => {
|
|
18515
18933
|
await refreshCatalogs().catch((e) => {
|
|
18516
|
-
|
|
18934
|
+
log35.debug("catalog refresh failed", e);
|
|
18517
18935
|
});
|
|
18518
18936
|
face.reprepare();
|
|
18519
18937
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -18641,7 +19059,7 @@ ${task}`;
|
|
|
18641
19059
|
return;
|
|
18642
19060
|
}
|
|
18643
19061
|
const current = dx.options.thinkModel === false ? void 0 : dx.options.thinkModel;
|
|
18644
|
-
const picked = await pickModel(current ?? "
|
|
19062
|
+
const picked = await pickModel(current ?? latestClaude("opus"));
|
|
18645
19063
|
if (picked) applyThinkModel(picked);
|
|
18646
19064
|
else err(dim(` think ${dx.options.thinkModel === false ? "off" : dx.options.thinkModel}
|
|
18647
19065
|
`));
|
|
@@ -18997,7 +19415,7 @@ ${task}`;
|
|
|
18997
19415
|
await fs2.writeFile(p, [
|
|
18998
19416
|
"---",
|
|
18999
19417
|
`description: What "${name}" is for \u2014 the Task tool picks agents by this line`,
|
|
19000
|
-
"# model: anthropic/
|
|
19418
|
+
"# model: anthropic/haiku # optional per-agent model override",
|
|
19001
19419
|
"# tools: Read, Grep, Glob # optional tool allowlist (omit = all tools)",
|
|
19002
19420
|
"---",
|
|
19003
19421
|
"",
|
|
@@ -19016,7 +19434,7 @@ ${task}`;
|
|
|
19016
19434
|
try {
|
|
19017
19435
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
19018
19436
|
} catch (e) {
|
|
19019
|
-
|
|
19437
|
+
log35.debug(`loadAgents(${d}) failed`, e);
|
|
19020
19438
|
}
|
|
19021
19439
|
}
|
|
19022
19440
|
if (!seen.size) {
|
|
@@ -19104,7 +19522,7 @@ ${task}`;
|
|
|
19104
19522
|
}
|
|
19105
19523
|
if (idx >= 0) {
|
|
19106
19524
|
const old = mounted.splice(idx, 1)[0];
|
|
19107
|
-
await old.client.close().catch((e) =>
|
|
19525
|
+
await old.client.close().catch((e) => log35.debug("mcp close failed", e));
|
|
19108
19526
|
}
|
|
19109
19527
|
try {
|
|
19110
19528
|
const m = await mountMcpServer(name, conf);
|
|
@@ -19132,7 +19550,7 @@ ${task}`;
|
|
|
19132
19550
|
}
|
|
19133
19551
|
const m = mounted.splice(idx, 1)[0];
|
|
19134
19552
|
remountMcpTools();
|
|
19135
|
-
await m.client.close().catch((e) =>
|
|
19553
|
+
await m.client.close().catch((e) => log35.debug("mcp close failed", e));
|
|
19136
19554
|
err(dim(` removed "${name}"
|
|
19137
19555
|
`));
|
|
19138
19556
|
return;
|
|
@@ -19335,7 +19753,7 @@ ${task}`;
|
|
|
19335
19753
|
try {
|
|
19336
19754
|
return readdirSync4(join15(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
19337
19755
|
} catch (e) {
|
|
19338
|
-
|
|
19756
|
+
log35.debug("completion readdir failed", absDir, e);
|
|
19339
19757
|
return null;
|
|
19340
19758
|
}
|
|
19341
19759
|
};
|
|
@@ -19472,7 +19890,7 @@ ${out}
|
|
|
19472
19890
|
return;
|
|
19473
19891
|
}
|
|
19474
19892
|
await refreshCatalogs().catch((e) => {
|
|
19475
|
-
|
|
19893
|
+
log35.debug("catalog refresh failed", e);
|
|
19476
19894
|
});
|
|
19477
19895
|
const sk = skills.find((x) => x.name === name);
|
|
19478
19896
|
if (sk) {
|
|
@@ -19583,7 +20001,7 @@ ${out}
|
|
|
19583
20001
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
19584
20002
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
19585
20003
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
19586
|
-
|
|
20004
|
+
log35.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
19587
20005
|
return void 0;
|
|
19588
20006
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
19589
20007
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -19678,6 +20096,14 @@ ${out}
|
|
|
19678
20096
|
};
|
|
19679
20097
|
try {
|
|
19680
20098
|
await voiceIO.start();
|
|
20099
|
+
if (args.duplexV2 && dx) {
|
|
20100
|
+
const v = voiceIO;
|
|
20101
|
+
bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
|
|
20102
|
+
err("\r\x1B[0J" + dim(` \u21B3 ${t}
|
|
20103
|
+
`));
|
|
20104
|
+
editorRef?.redrawNow();
|
|
20105
|
+
} }), () => voiceIO === v);
|
|
20106
|
+
}
|
|
19681
20107
|
const inDev = voiceIO.usingAec ? detectedInputDevice() : null;
|
|
19682
20108
|
const inNote = inDev ? `, in: ${inDev.name}` : "";
|
|
19683
20109
|
err(dim(` \u{1F3A4} voice on (${voiceIO.usingAec ? "echo-cancelled" : "heuristic echo \u2014 headphones recommended"}${inNote}) \u2014 just talk; speak over it to interrupt; say "voice off" to stop
|
|
@@ -19928,6 +20354,16 @@ function duplexError(e, reflexModel) {
|
|
|
19928
20354
|
return code && !msg.includes(String(code)) ? `${msg} (${code})` : msg;
|
|
19929
20355
|
}
|
|
19930
20356
|
var leakDecider = () => process.env.TYPESAFE_AI_KEY ? new DecideClient({ apiKey: process.env.TYPESAFE_AI_KEY }) : void 0;
|
|
20357
|
+
function makeSecondChannel(ai, reflexModel, opts) {
|
|
20358
|
+
const decider = leakDecider();
|
|
20359
|
+
const instinct = decider ? new Instinct({ decider }) : void 0;
|
|
20360
|
+
return new SecondChannel({
|
|
20361
|
+
decide: instinct ? (heard, state) => instinct.channel(heard, state) : void 0,
|
|
20362
|
+
generate: fastLineGenerator(ai, process.env.AGENTX_CHANNEL_MODEL || reflexModel),
|
|
20363
|
+
onEvent: (e) => log35.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""} ${e.ms}ms`),
|
|
20364
|
+
...opts
|
|
20365
|
+
});
|
|
20366
|
+
}
|
|
19931
20367
|
async function duplexServe(args, ai, cfg, cwd) {
|
|
19932
20368
|
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
19933
20369
|
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join15(cwd, ".agent", "mcp-auth.json") }));
|
|
@@ -19937,8 +20373,10 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19937
20373
|
const host = {
|
|
19938
20374
|
notify(e) {
|
|
19939
20375
|
const k = String(e.kind);
|
|
19940
|
-
if (k === "text_delta")
|
|
19941
|
-
|
|
20376
|
+
if (k === "text_delta") {
|
|
20377
|
+
out({ type: "text", text: e.message });
|
|
20378
|
+
sc?.onMainTokens(String(e.message ?? ""));
|
|
20379
|
+
} else if (k === "thinking_delta") out({ type: "thinking", text: e.message });
|
|
19942
20380
|
else if (k === "speak_utterance") out({ type: "speak", text: e.message });
|
|
19943
20381
|
else if (k === "hold_filler") out({ type: "filler", text: e.message });
|
|
19944
20382
|
else if (k === "turn_replace") out({ type: "replaced", ...e.data });
|
|
@@ -19970,6 +20408,8 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19970
20408
|
reflexOptions: { fs: agent.options.fs, tools: [exitSessionTool(() => out({ type: "exit" }))] }
|
|
19971
20409
|
});
|
|
19972
20410
|
if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
|
|
20411
|
+
let scTurn = 0;
|
|
20412
|
+
const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
|
|
19973
20413
|
const store = new SessionStore(cwd);
|
|
19974
20414
|
const session = startSession(args, store, dx.voice, cwd);
|
|
19975
20415
|
persist = () => {
|
|
@@ -19978,7 +20418,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19978
20418
|
try {
|
|
19979
20419
|
store.save(session);
|
|
19980
20420
|
} catch (e) {
|
|
19981
|
-
|
|
20421
|
+
log35.warn("duplex session save failed", e);
|
|
19982
20422
|
}
|
|
19983
20423
|
};
|
|
19984
20424
|
out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
|
|
@@ -19992,20 +20432,30 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19992
20432
|
return;
|
|
19993
20433
|
}
|
|
19994
20434
|
if (m.type === "barge") {
|
|
20435
|
+
sc?.onBargeIn();
|
|
19995
20436
|
activeTurn?.abort();
|
|
19996
20437
|
dx.parkInFlightDeliveries();
|
|
20438
|
+
} else if (sc && channelInput(sc, m)) {
|
|
19997
20439
|
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
19998
20440
|
else if (m.type === "exit") rl.close();
|
|
19999
20441
|
else if (m.type === "user" && typeof m.text === "string" && m.text.trim()) {
|
|
20000
|
-
const {
|
|
20442
|
+
const cut = m.cut && sc?.spoke(String(m.cut.full ?? "")) ? { ...m.cut, full: "", heard: "" } : m.cut;
|
|
20443
|
+
const { reply, delivery, heard } = splitCut(cut ?? null);
|
|
20001
20444
|
if (delivery || reply) dx.parkCutDelivery(delivery, !!reply, heard);
|
|
20002
20445
|
const note = reply && reply.full.length - reply.heard.length > 40 ? interruptionNote(reply) : "";
|
|
20003
20446
|
const id = m.id ? String(m.id) : void 0;
|
|
20447
|
+
sc?.onUserTurn(m.text);
|
|
20448
|
+
const seq = ++scTurn;
|
|
20449
|
+
const done2 = () => {
|
|
20450
|
+
if (seq === scTurn) sc?.onAgentState("idle");
|
|
20451
|
+
};
|
|
20004
20452
|
void runTurn(dx.voice, store, session, m.text + note, void 0, cwd, (c) => dx.send(c, { id, replaces: m.replaces ? String(m.replaces) : void 0, followUp: typeof m.followUp === "string" || m.followUp && typeof m.followUp === "object" ? m.followUp : void 0 })).then(({ res, error }) => {
|
|
20005
20453
|
const why = (res?.finishReason ?? "error") === "error" ? duplexError(res?.error ?? error, dx.options.reflexModel) : "";
|
|
20454
|
+
done2();
|
|
20006
20455
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
20007
20456
|
}).catch((e) => {
|
|
20008
|
-
|
|
20457
|
+
log35.error("duplex turn failed", e);
|
|
20458
|
+
done2();
|
|
20009
20459
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
20010
20460
|
});
|
|
20011
20461
|
}
|