agentus 0.12.2 → 0.13.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/bin/cli.mjs +108 -8
- package/docs/dev/04-recover.md +5 -3
- package/docs/transcript-folding.md +21 -1
- package/package.json +8 -2
- package/packages/server/mock/agent.mjs +39 -2
- package/packages/server/src/acp/backends.ts +22 -0
- package/packages/server/src/acp/session-manager.ts +229 -23
- package/packages/server/src/acp/voice-mode.ts +7 -1
- package/packages/server/src/cockpit/api.ts +3 -1
- package/packages/server/src/index.ts +129 -5
- package/packages/server/src/settings.ts +15 -3
- package/packages/server/src/store/store.ts +286 -28
- package/packages/shared/src/index.ts +22 -0
- package/packages/web/dist/assets/index-BEpVrnpI.js +131 -0
- package/packages/web/dist/assets/{index-Dej-v4qD.css → index-vVv5fr7N.css} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/index-1RjGZyMJ.js +0 -131
package/bin/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* cross-session `send`. Dispatched by `bin/agentus.mjs`, which is also what the cockpit hands agents
|
|
5
5
|
* as `AGENTUS_COCKPIT_BIN`, so there is exactly one command and one spelling everywhere.
|
|
6
6
|
*
|
|
7
|
-
* SHAPE: noun, then verb — `agentus session list|show|messages|plan|send`
|
|
7
|
+
* SHAPE: noun, then verb — `agentus session list|show|messages|plan|send|wake|create|stop`
|
|
8
8
|
* The five things an agent needs are all about ONE object (a session), so the noun is not decoration:
|
|
9
9
|
* it keeps `list` from meaning "list what?" and leaves the top level free for the two commands that
|
|
10
10
|
* are NOT about a session — `capabilities` (this CLI's contract) and `skill` (its manual). The
|
|
@@ -54,10 +54,16 @@ another one
|
|
|
54
54
|
agentus session messages <id> [--tail N]
|
|
55
55
|
the last N messages, each cut to the documented size
|
|
56
56
|
agentus session plan <id> the plan object the operator's card renders
|
|
57
|
-
agentus session send <id> <text…> [--wake]
|
|
57
|
+
agentus session send <id> <text…> [--wake] [--interrupt] [--reply-on-done]
|
|
58
58
|
deliver a message into another session (stdin if no text);
|
|
59
|
-
--wake also starts the target when it is cold
|
|
59
|
+
--wake also starts the target when it is cold;
|
|
60
|
+
--interrupt cancels the turn it is running first (explicit)
|
|
60
61
|
agentus session wake <id> start a cold session's process (same switch as send)
|
|
62
|
+
agentus session create <cwd> [--backend <id>] [--title <text>]
|
|
63
|
+
open a NEW session in that directory and answer with its row
|
|
64
|
+
(same switch as send — it costs one agent process)
|
|
65
|
+
agentus session stop <id> [--close] interrupt the turn it is running; --close stops the PROCESS
|
|
66
|
+
(the slot goes cold, the transcript stays)
|
|
61
67
|
|
|
62
68
|
agentus capabilities the contract: commands, fields, truncation, scope, send state
|
|
63
69
|
agentus skill [--path] this CLI's manual, as shipped in the package
|
|
@@ -295,7 +301,15 @@ if (cmd === "capabilities") {
|
|
|
295
301
|
// The text is the plain words after the id — an agent writing a sentence should not have to
|
|
296
302
|
// build JSON to say it. With no words, stdin is accepted: either a JSON `{"message": "…"}` (what
|
|
297
303
|
// older callers sent) or the raw text.
|
|
298
|
-
|
|
304
|
+
const wantWake = args.includes("--wake");
|
|
305
|
+
const wantInterrupt = args.includes("--interrupt");
|
|
306
|
+
// #59:`--reply-on-done` 不是"抢话头",是"别让我在这儿等它跑完" —— 调用立即返回,目标回合
|
|
307
|
+
// 自然结束时服务端往本会话投一条【完成回推】。派活给一个要跑十分钟的会话时才用得上。
|
|
308
|
+
const wantReply = args.includes("--reply-on-done");
|
|
309
|
+
const FLAGS = new Set(["--wake", "--interrupt", "--reply-on-done"]);
|
|
310
|
+
const straySend = args.slice(1).filter((a) => a.startsWith("--") && !FLAGS.has(a));
|
|
311
|
+
if (straySend.length) fail("bad_request", `unknown flag ${straySend[0]} (usage: agentus session send <id> <text…> [--wake] [--interrupt] [--reply-on-done])`);
|
|
312
|
+
let message = args.slice(1).filter((a) => !FLAGS.has(a)).join(" ").trim();
|
|
299
313
|
if (!message) {
|
|
300
314
|
const raw = await readStdin();
|
|
301
315
|
if (raw) {
|
|
@@ -313,7 +327,7 @@ if (cmd === "capabilities") {
|
|
|
313
327
|
res = await fetch(`${EP}/api/cockpit/send`, {
|
|
314
328
|
method: "POST",
|
|
315
329
|
headers: headers(true),
|
|
316
|
-
body: JSON.stringify({ to: id, message, ...(
|
|
330
|
+
body: JSON.stringify({ to: id, message, ...(wantWake ? { wake: true } : {}), ...(wantInterrupt ? { interrupt: true } : {}), ...(wantReply ? { replyOnDone: true } : {}) }),
|
|
317
331
|
});
|
|
318
332
|
} catch (e) {
|
|
319
333
|
fail("unreachable", `the cockpit did not answer: ${e?.message ?? e}`);
|
|
@@ -333,9 +347,17 @@ if (cmd === "capabilities") {
|
|
|
333
347
|
data: body,
|
|
334
348
|
meta: {
|
|
335
349
|
schema_version: 1,
|
|
336
|
-
note:
|
|
337
|
-
|
|
338
|
-
|
|
350
|
+
note: [
|
|
351
|
+
body?.delivered_after_wake
|
|
352
|
+
? "the target was COLD: this call woke it (a process now exists) and then delivered."
|
|
353
|
+
: null,
|
|
354
|
+
body?.interrupted
|
|
355
|
+
? "the target was BUSY: its turn was interrupted (cancelled, then the session was waited to really stop) before yours started — the words it had already produced stay in its transcript."
|
|
356
|
+
: null,
|
|
357
|
+
body?.reply_on_done
|
|
358
|
+
? "reply-on-done: this call returned as soon as the target's turn STARTED. When that turn ends, the cockpit posts a 【完成回推】 notice into this session and wakes you; read the actual result with `session messages <target>`."
|
|
359
|
+
: "delivered means the target agent received it and started a turn — it does NOT mean it read or agreed. Only the operator sees the reply.",
|
|
360
|
+
].filter(Boolean).join(" "),
|
|
339
361
|
},
|
|
340
362
|
});
|
|
341
363
|
} else if (verb === "wake") {
|
|
@@ -364,6 +386,84 @@ if (cmd === "capabilities") {
|
|
|
364
386
|
: "the target was already live: nothing was started.",
|
|
365
387
|
},
|
|
366
388
|
});
|
|
389
|
+
} else if (verb === "create") {
|
|
390
|
+
// The FIRST positional is the directory, not an id: this is the one verb that makes an object
|
|
391
|
+
// instead of acting on one. `~` is expanded HERE as well as server-side so the error a caller
|
|
392
|
+
// reads names the path it actually meant.
|
|
393
|
+
// Walk argv once, skipping each flag's VALUE — otherwise `create --backend hermes /tmp/x`
|
|
394
|
+
// reads "hermes" as the directory and then rejects the real path as an extra argument.
|
|
395
|
+
const VALUE_FLAGS = new Set(["--backend", "--title", "--profile", "--model"]);
|
|
396
|
+
let cwdArg;
|
|
397
|
+
for (let i = 0; i < args.length; i++) {
|
|
398
|
+
const a = args[i];
|
|
399
|
+
if (a.startsWith("--")) { if (VALUE_FLAGS.has(a)) i++; continue; }
|
|
400
|
+
cwdArg = a;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
if (!cwdArg) fail("bad_request", "usage: agentus session create <cwd> [--backend <id>] [--title <text>]");
|
|
404
|
+
const flags = { backend: undefined, title: undefined };
|
|
405
|
+
for (let i = 0; i < args.length; i++) {
|
|
406
|
+
const a = args[i];
|
|
407
|
+
if (a === cwdArg) continue;
|
|
408
|
+
if (a === "--backend" || a === "--title") {
|
|
409
|
+
const v = args[++i];
|
|
410
|
+
if (v === undefined) fail("bad_request", `${a} needs a value`);
|
|
411
|
+
flags[a.slice(2)] = v;
|
|
412
|
+
} else fail("bad_request", `unexpected argument "${a}" (usage: agentus session create <cwd> [--backend <id>] [--title <text>])`);
|
|
413
|
+
}
|
|
414
|
+
let res;
|
|
415
|
+
try {
|
|
416
|
+
res = await fetch(`${EP}/api/cockpit/create`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: headers(true),
|
|
419
|
+
body: JSON.stringify({ cwd: cwdArg, ...flags }),
|
|
420
|
+
});
|
|
421
|
+
} catch (e) {
|
|
422
|
+
fail("unreachable", `the cockpit did not answer: ${e?.message ?? e}`);
|
|
423
|
+
}
|
|
424
|
+
const body = await res.json().catch(() => null);
|
|
425
|
+
if (!res.ok) {
|
|
426
|
+
// An unknown backend is only actionable if the caller is told WHICH ones exist — the same reason
|
|
427
|
+
// `target_cold` names `--wake` instead of being a bare prohibition.
|
|
428
|
+
fail(String(body?.error ?? `http_${res.status}`),
|
|
429
|
+
String(body?.message ?? body?.error ?? `the cockpit answered HTTP ${res.status}`),
|
|
430
|
+
Array.isArray(body?.backends) ? { details: { backends: body.backends } } : {});
|
|
431
|
+
}
|
|
432
|
+
out({
|
|
433
|
+
success: true,
|
|
434
|
+
data: body,
|
|
435
|
+
meta: {
|
|
436
|
+
schema_version: 1,
|
|
437
|
+
note: "the session exists and is starting. Read its id from data.session and send into it at once; a slot with no process yet answers cold_target unless you pass --wake.",
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
} else if (verb === "stop") {
|
|
441
|
+
if (!id) fail("bad_request", "usage: agentus session stop <id> [--close]");
|
|
442
|
+
const close = args.includes("--close");
|
|
443
|
+
const leftovers = args.slice(1).filter((a) => a !== "--close");
|
|
444
|
+
if (leftovers.length) fail("bad_request", `unexpected argument "${leftovers[0]}" (usage: agentus session stop <id> [--close])`);
|
|
445
|
+
let res;
|
|
446
|
+
try {
|
|
447
|
+
res = await fetch(`${EP}/api/cockpit/stop`, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers: headers(true),
|
|
450
|
+
body: JSON.stringify(close ? { id, close: true } : { id }),
|
|
451
|
+
});
|
|
452
|
+
} catch (e) {
|
|
453
|
+
fail("unreachable", `the cockpit did not answer: ${e?.message ?? e}`);
|
|
454
|
+
}
|
|
455
|
+
const body = await res.json().catch(() => null);
|
|
456
|
+
if (!res.ok) fail(String(body?.error ?? `http_${res.status}`), String(body?.message ?? body?.error ?? `the cockpit answered HTTP ${res.status}`));
|
|
457
|
+
out({
|
|
458
|
+
success: true,
|
|
459
|
+
data: body,
|
|
460
|
+
meta: {
|
|
461
|
+
schema_version: 1,
|
|
462
|
+
note: close
|
|
463
|
+
? (body?.closed ? "its agent process is stopped and the slot is cold; the transcript is untouched." : "nothing was running: the slot was already cold.")
|
|
464
|
+
: (body?.stopped ? "the turn was cancelled; the words it had already produced stay in the transcript." : "no turn was running — nothing was interrupted."),
|
|
465
|
+
},
|
|
466
|
+
});
|
|
367
467
|
} else {
|
|
368
468
|
process.stderr.write(USAGE);
|
|
369
469
|
fail("bad_request", `unknown session command: ${verb ?? "(none)"}`);
|
package/docs/dev/04-recover.md
CHANGED
|
@@ -99,9 +99,11 @@ cp -p packages/server/.data/agentus.sqlite \
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
The one migration path in the project that rewrites rows in place — the legacy transcript
|
|
102
|
-
fold — takes its own backup first with `VACUUM INTO <db>.pre-fold
|
|
103
|
-
`
|
|
104
|
-
|
|
102
|
+
fold — takes its own backup first with `VACUUM INTO <db>.pre-fold-v<ver>-<timestamp>.bak` and
|
|
103
|
+
records itself in the `repairs` table, so it runs once per version and is idempotent. Each pass
|
|
104
|
+
version gets its own file and the newest three per pass version are kept (older ones are pruned), so
|
|
105
|
+
"a `.pre-fold.bak` exists" no longer means anything: read `repairs` for what ran, and pick the
|
|
106
|
+
backup by timestamp.
|
|
105
107
|
|
|
106
108
|
Two cautions that come from real incidents:
|
|
107
109
|
|
|
@@ -112,9 +112,19 @@ accepted rather than guessed away.
|
|
|
112
112
|
2. **The row is the truth on reads; `delta` exists only so a live turn does not re-send the whole text
|
|
113
113
|
every frame.** Anything that reads the transcript (`hotwords.ts` context, the phone's notifications)
|
|
114
114
|
reads rows, so it sees accumulated text for free — it used to see 40 chunks of 3.8 characters.
|
|
115
|
+
2b. **Two frames are one stream only if they are close in time.** The rule is: the gap from the
|
|
116
|
+
previous frame is at most 2 s, the turn id (when both rows carry one) agrees, and a sentence end
|
|
117
|
+
only counts as a boundary when that gap is over 750 ms. The accepted cost is written down rather
|
|
118
|
+
than hidden: two id-less messages written less than 750 ms apart, the first ending in a full stop,
|
|
119
|
+
are indistinguishable from one stream and get merged — but only when both are short fragments (≤24
|
|
120
|
+
characters) carrying no `messageId` and no `block_key`, which is the only shape this rule applies to;
|
|
121
|
+
a longer second message stays its own row even at a 40 ms gap. Time is the only witness a store without
|
|
122
|
+
message ids has, and the alternative — the rule that shipped in v3 — cut every reply at its first
|
|
123
|
+
full stop.
|
|
115
124
|
3. **Never fold on a guess.** Same id, or whitespace-insensitive equality, or ≥60% coverage. Everything
|
|
116
125
|
else stays as it is, and the migration is logged (`[store] 历史分片已折叠:3804 行 → …`) with a
|
|
117
|
-
one-time `VACUUM INTO` backup beside the DB (`agentus.sqlite.pre-fold
|
|
126
|
+
one-time `VACUUM INTO` backup beside the DB (`agentus.sqlite.pre-fold-v<ver>-<timestamp>.bak`,
|
|
127
|
+
newest three kept per pass version; what actually ran is in the `repairs` table).
|
|
118
128
|
4. **A page boundary must not be able to cut a message.** Page sizes are row- and byte-bounded, and the
|
|
119
129
|
store guarantees a message is one row; if either changes, re-run the checks below.
|
|
120
130
|
|
|
@@ -137,3 +147,13 @@ curl -s -H "Authorization: Bearer $(cat "${AGENTUS_DATA:-$HOME/.agentus}/auth.to
|
|
|
137
147
|
Result for `hello`: **3804 rows → 70** (10 agent messages, 15 thought blocks, 37 tool cards, 8 prompts),
|
|
138
148
|
`hasOlder=false`, both markdown tables reported complete (the 2623-character reply holds its 8-row table
|
|
139
149
|
in one block), and one duplicate left — the 12-character re-send described above.
|
|
150
|
+
|
|
151
|
+
## Splitting rows an older build glued together
|
|
152
|
+
|
|
153
|
+
A re-walk cannot undo a merge: the second half's row is gone, so there is nothing to fold back out.
|
|
154
|
+
`scripts/repair-glued-rows.py <db> <older-backup.bak>` finds the pairs that can be **proved** glued —
|
|
155
|
+
both halves must appear, in that order, as message texts in an older copy of the same database, and
|
|
156
|
+
both must be ≤24 characters — and prints them; `--apply` rewrites them. It is a dry run unless you
|
|
157
|
+
pass `--apply`, and it puts the second half 5 s after the first so the corrected rule never joins
|
|
158
|
+
them again.
|
|
159
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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": {
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"apk": "bash scripts/build-apk.sh",
|
|
76
76
|
"transcript-fold": "tsx scripts/qa/transcript-fold.mts",
|
|
77
77
|
"store-fold": "tsx scripts/qa/store-fold.mts",
|
|
78
|
+
"context-window": "tsx scripts/qa/context-window.mts",
|
|
78
79
|
"live-stream": "tsx scripts/qa/live-stream.mts",
|
|
79
80
|
"open-target": "tsx scripts/qa/open-target.mts",
|
|
80
81
|
"rail-recent": "tsx scripts/qa/rail-recent.mts",
|
|
@@ -86,7 +87,12 @@
|
|
|
86
87
|
"replay-window": "node scripts/qa/replay-window.mjs",
|
|
87
88
|
"think-cadence": "node scripts/qa/think-cadence.mjs",
|
|
88
89
|
"vad": "tsx scripts/qa/vad.mts",
|
|
89
|
-
"vad-silero": "tsx scripts/qa/vad-silero.mts"
|
|
90
|
+
"vad-silero": "tsx scripts/qa/vad-silero.mts",
|
|
91
|
+
"tus-mood": "tsx scripts/qa/tus-mood.mts",
|
|
92
|
+
"tus-mode-sweep": "node scripts/qa/tus-mode-sweep.mjs",
|
|
93
|
+
"usage-row-sweep": "node scripts/qa/usage-row-sweep.mjs",
|
|
94
|
+
"call-brief-sweep": "node scripts/qa/call-brief-sweep.mjs",
|
|
95
|
+
"reply-on-done": "node scripts/qa/reply-on-done.mjs"
|
|
90
96
|
},
|
|
91
97
|
"engines": {
|
|
92
98
|
"node": ">=22.5"
|
|
@@ -643,9 +643,13 @@ const agent = () => ({
|
|
|
643
643
|
// A turn that does real work: N tool calls with a reasoning burst before each — the
|
|
644
644
|
// transcript-flood case (ten calls + ten bursts). `[tools:10]` drives it with no permission
|
|
645
645
|
// prompts: the point is the SHAPE of the transcript, not the approval.
|
|
646
|
-
|
|
646
|
+
// `[tools:N]` (25 ms in-progress window, the real shape) or `[tools:N:ms]` for QA: a 25 ms
|
|
647
|
+
// window is honest but a page that misses the re-render inside it can only be blamed on the
|
|
648
|
+
// machine's load, so a test that needs to OBSERVE the running state widens it explicitly.
|
|
649
|
+
const many = /\[tools:(\d+)(?::(\d+))?\]/.exec(text);
|
|
647
650
|
if (many) {
|
|
648
651
|
const n = Math.min(20, Math.max(1, Number(many[1]) || 1));
|
|
652
|
+
const windowMs = Math.min(3000, Math.max(10, Number(many[2]) || 25));
|
|
649
653
|
for (let i = 1; i <= n; i++) {
|
|
650
654
|
const burst = (`step ${i}: reading the file, checking the surrounding code, then writing `
|
|
651
655
|
+ "the change back. This sentence is deliberately long so the reasoning block is taller "
|
|
@@ -669,7 +673,7 @@ const agent = () => ({
|
|
|
669
673
|
status: "in_progress",
|
|
670
674
|
rawInput: { step: i },
|
|
671
675
|
});
|
|
672
|
-
await sleep(
|
|
676
|
+
await sleep(windowMs);
|
|
673
677
|
await send(agent._conn, sessionId, {
|
|
674
678
|
sessionUpdate: "tool_call_update",
|
|
675
679
|
toolCallId: id,
|
|
@@ -846,6 +850,23 @@ const agent = () => ({
|
|
|
846
850
|
// One id per logical message, exactly like the real backend: the cockpit (and the store) fold the
|
|
847
851
|
// chunks of a message by this id, so a mock that omits it would test a path nobody runs.
|
|
848
852
|
const messageId = randomUUID();
|
|
853
|
+
// 表情通道的探针(糯米模式):提示里带 `[emo-probe]`(只有 QA sweep 会发)**并且**同一轮
|
|
854
|
+
// 真的收到了 `[emo:x]` 提示句,才在回复开头吐一个标签。于是「服务端那段提示词到底有没有
|
|
855
|
+
// 到达 agent」变成一个能观测的事实,而不是靠读服务端代码相信;别的 sweep 不发这个探针,
|
|
856
|
+
// 行为零变化(也不需要给子进程多开一个 env 白名单出口)。
|
|
857
|
+
// 提示句是**另一个内容块**(ACP resource),不在 `text` 里 —— 只看 text 会得出「服务端没注入」
|
|
858
|
+
// 的错误结论(这次就是这么踩的)。所以探针看的是整条 prompt 的所有块。
|
|
859
|
+
const allBlocks = prompt.map((p) => p.text || p.resource?.text || "").join(" ");
|
|
860
|
+
const probe = allBlocks.match(/\[emo-probe(?::([a-z]+))?\]/i);
|
|
861
|
+
if (probe && /\[emo:[a-z]+\]/.test(allBlocks)) {
|
|
862
|
+
// 带后缀可指定要回哪张脸([emo-probe:calm]);不带后缀保持 happy,老 sweep 不用改。
|
|
863
|
+
const mood = (probe[1] || "happy").toLowerCase();
|
|
864
|
+
await send(agent._conn, sessionId, {
|
|
865
|
+
sessionUpdate: "agent_message_chunk",
|
|
866
|
+
messageId,
|
|
867
|
+
content: { type: "text", text: `[emo:${mood}] ` },
|
|
868
|
+
});
|
|
869
|
+
}
|
|
849
870
|
const words = `Mock echo (${effort} effort) to: "${text}". `.repeat(2).split(" ");
|
|
850
871
|
for (const w of words) {
|
|
851
872
|
if (s.cancelled) return { stopReason: "cancelled" };
|
|
@@ -865,6 +886,22 @@ const agent = () => ({
|
|
|
865
886
|
messageId,
|
|
866
887
|
content: { type: "text", text: MOCK_MARKDOWN },
|
|
867
888
|
});
|
|
889
|
+
// `[think-late:N]` — 回答已经开始之后才到的思考块。真实后端会这样交错(思考 → 回答 →
|
|
890
|
+
// 又一段思考),而这正是「耳朵里在念回答、转录里还写着思考中」的来源。QA only:
|
|
891
|
+
// 没有这个探针的轮次行为零变化。
|
|
892
|
+
const lateThink = /\[think-late(?::(\d+))?\]/.exec(text);
|
|
893
|
+
if (lateThink) {
|
|
894
|
+
const thinkId = `th-${sessionId}-late`;
|
|
895
|
+
const n = Math.min(Math.max(Number(lateThink[1] ?? 2) || 2, 1), 12);
|
|
896
|
+
for (let i = 0; i < n; i++) {
|
|
897
|
+
await send(agent._conn, sessionId, {
|
|
898
|
+
sessionUpdate: "agent_thought_chunk",
|
|
899
|
+
messageId: thinkId,
|
|
900
|
+
content: { type: "text", text: `after the answer started, thought ${i + 1}. ` },
|
|
901
|
+
});
|
|
902
|
+
await sleep(SLOW);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
868
905
|
// Context gauge data, shaped like a real agent's usage_update (AionUi F-DISPLAY-07).
|
|
869
906
|
// `/compress` is the agent's own command for this: honour it by actually lowering the
|
|
870
907
|
// reported context, so "the button did something" is a measurable claim.
|
|
@@ -24,6 +24,28 @@ export interface HomeIsolation {
|
|
|
24
24
|
homeDefault: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Hand a backend its declared context window as argv, when that is the only way it can hear it.
|
|
29
|
+
*
|
|
30
|
+
* Qoder reports no `usage_update` and advertises no context option over ACP (issue #57), but its CLI
|
|
31
|
+
* takes a global `--context-window <n>`; its own log line (`context_window.restore_skipped
|
|
32
|
+
* reason=cli_context_window_override`) says the flag wins over the window restored from a resumed
|
|
33
|
+
* session. Every other backend keeps the ACP route (Hermes has `context_budget`), so this does
|
|
34
|
+
* nothing for them — and an arg the operator already put in the row is never duplicated.
|
|
35
|
+
*/
|
|
36
|
+
export function applyContextWindow(
|
|
37
|
+
id: string,
|
|
38
|
+
spec: { kind?: string },
|
|
39
|
+
args: string[],
|
|
40
|
+
contextLimit?: number | null,
|
|
41
|
+
): string[] {
|
|
42
|
+
const declared = typeof contextLimit === "number" && contextLimit > 0 ? Math.round(contextLimit) : null;
|
|
43
|
+
if (!declared) return args;
|
|
44
|
+
if (id !== "qoder" && spec.kind !== "qoder") return args;
|
|
45
|
+
if (args.some((a) => a === "--context-window" || a.startsWith("--context-window="))) return args;
|
|
46
|
+
return [...args, "--context-window", String(declared)];
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
export interface BackendSpec {
|
|
28
50
|
/** registry row id (M6); the builtin seeds carry theirs, spawn does not need it */
|
|
29
51
|
id?: string;
|