agentus 0.8.1 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentus",
3
- "version": "0.8.1",
3
+ "version": "0.9.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": {
@@ -49,7 +49,7 @@ import type {
49
49
  } from "../../../shared/src/index.js";
50
50
  import { acceptsToolWrite, acceptsWriteFrom, clampExplanation, clampTitle, demoteInProgress, foldPlanUpdate, hasUnfinished, normalizeItems, v2PlanEnabled } from "../plan/plan.js";
51
51
  import { PLAN_GUIDANCE_ACTIVE, PLAN_GUIDANCE_INTRO, PLAN_GUIDANCE_NATIVE } from "../../../shared/src/index.js";
52
- import { PREFS_DEFAULT, agentSettings, type PlanHintSettings } from "../settings.js";
52
+ import { AGENT_DEFAULT, PREFS_DEFAULT, agentSettings, type PlanHintSettings } from "../settings.js";
53
53
 
54
54
  // The cockpit CLI an agent is handed (issue #15). Resolved from THIS module's location, so it points
55
55
  // at the same tree the server runs from — a checkout and an installed package alike (`bin/` is in the
@@ -1334,12 +1334,18 @@ export class SessionManager {
1334
1334
  * these three. Rendering it into a prompt, an `echo`, or a `ps` line is the one thing the CLI's own
1335
1335
  * `capabilities` forbids, and the token is worth nothing once this process is gone. */
1336
1336
  #cockpitEnv(env: NodeJS.ProcessEnv, sessionId: string, token: string): NodeJS.ProcessEnv {
1337
+ // 召回条目的两个上限跟着设置一起下发给 agent(issue #37 的容量):provider 读这两个环境变量,
1338
+ // 与 server 的 `statusItemsOf` 同源。只在这一处传,是因为「一轮发多少字」是**宿主**的存储决定,
1339
+ // 不该要求用户去改 agent 自己的 config.json,也不该让两边各有一套默认值。
1340
+ const agent = agentSettings();
1337
1341
  return {
1338
1342
  ...env,
1339
1343
  AGENTUS_COCKPIT_ENDPOINT: `http://127.0.0.1:${process.env.AGENTUS_PORT ?? 8788}`,
1340
1344
  AGENTUS_COCKPIT_TOKEN: token,
1341
1345
  AGENTUS_COCKPIT_SESSION: sessionId,
1342
1346
  AGENTUS_COCKPIT_BIN: COCKPIT_BIN,
1347
+ HINDSIGHT_RECALL_ITEM_CHARS: String(agent.recallItemChars || AGENT_DEFAULT.recallItemChars),
1348
+ HINDSIGHT_RECALL_TOTAL_CHARS: String(agent.recallTotalChars || AGENT_DEFAULT.recallTotalChars),
1343
1349
  };
1344
1350
  }
1345
1351
 
@@ -2765,21 +2771,38 @@ export function visibleBackgroundTasks(tasks: BackgroundTask[]): BackgroundTask[
2765
2771
  *
2766
2772
  * 形状(fork `acp_adapter/events.py:make_status_cb`):与状态句同一条 thought 帧,
2767
2773
  * `items: [{text, tags?, type?}]`、`truncated` 说明 agent 侧已经截断过。**在这里限一次量**
2768
- * 是因为这串东西来自 agent 进程、还要落库:provider 已经按 200 字符/条、6K/轮截过,但那是
2769
- * agent 的承诺,不是我们的保证。上限与前端展开的容量对齐,绝不把一帧 200KB 的 memory 写进 SQLite。
2770
- */
2771
- export const NOTE_ITEMS_MAX = 20;
2772
- export const NOTE_ITEM_CHARS = 300;
2774
+ * 是因为这串东西来自 agent 进程、还要落库:provider 也会截,但那是 agent 的承诺,不是我们的保证,
2775
+ * 上限与前端展开的容量对齐,绝不把一帧 200KB 的 memory 写进 SQLite。
2776
+ *
2777
+ * 闸门是**总字符数**,不是条数(操作者 2026-10-21:「条数不限制,按照 12K 总数来」):按条数截会
2778
+ * 在预算还没用完时就把整条短记忆丢掉,而真正花 wire 和 SQLite 的是字节。两个数都可配
2779
+ * (`agent.recallItemChars` / `agent.recallTotalChars`),并且以环境变量交给 agent 侧的 provider,
2780
+ * 让两边同源 —— 否则界面说 12K、agent 按 6K 发,用户永远对不上。 */
2781
+ export const NOTE_ITEM_CHARS = AGENT_DEFAULT.recallItemChars;
2782
+ export const NOTE_TOTAL_CHARS = AGENT_DEFAULT.recallTotalChars;
2773
2783
 
2774
2784
  export function statusItemsOf(u: Record<string, unknown>): { items: StatusNoteItem[]; truncated: boolean } | undefined {
2775
2785
  const raw = hermesPayload(u).items;
2776
2786
  if (!Array.isArray(raw)) return undefined;
2787
+ const caps = agentSettings();
2788
+ const itemCap = caps.recallItemChars > 0 ? caps.recallItemChars : NOTE_ITEM_CHARS;
2789
+ const totalCap = caps.recallTotalChars > 0 ? caps.recallTotalChars : NOTE_TOTAL_CHARS;
2777
2790
  const items: StatusNoteItem[] = [];
2791
+ let used = 0;
2792
+ let cut = false;
2778
2793
  for (const entry of raw) {
2779
- if (items.length >= NOTE_ITEMS_MAX) break;
2780
2794
  const text = typeof entry === "object" && entry ? (entry as { text?: unknown }).text : undefined;
2781
2795
  if (typeof text !== "string" || !text.trim()) continue;
2782
- const item: StatusNoteItem = { text: text.trim().slice(0, NOTE_ITEM_CHARS) };
2796
+ let body = text.trim();
2797
+ if (body.length > itemCap) {
2798
+ body = body.slice(0, itemCap);
2799
+ cut = true;
2800
+ }
2801
+ if (used + body.length > totalCap) {
2802
+ cut = true;
2803
+ break;
2804
+ }
2805
+ const item: StatusNoteItem = { text: body };
2783
2806
  const tags = (entry as { tags?: unknown }).tags;
2784
2807
  if (Array.isArray(tags)) {
2785
2808
  const clean = tags.filter((t): t is string => typeof t === "string" && !!t).slice(0, 8);
@@ -2788,9 +2811,10 @@ export function statusItemsOf(u: Record<string, unknown>): { items: StatusNoteIt
2788
2811
  const kind = (entry as { type?: unknown }).type;
2789
2812
  if (typeof kind === "string" && kind) item.type = kind;
2790
2813
  items.push(item);
2814
+ used += body.length;
2791
2815
  }
2792
2816
  if (!items.length) return undefined;
2793
- return { items, truncated: hermesPayload(u).truncated === true || raw.length > items.length };
2817
+ return { items, truncated: cut || hermesPayload(u).truncated === true || raw.length > items.length };
2794
2818
  }
2795
2819
 
2796
2820
  function statusKindOf(u: Record<string, unknown>): string | undefined {
@@ -194,15 +194,26 @@ export interface AgentSettings {
194
194
  * agent knows it is there, the operator does not need to. Turned ON, every running process
195
195
  * shows, which is what the fork's original "a snapshot is the only witness" design wanted. */
196
196
  showSilentBackground: boolean;
197
+ /** 记忆召回条目:单条最多多少字符(issue #37 的容量)。These items are a per-turn status frame
198
+ * that also lands in SQLite, so the number is a cost knob, not a formatting preference: it is
199
+ * handed to the agent as `HINDSIGHT_RECALL_ITEM_CHARS`, and the server applies the same limit
200
+ * when it re-caps what arrives. 400 = the operator's 2026-10-21 decision (was 200). */
201
+ recallItemChars: number;
202
+ /** 记忆召回条目:一轮最多多少字符。**这是唯一的条数闸门** —— 不按条数截,短记忆本来就该多
203
+ * 进来几条,按条数截会连预算都没用完就把整条记忆丢掉。Handed to the agent as
204
+ * `HINDSIGHT_RECALL_TOTAL_CHARS`(12K = 操作者 2026-10-21 的决定,原来是 6K)。 */
205
+ recallTotalChars: number;
197
206
  }
198
207
 
199
208
  export const AGENT_DEFAULT: AgentSettings = {
200
209
  idleKillMin: 5, warmSlots: 5, warmTtlHours: 24, cockpitSend: false, completionWake: true,
201
- showSilentBackground: false,
210
+ showSilentBackground: false, recallItemChars: 400, recallTotalChars: 12000,
202
211
  };
203
212
 
204
213
  /** The numeric half of `AgentSettings` — the keys the range-checked helpers may write. */
205
- type AgentNumberKey = "idleKillMin" | "warmSlots" | "warmTtlHours";
214
+ type AgentNumberKey =
215
+ | "idleKillMin" | "warmSlots" | "warmTtlHours" | "recallItemChars" | "recallTotalChars";
216
+
206
217
 
207
218
  /** What the settings page may offer: the ends of the number field. Server-side enforcement uses
208
219
  * the same pair, so a hand-written request cannot store a nonsense value. */
@@ -212,6 +223,11 @@ export const AGENT_IDLE_RANGE: [number, number] = [0, 60];
212
223
  export const AGENT_WARM_RANGE: [number, number] = [0, 20];
213
224
  /** 0 = 不过期; a week is the longest span worth offering on the page. */
214
225
  export const AGENT_WARM_TTL_RANGE: [number, number] = [0, 168];
226
+ /** 召回条目单条字符数:下限是为了别把一条记忆截成半句话,上限是「一整帧别超过一次 prompt 的量」。
227
+ * 与 provider 的默认同值(400),页面改这里,spawn 时用环境变量覆盖 agent 侧同一对常量。 */
228
+ export const AGENT_RECALL_ITEM_RANGE: [number, number] = [80, 2000];
229
+ /** 一轮条目的总字符数。这就是条数闸门本身,所以下限不能太小(2000 ≈ 5 条长记忆)。 */
230
+ export const AGENT_RECALL_TOTAL_RANGE: [number, number] = [2000, 60000];
215
231
 
216
232
  /** 观察智能体 (`工作播报`, issue #16): ONE long-lived ACP session per observed session, spawned by the
217
233
  * cockpit, in its own scratch directory, read-only, out of the rail. It is the thing that turns
@@ -514,6 +530,8 @@ function pickAgent(raw: unknown): AgentSettings {
514
530
  take("idleKillMin", AGENT_IDLE_RANGE);
515
531
  take("warmSlots", AGENT_WARM_RANGE);
516
532
  take("warmTtlHours", AGENT_WARM_TTL_RANGE);
533
+ take("recallItemChars", AGENT_RECALL_ITEM_RANGE);
534
+ take("recallTotalChars", AGENT_RECALL_TOTAL_RANGE);
517
535
  if (typeof r.cockpitSend === "boolean") out.cockpitSend = r.cockpitSend;
518
536
  if (typeof r.completionWake === "boolean") out.completionWake = r.completionWake;
519
537
  if (typeof r.showSilentBackground === "boolean") out.showSilentBackground = r.showSilentBackground;
@@ -631,6 +649,8 @@ export function publicSettings(): Record<string, unknown> {
631
649
  agentIdleRange: AGENT_IDLE_RANGE,
632
650
  agentWarmRange: AGENT_WARM_RANGE,
633
651
  agentWarmTtlRange: AGENT_WARM_TTL_RANGE,
652
+ agentRecallItemRange: AGENT_RECALL_ITEM_RANGE,
653
+ agentRecallTotalRange: AGENT_RECALL_TOTAL_RANGE,
634
654
  observer: cache.observer,
635
655
  observerDefaults: OBSERVER_DEFAULT,
636
656
  observerPromptDefault: OBSERVER_PROMPT_DEFAULT,
@@ -823,6 +843,8 @@ export function saveAgent(patch: Record<string, unknown>): Settings {
823
843
  set("idleKillMin", AGENT_IDLE_RANGE, "minutes", "0 = off");
824
844
  set("warmSlots", AGENT_WARM_RANGE, "slots", "0 = off");
825
845
  set("warmTtlHours", AGENT_WARM_TTL_RANGE, "hours", "0 = never expires");
846
+ set("recallItemChars", AGENT_RECALL_ITEM_RANGE, "chars", "no 0 — a cap must stay a cap");
847
+ set("recallTotalChars", AGENT_RECALL_TOTAL_RANGE, "chars", "no 0 — a cap must stay a cap");
826
848
  // A boolean next to three numbers: spelled out rather than forced through the numeric `set`.
827
849
  if ("cockpitSend" in patch) {
828
850
  if (typeof patch.cockpitSend !== "boolean") throw new Error("invalid cockpitSend: expected true or false");