agentus 0.15.3 → 0.15.4

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 CHANGED
@@ -57,7 +57,7 @@ background process comes back as a turn of its own), an **ACP session that write
57
57
  cockpit-side failure is diagnosable afterwards), and a **DeepSeek web backend** (search +
58
58
  extract on the model key) — come from the fork's integration build instead. That is a five-line install:
59
59
  [§ Which Hermes to run](docs/installation.md#which-hermes-to-run) — currently the tag
60
- `agentus/v0.22.0/merge-20261026-1237`, on Hermes `v0.22.0`.
60
+ `agentus/v0.22.0/merge-20261028-1850`, on Hermes `v0.22.0`.
61
61
 
62
62
  The first slot you start will use the Hermes home **your own agent already uses**
63
63
  (`~/.hermes`) unless the backend row names another one — see "HERMES_HOME" below.
package/README.zh-CN.md CHANGED
@@ -51,7 +51,7 @@ web bundle、配套 APK —— 然后直接退出,不启动任何东西)。
51
51
  以及 **DeepSeek 的 web 后端**(搜索 + 正文抓取,共用模型 key)—— 来自 fork 的集成构建(tag)。
52
52
  装法就五行:
53
53
  [§ 该跑哪份 Hermes](docs/installation.zh-CN.md#该跑哪份-hermes) —— 当前是 tag
54
- `agentus/v0.22.0/merge-20261026-1237`,基座 Hermes `v0.22.0`。
54
+ `agentus/v0.22.0/merge-20261028-1850`,基座 Hermes `v0.22.0`。
55
55
 
56
56
  你启动的第一个 slot 会使用**你自己的 agent 已经在用的**那个 Hermes home(`~/.hermes`),除非后端那一行
57
57
  指定了另一个 —— 见下面的「HERMES_HOME」。
@@ -60,7 +60,7 @@ ref and gets rebuilt, while a tag never moves — so pin the tag. The `agentus/`
60
60
  ours apart from upstream's own tags (`v0.22.0`, `v0.22.0+canary.*`), and the timestamp in the
61
61
  last segment sorts, so "newest" is a `sort -V`.
62
62
 
63
- **Current integration build: `agentus/v0.22.0/merge-20261026-1237`** — the newest tag on the fork,
63
+ **Current integration build: `agentus/v0.22.0/merge-20261028-1850`** — the newest tag on the fork,
64
64
  built on Hermes `v0.22.0`. This line is for orientation only, not the source of truth: the
65
65
  `TAG=$(…)` step below resolves the newest one on every run, so read it there if this sentence
66
66
  ever looks stale.
@@ -52,7 +52,7 @@ agentus/<hermes 内部版本>/merge-<YYYYMMDD-HHMM>
52
52
  tag。`agentus/` 这个前缀也把它和上游自己的 tag(`v0.22.0`、`v0.22.0+canary.*`)分开;最后一段是时间戳,
53
53
  可排序,所以「最新」就是一次 `sort -V`。
54
54
 
55
- **当前集成构建:`agentus/v0.22.0/merge-20261026-1237`** —— fork 上最新的那个 tag,基座 Hermes
55
+ **当前集成构建:`agentus/v0.22.0/merge-20261028-1850`** —— fork 上最新的那个 tag,基座 Hermes
56
56
  `v0.22.0`。这行只为一眼看个大概,不是权威:下面的 `TAG=$(…)` 每次都会自己解析出最新,觉得这行过期就
57
57
  以它为准。
58
58
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentus",
3
- "version": "0.15.3",
4
- "description": "Multi-session web cockpit for ACP-speaking coding agents (Qoder CLI / Hermes). UI has no intelligence \u2014 the agent lives in spawned CLI subprocesses.",
3
+ "version": "0.15.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": {
7
7
  "type": "git",
@@ -949,17 +949,23 @@ const agent = () => ({
949
949
  used: s.used,
950
950
  size: picked > 0 ? picked : Number(process.env.MOCK_USAGE_SIZE || 200_000),
951
951
  });
952
- // The prompt response's `usage`, shaped like the hermes adapter's (issue #70). ACP's semantics
953
- // are "Total … across all turns", so the counters ACCUMULATE on the session state — a cockpit
954
- // that forgot to diff would show the whole conversation on every turn, and a two-turn QA sweep
955
- // is what proves it does not. `cachedWriteTokens` is filled too, because the fork's adapter
956
- // still omits it and the cockpit must not depend on that omission.
952
+ // The prompt response's `usage`, shaped like the hermes adapter's (issue #70, #71). ACP's
953
+ // semantics are "Total … across all turns", so the counters ACCUMULATE on the session state —
954
+ // a cockpit that forgot to diff would show the whole conversation on every turn, and a
955
+ // two-turn QA sweep is what proves it does not.
956
+ //
957
+ // Issue #71: `inputTokens` is the FULL prompt — hermes fills it from `prompt_tokens`, which
958
+ // its own accounting defines as fresh input + cache read + cache write. The mock therefore
959
+ // adds the cache buckets INTO the input counter, exactly as the real adapter does: a mock that
960
+ // kept them separate would let the old 「input + read + write」 denominator stay green.
961
+ // `cachedWriteTokens` is filled too, because the fork's adapter still omits it and the cockpit
962
+ // must not depend on that omission.
957
963
  const outChars = words.join(" ").length + MOCK_MARKDOWN.length;
958
964
  s.tokens = s.tokens || {
959
965
  totalTokens: 0, inputTokens: 0, outputTokens: 0, thoughtTokens: 0,
960
966
  cachedReadTokens: 0, cachedWriteTokens: 0,
961
967
  };
962
- s.tokens.inputTokens += text.length + 200;
968
+ s.tokens.inputTokens += text.length + 200 + 4096 + 128;
963
969
  s.tokens.cachedReadTokens += 4096;
964
970
  s.tokens.cachedWriteTokens += 128;
965
971
  s.tokens.outputTokens += Math.ceil(outChars / 4);
@@ -3574,27 +3574,44 @@ export function turnUsageOf(
3574
3574
  input: input ?? 0,
3575
3575
  output: output ?? 0,
3576
3576
  thought: thought ?? 0,
3577
- cacheRead: cacheRead ?? 0,
3578
- cacheWrite: cacheWrite ?? 0,
3577
+ // issue #71: never `?? 0` here — the baseline is also what a persisted session reloads, and a
3578
+ // washed-in 0 is indistinguishable from a measured zero on the next diff.
3579
+ cacheRead,
3580
+ cacheWrite,
3579
3581
  };
3580
3582
  const delta = (now: number, before: number | undefined): number =>
3581
3583
  (before === undefined ? now : now >= before ? now - before : now);
3584
+ // Issue #71: a bucket the agent never reports stays NULL through the bill. 「写 0」 reads as
3585
+ // "this turn's cache writes were measured at zero", and cache writes are the billed kind
3586
+ // (Anthropic 1.25×). Omission is the honest shape; see tokens.ts. A null BASELINE means the
3587
+ // first reported value is the turn's own — undefined and null never had a prior count.
3588
+ const deltaOpt = (now: number | null, before: number | null | undefined): number | null =>
3589
+ (now === null ? null : delta(now, before ?? undefined));
3590
+ // Computed up front: the hit rate below divides by THIS turn's prompt delta, not the raw one.
3591
+ const billInput = delta(cur.input, base?.input);
3592
+ const billCacheRead = deltaOpt(cur.cacheRead, base?.cacheRead);
3582
3593
  const bill: TurnUsage = {
3583
3594
  total: delta(cur.total, base?.total),
3584
- input: delta(cur.input, base?.input),
3595
+ input: billInput,
3585
3596
  output: delta(cur.output, base?.output),
3586
3597
  thought: delta(cur.thought, base?.thought),
3587
- cacheRead: delta(cur.cacheRead, base?.cacheRead),
3588
- cacheWrite: delta(cur.cacheWrite, base?.cacheWrite),
3598
+ cacheRead: billCacheRead,
3599
+ cacheWrite: deltaOpt(cur.cacheWrite, base?.cacheWrite),
3589
3600
  durationMs,
3590
- // Webui's denominator: the FULL prompt = input + cache read + cache write. Only when the agent
3591
- // reports a cache bucket at all — otherwise the page says 「未上报」, not a 0% hit rate.
3601
+ // Issue #71 — the denominator is this turn's prompt total, i.e. the billed `input` DELTA.
3602
+ // ACP's Usage.inputTokens is the FULL prompt (hermes' adapter fills it with `prompt_tokens`,
3603
+ // which by definition = fresh input + cache read + cache write; see agent/usage_pricing.py in
3604
+ // the fork), so the cache buckets are a SUBSET of input, not peers beside it. Adding them
3605
+ // back counted every cached token twice and pinned the reading at cacheRead/(input+cacheRead)
3606
+ // ≈ 50% for any cache-heavy session — the exact defect #71 measured (real hits 91–100%, page
3607
+ // 50%). webui keeps one invariant across both of its sources: the denominator is a number
3608
+ // that already IS the full prompt (api/models.py passes input_tokens alone); its additive
3609
+ // form exists only for a rollup that stored uncached input separately. We read the first
3610
+ // source, so no addition. `min(100, …)` stays as a clamp against a skewed diff, never as the
3611
+ // thing that makes the ratio believable: with correct semantics normal data cannot exceed 100.
3592
3612
  cacheHitPercent: cacheRead === null && cacheWrite === null
3593
3613
  ? null
3594
- : (() => {
3595
- const prompt = (input ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
3596
- return prompt > 0 ? Math.min(100, Math.round(((cacheRead ?? 0) / prompt) * 100)) : null;
3597
- })(),
3614
+ : (billInput > 0 ? Math.min(100, Math.round(((billCacheRead ?? 0) / billInput) * 100)) : null),
3598
3615
  };
3599
3616
  // Average speed: this turn's OWN output over wall time (tool calls included, so a long tool
3600
3617
  // turn honestly reads slow rather than being dressed up as model speed).
@@ -140,29 +140,34 @@ export interface UsageView {
140
140
  totals?: UsageTotals | null;
141
141
  }
142
142
 
143
- /** Cumulative token buckets a turn's usage is measured against (ACP `Usage`). */
143
+ /** Cumulative token buckets a turn's usage is measured against (ACP `Usage`).
144
+ * `cacheRead`/`cacheWrite` are null = the agent never reports that bucket (issue #71); a
145
+ * baseline of null diffs the first REPORTED value as the turn's own, same as undefined. */
144
146
  export interface UsageTotals {
145
147
  total: number;
146
148
  input: number;
147
149
  output: number;
148
150
  thought: number;
149
- cacheRead: number;
150
- cacheWrite: number;
151
+ cacheRead: number | null;
152
+ cacheWrite: number | null;
151
153
  }
152
154
 
153
155
  /** One turn's token bill (issue #70) — the DELTA of the agent's cumulative counters.
154
156
  *
155
- * `cacheHitPercent` uses the webui's definition (prompt cache reads over the full prompt total:
156
- * input + cache read + cache write), and is null when the agent reports no cache buckets at all —
157
- * a Qoder session must read as unavailable, not as a 0% hit rate. `tps` is the turn AVERAGE
157
+ * `cacheHitPercent` is cacheRead / input, where ACP's `inputTokens` is ALREADY the full prompt
158
+ * (fresh + cache read + cache write — issue #71; do not add the buckets back, it counts cached
159
+ * tokens twice and pins the reading near 50%). Null when the agent reports no cache buckets at
160
+ * all — a Qoder session must read as unavailable, not as a 0% hit rate. `tps` is the turn AVERAGE
158
161
  * (output tokens over wall time, tool calls included); it is deliberately labelled as an average
159
162
  * rather than sold as instantaneous speed. */
160
163
  export interface TurnUsage {
161
164
  input: number;
162
165
  output: number;
163
166
  thought: number;
164
- cacheRead: number;
165
- cacheWrite: number;
167
+ /** NULL = the agent never reports this bucket (issue #71). A reported 0 is a real 0 — the
168
+ * two must not collapse into one number, or 「写 0」 lies about cache writes (1.25× billed). */
169
+ cacheRead: number | null;
170
+ cacheWrite: number | null;
166
171
  total: number;
167
172
  durationMs?: number;
168
173
  cacheHitPercent?: number | null;