agentus 0.9.0 → 0.10.0
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/package.json +4 -2
- package/packages/server/src/settings.ts +50 -4
- package/packages/web/dist/assets/{index-U7648MzE.js → index-B2YR8mN4.js} +60 -57
- package/packages/web/dist/assets/index-CDd0dpAg.css +32 -0
- package/packages/web/dist/assets/ort-wasm-simd-threaded-DcHrbrbl.wasm +0 -0
- package/packages/web/dist/assets/ort.wasm.bundle.min-kEi4lvn9.js +7 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/vad/silero_vad.onnx +0 -0
- package/packages/web/dist/assets/index-D2rwXFNG.css +0 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Multi-session web cockpit for ACP-speaking coding agents (Qoder CLI / Hermes). UI has no intelligence — the agent lives in spawned CLI subprocesses.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -83,7 +83,9 @@
|
|
|
83
83
|
"resume-tail": "tsx scripts/qa/resume-tail.mts",
|
|
84
84
|
"render-lag-sweep": "node scripts/qa/render-lag-sweep.mjs",
|
|
85
85
|
"replay-window": "node scripts/qa/replay-window.mjs",
|
|
86
|
-
"think-cadence": "node scripts/qa/think-cadence.mjs"
|
|
86
|
+
"think-cadence": "node scripts/qa/think-cadence.mjs",
|
|
87
|
+
"vad": "tsx scripts/qa/vad.mts",
|
|
88
|
+
"vad-silero": "tsx scripts/qa/vad-silero.mts"
|
|
87
89
|
},
|
|
88
90
|
"engines": {
|
|
89
91
|
"node": ">=22.5"
|
|
@@ -62,7 +62,7 @@ export const THEME_DEFAULT: ThemeSettings = { mode: "system", accent: "" };
|
|
|
62
62
|
* what a phone's own loudspeaker leaks back into its microphone is nothing like a headset
|
|
63
63
|
* in a quiet room — so the defaults are a starting point, not a truth. The call page owns
|
|
64
64
|
* the panel; the settings page reads the same numbers. */
|
|
65
|
-
export type InterruptMode = "voice" | "button";
|
|
65
|
+
export type InterruptMode = "voice" | "button" | "keyword";
|
|
66
66
|
|
|
67
67
|
export interface CallSettings {
|
|
68
68
|
/** WHO gets to interrupt the agent, and the operator picks:
|
|
@@ -85,6 +85,10 @@ export interface CallSettings {
|
|
|
85
85
|
* as a turn (1 = no limit). A tap on the orb always sends what is there — that gesture
|
|
86
86
|
* is an explicit instruction, not a guess about where a sentence ended. */
|
|
87
87
|
minChars: number;
|
|
88
|
+
/** 关键词打断 — the phrase that must be HEARD before the floor changes hands (opt-in; empty means
|
|
89
|
+
* the mode is inert). No shipped default on purpose: nobody should be able to interrupt this
|
|
90
|
+
* agent by guessing a word we chose for them. */
|
|
91
|
+
wakePhrase: string;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
/** How the operator wants to TALK to the agent. These were browser-local (localStorage) and
|
|
@@ -150,14 +154,32 @@ export const PREFS_DEFAULT: PrefsSettings = {
|
|
|
150
154
|
planHint: { on: true, everyTurn: true, instruction: "" },
|
|
151
155
|
};
|
|
152
156
|
|
|
157
|
+
/** The longest wake phrase, in CODE POINTS (an emoji is one, not two). One number, one place — the
|
|
158
|
+
* web side has its own copy (`WAKE_MAX` in callSettings.ts) and the two must keep saying 24. */
|
|
159
|
+
const WAKE_MAX = 24;
|
|
160
|
+
|
|
161
|
+
/** Drop half-pairs left by a client that counted UTF-16 units and cut in the middle of an emoji.
|
|
162
|
+
* `for...of` walks code points, so a lone surrogate arrives as a one-character string and is easy
|
|
163
|
+
* to spot; storing it would put U+FFFD in the database. */
|
|
164
|
+
function stripLone(s: string): string {
|
|
165
|
+
let out = "";
|
|
166
|
+
for (const ch of s) {
|
|
167
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
168
|
+
if (cp >= 0xD800 && cp <= 0xDFFF) continue; // a surrogate that did not pair with its other half
|
|
169
|
+
out += ch;
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
export const CALL_DEFAULT: CallSettings = {
|
|
154
175
|
/* 说完停顿 1.5s (was 1.2): the operator reads a pause as "it stopped listening" before 1.2 s,
|
|
155
176
|
and a long sentence with a breath in the middle got cut. Both ends of the range still let
|
|
156
177
|
the room decide — this is only where a fresh install starts. */
|
|
157
178
|
interruptMode: "voice", bargeSensitivity: 60, bargeMs: 300, silenceMs: 1500, minChars: 3,
|
|
179
|
+
wakePhrase: "",
|
|
158
180
|
};
|
|
159
181
|
|
|
160
|
-
export const INTERRUPT_MODES: InterruptMode[] = ["voice", "button"];
|
|
182
|
+
export const INTERRUPT_MODES: InterruptMode[] = ["voice", "button", "keyword"];
|
|
161
183
|
|
|
162
184
|
/** How the cockpit treats AGENT PROCESSES — as opposed to what a conversation contains. AionUi
|
|
163
185
|
* ships the same knob in 设置 → 系统 as 「Agent 空闲超时(分钟)」 (default 5, 1–60, step 5) with the
|
|
@@ -369,7 +391,11 @@ function callAtShippedDefaults(r: Record<string, unknown>): boolean {
|
|
|
369
391
|
// The mode counts too: a row that only says `interruptMode: "button"` IS a deliberate choice and
|
|
370
392
|
// must not be reset along with a section that happens to still carry the old numbers.
|
|
371
393
|
const modeShipped = r.interruptMode === undefined || r.interruptMode === CALL_DEFAULT.interruptMode;
|
|
372
|
-
|
|
394
|
+
// …and a stored wake phrase is a deliberate choice too: without this, setting one and switching
|
|
395
|
+
// back to 语音打断 would make the whole section look untouched and the phrase would be dropped.
|
|
396
|
+
const wakeShipped = r.wakePhrase === undefined || r.wakePhrase === CALL_DEFAULT.wakePhrase;
|
|
397
|
+
return sens && modeShipped && wakeShipped
|
|
398
|
+
&& Number(r.bargeMs) === 300 && Number(r.silenceMs) === 1200 && Number(r.minChars) === 1;
|
|
373
399
|
}
|
|
374
400
|
|
|
375
401
|
/** What each knob may be. The ranges are the panel's slider ends; the server is the one
|
|
@@ -503,6 +529,12 @@ function pickCall(raw: unknown): CallSettings {
|
|
|
503
529
|
if (callAtShippedDefaults(raw as Record<string, unknown>)) return out;
|
|
504
530
|
const mode = (raw as Record<string, unknown>).interruptMode;
|
|
505
531
|
if (INTERRUPT_MODES.includes(mode as InterruptMode)) out.interruptMode = mode as InterruptMode;
|
|
532
|
+
const wake = (raw as Record<string, unknown>).wakePhrase;
|
|
533
|
+
// NOT length-capped here on purpose: this is what somebody ELSE stored (curl, a script, a previous
|
|
534
|
+
// version). Cutting it on the way out would then be written straight back by the `persist()` that
|
|
535
|
+
// runs at boot — silently destroying the operator's phrase with no log and nothing on screen.
|
|
536
|
+
// Length belongs to the write path (`saveCall`), which is where a human typed it.
|
|
537
|
+
if (typeof wake === "string") out.wakePhrase = wake.replace(/\s+/g, " ").trim();
|
|
506
538
|
for (const k of CALL_NUMERIC) {
|
|
507
539
|
const v = Number((raw as Record<string, unknown>)[k]);
|
|
508
540
|
const [lo, hi] = CALL_RANGE[k];
|
|
@@ -734,10 +766,24 @@ export function saveCall(patch: Record<string, unknown>): Settings {
|
|
|
734
766
|
if ("interruptMode" in patch) {
|
|
735
767
|
const mode = patch.interruptMode;
|
|
736
768
|
if (!INTERRUPT_MODES.includes(mode as InterruptMode)) {
|
|
737
|
-
throw new Error(`invalid interruptMode: ${JSON.stringify(mode)} (expected voice|button)`);
|
|
769
|
+
throw new Error(`invalid interruptMode: ${JSON.stringify(mode)} (expected voice|button|keyword)`);
|
|
738
770
|
}
|
|
739
771
|
c.interruptMode = mode as InterruptMode;
|
|
740
772
|
}
|
|
773
|
+
if ("wakePhrase" in patch) {
|
|
774
|
+
if (typeof patch.wakePhrase !== "string") throw new Error("invalid wakePhrase: expected a string");
|
|
775
|
+
// Truncated, not rejected: this field arrives in the same body as the sliders, and refusing it
|
|
776
|
+
// would throw away the operator's OTHER changes with it (and the panel would then say "saved
|
|
777
|
+
// locally, not on the server" for a phrase nobody was trying to abuse). Same rule as pickCall.
|
|
778
|
+
const wake = stripLone(patch.wakePhrase.replace(/\s+/g, " ").trim()); // an old client can slice a surrogate in half
|
|
779
|
+
const kept = [...wake];
|
|
780
|
+
if (kept.length > WAKE_MAX) {
|
|
781
|
+
// Truncated rather than refused, but never silently: one line in the log is the difference
|
|
782
|
+
// between 「我设过这个词」 and 「它自己变短了」 when a script writes 40 characters.
|
|
783
|
+
console.warn(`[agentus] wakePhrase truncated: ${kept.length} → ${WAKE_MAX} characters`);
|
|
784
|
+
}
|
|
785
|
+
c.wakePhrase = kept.slice(0, WAKE_MAX).join("");
|
|
786
|
+
}
|
|
741
787
|
for (const key of CALL_NUMERIC) {
|
|
742
788
|
if (!(key in patch)) continue;
|
|
743
789
|
const raw = typeof patch[key] === "number" ? patch[key] as number : Number(patch[key]);
|