dsh-autoctx 1.2.4 → 1.2.5
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.en.md +48 -27
- package/README.md +48 -27
- package/briefs.js +24 -2
- package/cachespace.js +49 -0
- package/index.js +397 -14
- package/package.json +2 -1
- package/stats.js +10 -2
package/README.en.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# dsh-autoctx
|
|
2
2
|
|
|
3
|
-
Context and subagent decision plugin for dsh, version **1.2.
|
|
3
|
+
Context and subagent decision plugin for dsh, version **1.2.5**. Jev scores options and makes structured choices; the plugin implements them through native dsh sessions, tools, subagents and compaction. The executor model still does all the coding.
|
|
4
4
|
|
|
5
5
|
[中文](https://github.com/ZhiweiWei-NAMI/dsh-autoctx/blob/main/README.md) · [Source](https://github.com/ZhiweiWei-NAMI/dsh-autoctx) · MIT
|
|
6
6
|
|
|
7
7
|
## Environment and accounts
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Unit tests pass on **dsh `0.2.0-rc.2` and `0.2.1-rc.2`**, using Node 22; the benchmark below ran on `0.2.0-rc.2`, and real smoke runs were verified on `0.2.1-rc.2`. Peer dependencies for the `@deepseek-ai/dsh-*` packages accept `^0.1.5-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1`. 1.2.5 decides thinking/effort at decision points per trigger, using evidence and a cost estimate per option, as detailed under “How it works”; compatibility with dsh 0.2.1's subagent start API (`startActivation`) and 0.2.0 is kept. The npm package is `dsh-autoctx`, its entry is `index.js`, and its web client is `./client.js`.
|
|
10
10
|
|
|
11
11
|
You must provide:
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ Without `apiKey`, the plugin makes no network call, the session runs in native m
|
|
|
31
31
|
```sh
|
|
32
32
|
dsh plugin --profile autoctx add dsh-autoctx
|
|
33
33
|
# Or use the release tag
|
|
34
|
-
dsh plugin --profile autoctx add github:ZhiweiWei-NAMI/dsh-autoctx#v1.2.
|
|
34
|
+
dsh plugin --profile autoctx add github:ZhiweiWei-NAMI/dsh-autoctx#v1.2.5
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
`dsh plugin --profile <name> <pnpm-args...>` passes arguments to pnpm. Profiles live in `$DSH_HOME/profiles/<name>`. The package's declared dsh bundle patch (`cordis.patch.yml`) mounts the plugin and disables native `compaction-basic`; no duplicate manual mount is needed.
|
|
@@ -61,7 +61,7 @@ Upgrade to the latest version, or pin this version:
|
|
|
61
61
|
```sh
|
|
62
62
|
dsh plugin --profile autoctx add dsh-autoctx@latest
|
|
63
63
|
# Or pin the version
|
|
64
|
-
dsh plugin --profile autoctx add dsh-autoctx@1.2.
|
|
64
|
+
dsh plugin --profile autoctx add dsh-autoctx@1.2.5
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Before uninstalling, delete any `- id: dsh-autoctx` override you added to the profile's `cordis.patch.yml`, then run:
|
|
@@ -91,9 +91,13 @@ In the web UI, open bottom-left **Settings → DSH AutoCTX**, or **Plugins → d
|
|
|
91
91
|
| `advisorMaxConsults` | `1`; advisor consultation cap |
|
|
92
92
|
| `advisorStallSteps` | `6`; stall step threshold |
|
|
93
93
|
| `adaptiveEffort` | `high`; effort used when Jev's thinking policy calls for it; `off` keeps the default effort |
|
|
94
|
+
| `effortSwitchFreeTokens` | `4096`; a decision-point thinking switch whose estimated uncached re-read is at most this proceeds without asking Jev |
|
|
95
|
+
| `maxEffortDecisions` | `6`; separate Jev effort-decision calls per root task; never spends `maxDecisions` |
|
|
96
|
+
| `effortPhaseRequests` | `4`; maximum high-effort requests in a thinking phase; a new trigger renews the window |
|
|
97
|
+
| `cacheTtlSeconds` | `300`; assumed cache-reuse lifetime used in runtime estimates; the provider TTL is unverified |
|
|
94
98
|
| `learning` | `frozen`; choices: `auto`, `manual`, `frozen` |
|
|
95
99
|
| `contextSelection` | `jev`; choices: `jev`, `summary` |
|
|
96
|
-
| `maxDecisions` | `12`; Jev decision calls per root task |
|
|
100
|
+
| `maxDecisions` | `12`; Jev routing/context decision calls per root task |
|
|
97
101
|
| `maxChildren` | `2`; child count cap |
|
|
98
102
|
| `maxConcurrent` | `2`; concurrent child count cap |
|
|
99
103
|
| `maxChildRequests` | `16`; child request cap |
|
|
@@ -106,7 +110,9 @@ In the web UI, open bottom-left **Settings → DSH AutoCTX**, or **Plugins → d
|
|
|
106
110
|
|
|
107
111
|
**Decisions.** A plain single agent keeps one growing context; native subagents are chosen by the executor through spawn/fork, and native compaction summarizes old context. AutoCTX asks Jev for structured decisions at specific points. Jev is a scoring/choice model; it does not write plans or code. It decides whether a proposed delegation is worth a child and which route to use; gives a task tag and thinking policy at turn start; selects evidence under context pressure and chooses whether to replace context now or later, weighing the cost of re-reading uncached content against smaller later requests; and selects reviewer/advisor models when those roles are set to `auto`.
|
|
108
112
|
|
|
109
|
-
Thinking policies are `off`, `decision_points`, or `always`; decision points are turn start, test failure, review/advisor report and stall.
|
|
113
|
+
Thinking policies are `off`, `decision_points`, or `always`; decision points are turn start, test failure, review/advisor report and stall. In 1.2.5, each trigger supplies evidence such as a failing-test output excerpt, diff hunk, review/advisor report text or task head, with an estimated cost per option at catalog prices: `skip` stays in the current cache space; `pulse` makes one high-effort request; `phase` keeps high effort until the trigger is resolved (test passes, report acted on, progress) or `effortPhaseRequests` requests. Cheap switches (estimated uncached re-read ≤ `effortSwitchFreeTokens`) and switches right after a context replacement proceed without asking Jev. Effort questions use their own `maxEffortDecisions` budget and never spend the 12 routing/context decisions in `maxDecisions`. Cache reuse is estimated from exact request prefixes, with no rewriting, reordering or dropping of context for cache; these runtime estimates are theoretical, not actual bills.
|
|
114
|
+
|
|
115
|
+
A transient Jev gateway/network failure is retried once, then Jev pauses for 60 seconds. Authentication, billing or HTTP 429 stops Jev for the task, and local fallbacks take over: native compaction and no delegation.
|
|
110
116
|
|
|
111
117
|
**Context selection.** With `contextSelection: jev`, when context passes `pressureTokens`, the old region is split into evidence pieces—test-failure sections, diff hunks, code by function/class and paragraphs—rather than whole outputs. Jev scores how much each piece adds given the stated open problem, such as which test still fails, and the content kept regardless. Locally, duplicates keep one copy; a kept report pulls in the definition it refers to; pieces that add little are archived as one line each. At `pressureTokens × ceilingRatio`, the session must shrink, using native compaction if needed. `summary` lets the executor summarize instead.
|
|
112
118
|
|
|
@@ -124,42 +130,56 @@ Automatic learning is expected to maintain quality while lowering cost over time
|
|
|
124
130
|
|
|
125
131
|
## Extra calls and cost
|
|
126
132
|
|
|
127
|
-
Jev calls, reviewer/advisor children and their executor-model calls, and the turn-end learning call all cost money on your own API accounts. These are not free services. The model table in `models.json` lists `jev-1.13` at **$0.042 per 1 million input tokens, $0 for output tokens**; `jev-1.13-free` is rate limited. Benchmark costs below are estimates at
|
|
133
|
+
Jev calls, reviewer/advisor children and their executor-model calls, and the turn-end learning call all cost money on your own API accounts. These are not free services. The model table in `models.json` lists catalog prices for `jev-1.13` at **$0.042 per 1 million input tokens, $0 for output tokens**; `jev-1.13-free` is rate limited. Benchmark costs below are theoretical estimates at catalog prices, not actual bills (defined in the benchmark section). Over the 72 cases, 1.2.5 makes 368 Jev calls versus 233 for 1.2.3 frozen; these calls also count toward a task's request limit.
|
|
128
134
|
|
|
129
135
|
## Benchmark results and limitations
|
|
130
136
|
|
|
131
137
|
These results describe this run, not general superiority. Every arm uses the same **72 graded cases = 12 fixed SWE-bench Verified pytest tasks (`bench/tasks.json`) × 3 executor models × 2 runs per model**. Executors are `deepseek-v4.1-flash`, `glm-5.3-flash` and `deepseek-v4-flash`. The official SWE-bench harness **v4.1.0** decides resolved; a finished run or HTTP 200 is not a pass.
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
Shared limits are **600 seconds per task, 64 model requests per task (Jev included), case parallelism 3, at most 2 children / 2 concurrent children**. AutoCTX and H used a $0.10 per-case spend cap; B and C used the harness default $0.50. No case in any arm exceeded $0.066 (theoretical, at catalog prices), so the spend cap never bound.
|
|
134
140
|
|
|
135
141
|
| Arm | Configuration |
|
|
136
142
|
| --- | --- |
|
|
137
143
|
| B | Single agent; no subagent tools, native compaction |
|
|
138
144
|
| C | Native spawn/fork with the executor model only; native compaction, no reviewer/advisor/extra models |
|
|
139
145
|
| H | Native heterogeneous subagents; one native subagent tool per (model, effort) over the executor plus 3 extra models, chosen by the executor |
|
|
140
|
-
| AutoCTX frozen
|
|
146
|
+
| AutoCTX 1.2.5 frozen | New release; reviewer `auto`, advisor `auto`, the same 3 extra models, `jev-1.13`; frozen learning |
|
|
147
|
+
| AutoCTX 1.2.3 frozen / auto | Previous release, same harness; reviewer `auto`, advisor `auto`, the same 3 extra models, `jev-1.13`; frozen and auto learning respectively; auto carries the ledger across cases within a run |
|
|
148
|
+
|
|
149
|
+
The 3 extra models are `glm-5.3-flash` (use `deepseek-v4.1-flash` when the executor is glm), `deepseek-v4-pro` and `claude-haiku-5-5`. 1.2.5 ran on dsh `0.2.0-rc.2`, the same host version as 1.2.3/B/C/H. B/C/H do not load the plugin and were run before 1.2.3. B/C used the same harness entry as AutoCTX; H run 1 used an earlier harness snapshot (its `run.py` differs) with the same tasks and shared limits above.
|
|
150
|
+
|
|
151
|
+
**Theoretical cost basis.** All cost and cache figures below are **theoretical estimates at catalog prices, not actual bills**. Provider-reported cache hits vary with routing, accounts and eviction, so all arms are compared on the prompt cache they could reuse given exactly what they sent, with identical assumptions and prices: a request reuses the longest prefix it shares with an earlier request of the same task in the same cache space (model + thinking/effort config [+ tools]); no expiry within a task. Uncached input uses the catalog input price (cache-write price where listed), reused input the cache-read price, and output the output price; Jev is $0.042/M input, output free; executor, child and Jev costs are all included. This benchmark's no-expiry-within-task assumption differs from the runtime lifetime assumption in `cacheTtlSeconds`.
|
|
152
|
+
|
|
153
|
+
Two measurement methods: `exact` uses per-message prefix hashes recorded by the bench observer (AutoCTX 1.2.x runs). B/C/H were recorded before prefix hashes existed, so they use a `log` reconstruction (append-only growth per session; an input drop marks a context rewrite). Validation by re-running B and C on 12 tasks with `deepseek-v4.1-flash` and prefix hashes (24 cases): `log` gave 2.4% (B) and 3.1% (C) lower cost than `exact`; on AutoCTX it gives about 11% lower cost, because context replacement changes earlier prompt content without necessarily shrinking it. The baseline theoretical figures below are therefore probably ~3% low, and the AutoCTX premium is slightly overstated rather than understated.
|
|
154
|
+
|
|
155
|
+
Costs and cache rates in this table are theoretical estimates, not actual bills.
|
|
156
|
+
|
|
157
|
+
| Metric (72 cases per arm) | AutoCTX 1.2.5 frozen | AutoCTX 1.2.3 frozen | AutoCTX 1.2.3 auto | B | C | H |
|
|
158
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
159
|
+
| Resolved / 72 | 53/72 | 52/72 | 49/72 | 39/72 | 44/72 | 42/72 |
|
|
160
|
+
| Paired wins/losses of 1.2.5 frozen vs this arm (exact McNemar p) | — | 6/5 (p=1.0) | — | 15/1 (p=0.001) | 12/3 (p=0.035) | 13/2 (p=0.007) |
|
|
161
|
+
| Theoretical cost, total USD (method) | $1.738 (exact) | $1.810 (exact) | $1.603 (exact) | $0.908 (log) | $0.975 (log) | $1.027 (log) |
|
|
162
|
+
| Of which theoretical cost: executor / children / Jev (USD) | 1.383 / 0.273 / 0.082 | 1.451 / 0.284 / 0.076 | 1.321 / 0.214 / 0.068 | 0.805 / 0.102 / 0 | 0.844 / 0.131 / 0 | 0.867 / 0.160 / 0 |
|
|
163
|
+
| Theoretical cost per resolved case | $0.0328 | $0.0348 | $0.0327 | $0.0233 | $0.0222 | $0.0244 |
|
|
164
|
+
| Theoretical executor cache rate | 87.0% | 85.4% | 86.5% | 94.8% | 95.1% | 95.1% |
|
|
165
|
+
| Wall time sum (seconds) | 17580 | 18060 | 17670 | 17454 | 16520 | 17076 |
|
|
166
|
+
| Jev calls ok/all | 367/368 | 233/233 | 260/260 | — | — | — |
|
|
141
167
|
|
|
142
|
-
|
|
168
|
+
With the `log` method applied to every arm, the theoretical total is $1.547 for AutoCTX 1.2.5 frozen versus B $0.908, C $0.975 and H $1.027.
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
146
|
-
| Resolved / 72 | 52/72 | 49/72 | 39/72 | 44/72 | 42/72 |
|
|
147
|
-
| Estimated API catalog cost, executor + children + Jev (USD) | $1.514 | $1.374* | $0.876 | $0.933 | $1.018 |
|
|
148
|
-
| Jev portion of the above cost (USD) | $0.076 | $0.068* | N/A | N/A | N/A |
|
|
149
|
-
| Wall time sum (seconds) | 18060 | 17670 | 17454 | 16520 | 17076 |
|
|
150
|
-
| Prompt cache hit rate | 87.5% | 88.4% | 94.3% | 94.6% | 93.9% |
|
|
170
|
+
AutoCTX resolves more cases than B, C and H on this sample and costs more; on theoretical cost, 1.2.5 frozen is about 70–80% more than C overall and ~48% more per resolved case. Comparing 1.2.5 with 1.2.3 frozen, lane-matched and `exact`: 53 vs 52 resolved (6 wins / 5 losses, no quality difference); theoretical total cost is down 4.0% ($1.738 vs $1.810), theoretical executor cache rate is 87.0% vs 85.4%, and theoretical Jev cost is up $0.006.
|
|
151
171
|
|
|
152
|
-
|
|
172
|
+
1.2.5 effort decisions across 72 cases: 201 cheap (no Jev call), 86 skip, 51 phase, 9 pulse, 11 after context replacement, and 11 fell back after the effort budget was spent; 19 cases reached the 64-request limit (1.2.3: 18). The limit counts Jev calls, so AutoCTX reaches it with fewer executor requests than the baselines: 10 of those 19 cases were unresolved, and Jev made 77 of their 640 requests (12%). B/C/H also hit the limit (17/20/16 cases), with executor requests only. This benchmark keeps the shared limit; a rerun excluding Jev from it has not been done.
|
|
153
173
|
|
|
154
|
-
| Executor (24 cases per model per arm; resolved / 24) |
|
|
155
|
-
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
156
|
-
| deepseek-v4.1-flash | 17/24 | 16/24 | 11/24 | 15/24 | 14/24 |
|
|
157
|
-
| glm-5.3-flash | 18/24 | 18/24 | 17/24 | 18/24 | 18/24 |
|
|
158
|
-
| deepseek-v4-flash | 17/24 | 15/24 | 11/24 | 11/24 | 10/24 |
|
|
174
|
+
| Executor (24 cases per model per arm; resolved / 24) | AutoCTX 1.2.5 frozen | AutoCTX 1.2.3 frozen | AutoCTX 1.2.3 auto | B | C | H |
|
|
175
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
176
|
+
| deepseek-v4.1-flash | 18/24 | 17/24 | 16/24 | 11/24 | 15/24 | 14/24 |
|
|
177
|
+
| glm-5.3-flash | 17/24 | 18/24 | 18/24 | 17/24 | 18/24 | 18/24 |
|
|
178
|
+
| deepseek-v4-flash | 18/24 | 17/24 | 15/24 | 11/24 | 11/24 | 10/24 |
|
|
159
179
|
|
|
160
|
-
In the auto run, 49 completed turns reached learning: 35/49 were judged unassessable and left weights unchanged; 14/49 applied small updates. Auto did not outperform frozen here.
|
|
180
|
+
In the auto run, 49 completed turns reached learning: 35/49 were judged unassessable and left weights unchanged; 14/49 applied small updates. Auto did not outperform frozen here.
|
|
161
181
|
|
|
162
|
-
Only these 12 pytest tasks and 3 flash executors were measured; between repeats, resolved counts vary by about ±2 cases per 12-task run. AutoCTX costs more than the baselines here; auto learning remains beta. There is also a host limitation: a fork created mid-turn is seeded only with completed turns, so the review brief must carry the current turn's evidence itself.
|
|
182
|
+
Only these 12 pytest tasks and 3 flash executors were measured; between repeats, resolved counts vary by about ±2 cases per 12-task run. AutoCTX costs more than the baselines here on the theoretical cost basis; auto learning remains beta and off by default. There is also a host limitation: a fork created mid-turn is seeded only with completed turns, so the review brief must carry the current turn's evidence itself.
|
|
163
183
|
|
|
164
184
|
## Reproduce
|
|
165
185
|
|
|
@@ -173,10 +193,11 @@ python3 bench/run.py --model deepseek-v4.1-flash --arms B,C --tasks bench/tasks.
|
|
|
173
193
|
python3 bench/run.py --model deepseek-v4.1-flash --arms H --tasks bench/tasks.json --out <dir> --plugin . --parallel 3 \
|
|
174
194
|
--extra-models glm-5.3-flash,deepseek-v4-pro,claude-haiku-5-5 --max-usd 0.10
|
|
175
195
|
python3 bench/grade.py <dir> --parallel 3
|
|
196
|
+
python3 bench/theoretical_cost.py "A=<dir>/*/A" "B=<dir>/*/B" "C=<dir>/*/C"
|
|
176
197
|
```
|
|
177
198
|
|
|
178
|
-
Replace `<dir>` with an output directory. A is AutoCTX; use separate directories for frozen and auto. Run for all 3 executor models, with 2 runs per model and separate directories for independent runs. For `glm-5.3-flash`, change `--extra-models` to `deepseek-v4.1-flash,deepseek-v4-pro,claude-haiku-5-5`; `deepseek-v4-flash` uses the extra models shown above. Re-running the same command resumes unfinished cases. `--max-usd 0.10` is the bench observer's $0.10 per-case spend cap.
|
|
199
|
+
Replace `<dir>` with an output directory. A is AutoCTX; use separate directories for frozen and auto. Run for all 3 executor models, with 2 runs per model and separate directories for independent runs. For `glm-5.3-flash`, change `--extra-models` to `deepseek-v4.1-flash,deepseek-v4-pro,claude-haiku-5-5`; `deepseek-v4-flash` uses the extra models shown above. Re-running the same command resumes unfinished cases. `--max-usd 0.10` is the bench observer's $0.10 per-case spend cap. `bench/theoretical_cost.py` estimates cost and cache reuse with the method above; its outputs are theoretical estimates, not actual bills.
|
|
179
200
|
|
|
180
201
|
## Upgrading from older versions
|
|
181
202
|
|
|
182
|
-
Install 1.2.
|
|
203
|
+
Install 1.2.5 using the upgrade steps above and check profile overrides against the current configuration table. Do not carry over the old README's default model or installation tag: the current default is billed `jev-1.13`, and the release tag is `v1.2.5`.
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# dsh-autoctx
|
|
2
2
|
|
|
3
|
-
面向 dsh 的上下文与子代理决策插件,版本 **1.2.
|
|
3
|
+
面向 dsh 的上下文与子代理决策插件,版本 **1.2.5**。Jev 负责评分和结构化选择,插件通过 dsh 原生会话、工具、子代理和压缩执行决策;编码仍由执行模型完成。
|
|
4
4
|
|
|
5
5
|
[English](https://github.com/ZhiweiWei-NAMI/dsh-autoctx/blob/main/README.en.md) · [源码](https://github.com/ZhiweiWei-NAMI/dsh-autoctx) · MIT
|
|
6
6
|
|
|
7
7
|
## 环境与所需账号
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
单元测试通过的宿主为 **dsh `0.2.0-rc.2` 与 `0.2.1-rc.2`**,使用 Node 22;下方基准运行于 `0.2.0-rc.2`,真实 smoke 运行已在 `0.2.1-rc.2` 验证。`@deepseek-ai/dsh-*` 包的 peer dependency 范围为 `^0.1.5-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1`。1.2.5 按每次触发的证据和各选项成本估算决定决策点的思考强度,细节见「工作方式」;保留对 dsh 0.2.1 子代理启动接口(`startActivation`)及 0.2.0 的兼容。npm 包名为 `dsh-autoctx`,入口为 `index.js`,Web 客户端为 `./client.js`。
|
|
10
10
|
|
|
11
11
|
你需要提供:
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
```sh
|
|
32
32
|
dsh plugin --profile autoctx add dsh-autoctx
|
|
33
33
|
# 或使用发布标签
|
|
34
|
-
dsh plugin --profile autoctx add github:ZhiweiWei-NAMI/dsh-autoctx#v1.2.
|
|
34
|
+
dsh plugin --profile autoctx add github:ZhiweiWei-NAMI/dsh-autoctx#v1.2.5
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
`dsh plugin --profile <name> <pnpm-args...>` 把参数传给 pnpm。Profile 位于 `$DSH_HOME/profiles/<name>`。包声明的 dsh bundle patch(`cordis.patch.yml`)会挂载插件并停用原生 `compaction-basic`,无需手工重复挂载。
|
|
@@ -61,7 +61,7 @@ dsh --profile autoctx --patch "$DSH_HOME/profiles/autoctx/node_modules/dsh-autoc
|
|
|
61
61
|
```sh
|
|
62
62
|
dsh plugin --profile autoctx add dsh-autoctx@latest
|
|
63
63
|
# 或固定版本
|
|
64
|
-
dsh plugin --profile autoctx add dsh-autoctx@1.2.
|
|
64
|
+
dsh plugin --profile autoctx add dsh-autoctx@1.2.5
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
卸载前,先删除自己在 profile 的 `cordis.patch.yml` 中添加的 `- id: dsh-autoctx` 覆盖项,再执行:
|
|
@@ -91,9 +91,13 @@ Web 界面:左下角 **Settings → DSH AutoCTX**,或 **Plugins → dsh-auto
|
|
|
91
91
|
| `advisorMaxConsults` | `1`;顾问咨询上限 |
|
|
92
92
|
| `advisorStallSteps` | `6`;停滞步数阈值 |
|
|
93
93
|
| `adaptiveEffort` | `high`;Jev 思考策略要求时使用的 effort;`off` 保留默认 effort |
|
|
94
|
+
| `effortSwitchFreeTokens` | `4096`;决策点切换思考强度时,预计未缓存重读量不超过此值则不询问 Jev |
|
|
95
|
+
| `maxEffortDecisions` | `6`;每个根任务独立的 Jev 思考决策调用上限;不占用 `maxDecisions` |
|
|
96
|
+
| `effortPhaseRequests` | `4`;思考阶段内高强度请求上限;新触发会续期 |
|
|
97
|
+
| `cacheTtlSeconds` | `300`;运行时估算假设的缓存复用寿命;提供方 TTL 未验证 |
|
|
94
98
|
| `learning` | `frozen`;可选 `auto`、`manual`、`frozen` |
|
|
95
99
|
| `contextSelection` | `jev`;可选 `jev`、`summary` |
|
|
96
|
-
| `maxDecisions` | `12`;每个根任务的 Jev
|
|
100
|
+
| `maxDecisions` | `12`;每个根任务的 Jev 路由/上下文决策调用上限 |
|
|
97
101
|
| `maxChildren` | `2`;子代理数量上限 |
|
|
98
102
|
| `maxConcurrent` | `2`;并发子代理数量上限 |
|
|
99
103
|
| `maxChildRequests` | `16`;子代理请求上限 |
|
|
@@ -106,7 +110,9 @@ Web 界面:左下角 **Settings → DSH AutoCTX**,或 **Plugins → dsh-auto
|
|
|
106
110
|
|
|
107
111
|
**决策。** 普通单代理持续累积一份上下文;原生子代理由执行模型自行选择 spawn/fork,原生压缩总结旧上下文。AutoCTX 在特定节点向 Jev 请求结构化决策。Jev 是评分/选择模型,不编写计划或代码。它判断拟议委派是否值得创建子代理及采用哪条路由;在回合开始给出任务标签与思考策略;在上下文压力下选择证据并判断现在还是稍后替换上下文,权衡重新读取未缓存内容的成本与后续请求缩小的收益;当角色设为 `auto` 时选择 reviewer/advisor 模型。
|
|
108
112
|
|
|
109
|
-
思考策略为 `off`、`decision_points` 或 `always
|
|
113
|
+
思考策略为 `off`、`decision_points` 或 `always`;决策点包括回合开始、测试失败、审查/顾问报告和停滞。1.2.5 为每次触发提供失败测试输出摘录、diff hunk、审查/顾问报告文本或任务开头等证据,并按目录价给出各选项的估算成本:`skip` 留在当前缓存空间;`pulse` 仅一次高强度请求;`phase` 保持高强度,直到触发问题解决(测试通过、报告已处理、取得进展)或达到 `effortPhaseRequests` 次请求。预计未缓存重读量不超过 `effortSwitchFreeTokens` 的低成本切换,以及紧接上下文替换后的切换,不询问 Jev。思考决策使用独立的 `maxEffortDecisions`,不占用 `maxDecisions` 的 12 次路由/上下文决策。缓存复用按实际请求的精确前缀估算,不为缓存改写、重排或丢弃上下文;这些运行时估算均为理论值,非实际账单。
|
|
114
|
+
|
|
115
|
+
临时 Jev 网关/网络失败会重试一次,随后暂停 Jev 60 秒;认证、计费或 HTTP 429 会停止该任务的 Jev 调用,由本地回退接管:原生压缩、不委派。
|
|
110
116
|
|
|
111
117
|
**上下文选择。** `contextSelection: jev` 在上下文超过 `pressureTokens` 时,把旧区域拆成测试失败段、diff hunk、按函数/类划分的代码和段落等证据片段,而非整块输出。Jev 根据明确的待解决问题(例如仍失败的测试)以及必留内容,评估每片证据额外提供了什么。插件本地去重只保留一份;保留的报告会带入其引用的定义;增益低的片段各归档为一行。达到 `pressureTokens × ceilingRatio` 时必须缩减,必要时使用原生压缩。`summary` 则由执行模型总结。
|
|
112
118
|
|
|
@@ -124,42 +130,56 @@ Web 界面:左下角 **Settings → DSH AutoCTX**,或 **Plugins → dsh-auto
|
|
|
124
130
|
|
|
125
131
|
## 额外调用与费用
|
|
126
132
|
|
|
127
|
-
Jev 调用、reviewer/advisor 子代理及其执行模型调用、回合结束时的学习调用,均由用户自己的 API 账号承担费用,不是免费服务。`models.json` 模型表中 `jev-1.13`
|
|
133
|
+
Jev 调用、reviewer/advisor 子代理及其执行模型调用、回合结束时的学习调用,均由用户自己的 API 账号承担费用,不是免费服务。`models.json` 模型表中 `jev-1.13` 的目录价为 **每 100 万输入 tokens $0.042,输出 tokens $0**;`jev-1.13-free` 有限流。下方基准费用均为按目录价的理论估算,非实际账单(定义见基准一节)。1.2.5 在 72 个案例中调用 Jev 368 次,1.2.3 frozen 为 233 次;这些调用也占用任务请求上限。
|
|
128
134
|
|
|
129
135
|
## 基准结果与限制
|
|
130
136
|
|
|
131
137
|
仅报告本次运行,不代表普遍优势。每组使用相同的 **72 个评分案例 = 12 个固定 SWE-bench Verified pytest 任务(`bench/tasks.json`)× 3 个执行模型 × 每模型 2 次运行**。执行模型为 `deepseek-v4.1-flash`、`glm-5.3-flash`、`deepseek-v4-flash`。官方 SWE-bench harness **v4.1.0** 判定 resolved;运行完成或 HTTP 200 均不等于通过。
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
共同限制为:**每任务 600 秒、每任务 64 个模型请求(包含 Jev 调用)、案例并行数 3、最多 2 个子代理/2 个并发子代理**。AutoCTX 与 H 的每案例花费上限为 $0.10,B 与 C 使用 harness 默认的 $0.50;各组均无案例的理论费用(目录价)超过 $0.066,因此花费上限未起约束作用。
|
|
134
140
|
|
|
135
141
|
| 组别 | 配置 |
|
|
136
142
|
| --- | --- |
|
|
137
143
|
| B | 单代理;无子代理工具,原生压缩 |
|
|
138
144
|
| C | 原生 spawn/fork,仅执行模型;原生压缩,无 reviewer/advisor/额外模型 |
|
|
139
145
|
| H | 原生异构子代理;在执行模型及额外 3 个模型的每个(模型,effort)组合上提供一个原生子代理工具,由执行模型选择 |
|
|
140
|
-
| AutoCTX frozen
|
|
146
|
+
| AutoCTX 1.2.5 frozen | 新版本;reviewer `auto`、advisor `auto`、相同的额外 3 个模型、`jev-1.13`;frozen 学习 |
|
|
147
|
+
| AutoCTX 1.2.3 frozen / auto | 上一版本,相同 harness;reviewer `auto`、advisor `auto`、相同的额外 3 个模型、`jev-1.13`;分别使用 frozen 和 auto 学习;auto 在同一次运行的案例间沿用账本 |
|
|
148
|
+
|
|
149
|
+
额外 3 个模型为 `glm-5.3-flash`(执行模型为它时改用 `deepseek-v4.1-flash`)、`deepseek-v4-pro`、`claude-haiku-5-5`。1.2.5 与 1.2.3/B/C/H 均运行于 dsh `0.2.0-rc.2`。B/C/H 不加载插件,均早于 1.2.3 运行。B/C 与 AutoCTX 使用同一 harness 入口;H 第 1 轮使用更早的 harness 快照(`run.py` 有差异),任务和上述共同限制相同。
|
|
150
|
+
|
|
151
|
+
**理论费用依据。** 下文所有费用与缓存数据均为**按目录价的理论估算,非实际账单**。提供方报告的缓存命中受路由、账号和驱逐影响,因此各组按实际发送的 prompt 可复用的缓存,以相同假设和价格比较:请求复用同任务、同缓存空间(模型 + thinking/effort 配置[+ tools])内与此前请求共享的最长前缀;任务内不设过期。未缓存输入按目录输入价(列有缓存写入价时用写入价),复用输入按缓存读取价,输出按输出价;Jev 输入 $0.042/百万 tokens,输出免费;包含执行模型、子代理及 Jev 的全部费用。此基准的任务内不设过期假设与运行时 `cacheTtlSeconds` 的寿命假设不同。
|
|
152
|
+
|
|
153
|
+
两种测量方法:`exact` 使用 bench observer 记录的逐消息前缀哈希(AutoCTX 1.2.x);B/C/H 运行时尚无前缀哈希,使用 `log` 重建(每个 session 按追加增长,输入量下降标记上下文改写)。在 12 个任务上用 `deepseek-v4.1-flash` 重跑 B/C、记录前缀哈希(24 个案例)验证:`log` 比 `exact` 的费用低 2.4%(B)和 3.1%(C);在 AutoCTX 上约低 11%,因为上下文替换会改动较早的 prompt 内容却不一定缩短输入。因此下表基线理论费用可能低估约 3%,AutoCTX 溢价略被高估,而非低估。
|
|
154
|
+
|
|
155
|
+
下表费用与缓存率为理论估算,非实际账单。
|
|
156
|
+
|
|
157
|
+
| 指标(每组 72 个案例) | AutoCTX 1.2.5 frozen | AutoCTX 1.2.3 frozen | AutoCTX 1.2.3 auto | B | C | H |
|
|
158
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
159
|
+
| Resolved / 72 | 53/72 | 52/72 | 49/72 | 39/72 | 44/72 | 42/72 |
|
|
160
|
+
| 1.2.5 frozen 对该组的配对胜/负(精确 McNemar p) | — | 6/5 (p=1.0) | — | 15/1 (p=0.001) | 12/3 (p=0.035) | 13/2 (p=0.007) |
|
|
161
|
+
| 理论总费用 USD(方法) | $1.738 (exact) | $1.810 (exact) | $1.603 (exact) | $0.908 (log) | $0.975 (log) | $1.027 (log) |
|
|
162
|
+
| 其中理论费用:执行模型 / 子代理 / Jev(USD) | 1.383 / 0.273 / 0.082 | 1.451 / 0.284 / 0.076 | 1.321 / 0.214 / 0.068 | 0.805 / 0.102 / 0 | 0.844 / 0.131 / 0 | 0.867 / 0.160 / 0 |
|
|
163
|
+
| 每个 resolved 案例的理论费用 | $0.0328 | $0.0348 | $0.0327 | $0.0233 | $0.0222 | $0.0244 |
|
|
164
|
+
| 执行模型理论缓存率 | 87.0% | 85.4% | 86.5% | 94.8% | 95.1% | 95.1% |
|
|
165
|
+
| 墙钟时间总和(秒) | 17580 | 18060 | 17670 | 17454 | 16520 | 17076 |
|
|
166
|
+
| Jev 调用成功/总数 | 367/368 | 233/233 | 260/260 | — | — | — |
|
|
141
167
|
|
|
142
|
-
|
|
168
|
+
若全部组统一用 `log` 方法,理论总费用为 AutoCTX 1.2.5 frozen $1.547,B 为 $0.908、C 为 $0.975、H 为 $1.027。
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
146
|
-
| Resolved / 72 | 52/72 | 49/72 | 39/72 | 44/72 | 42/72 |
|
|
147
|
-
| API 目录价估算费用,执行模型 + 子代理 + Jev(USD) | $1.514 | $1.374* | $0.876 | $0.933 | $1.018 |
|
|
148
|
-
| 上述费用中的 Jev(USD) | $0.076 | $0.068* | 不适用 | 不适用 | 不适用 |
|
|
149
|
-
| 墙钟时间总和(秒) | 18060 | 17670 | 17454 | 16520 | 17076 |
|
|
150
|
-
| Prompt 缓存命中率 | 87.5% | 88.4% | 94.3% | 94.6% | 93.9% |
|
|
170
|
+
AutoCTX 在此样本中比 B、C、H 解决更多案例,也更贵;按理论费用,1.2.5 frozen 比 C 总费用约高 70–80%,每个 resolved 案例约高 48%。1.2.5 与 1.2.3 frozen 按相同运行通道配对、使用 `exact` 比较:53 对 52 个 resolved(6 胜 / 5 负,无质量差异);理论总费用降低 4.0%($1.738 对 $1.810),执行模型理论缓存率 87.0% 对 85.4%,Jev 理论费用增加 $0.006。
|
|
151
171
|
|
|
152
|
-
|
|
172
|
+
1.2.5 在 72 个案例中的思考决策:201 次低成本切换(不调用 Jev)、86 次 skip、51 次 phase、9 次 pulse、11 次紧接上下文替换、11 次在思考预算用尽后回退;19 个案例达到 64 请求上限(1.2.3 为 18 个)。该上限包含 Jev 调用,因此 AutoCTX 在执行请求更少时就会触顶:这 19 个案例中有 10 个未解决,其 640 次请求里 77 次(12%)为 Jev 调用。B/C/H 也分别有 17/20/16 个案例触顶,但全部为执行请求。本基准保持该共同上限;把 Jev 排除在上限之外的重跑尚未进行。
|
|
153
173
|
|
|
154
|
-
| 执行模型(每组、每模型 24 个案例;resolved / 24) |
|
|
155
|
-
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
156
|
-
| deepseek-v4.1-flash | 17/24 | 16/24 | 11/24 | 15/24 | 14/24 |
|
|
157
|
-
| glm-5.3-flash | 18/24 | 18/24 | 17/24 | 18/24 | 18/24 |
|
|
158
|
-
| deepseek-v4-flash | 17/24 | 15/24 | 11/24 | 11/24 | 10/24 |
|
|
174
|
+
| 执行模型(每组、每模型 24 个案例;resolved / 24) | AutoCTX 1.2.5 frozen | AutoCTX 1.2.3 frozen | AutoCTX 1.2.3 auto | B | C | H |
|
|
175
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
176
|
+
| deepseek-v4.1-flash | 18/24 | 17/24 | 16/24 | 11/24 | 15/24 | 14/24 |
|
|
177
|
+
| glm-5.3-flash | 17/24 | 18/24 | 18/24 | 17/24 | 18/24 | 18/24 |
|
|
178
|
+
| deepseek-v4-flash | 18/24 | 17/24 | 15/24 | 11/24 | 11/24 | 10/24 |
|
|
159
179
|
|
|
160
|
-
Auto 运行中,49 个已完成回合进入学习:35/49 个被判无法评估,权重不变;14/49 个应用了小幅更新。此处 auto 未超过 frozen
|
|
180
|
+
Auto 运行中,49 个已完成回合进入学习:35/49 个被判无法评估,权重不变;14/49 个应用了小幅更新。此处 auto 未超过 frozen。
|
|
161
181
|
|
|
162
|
-
测量范围仅为上述 12 个 pytest 任务和 3 个 flash 执行模型;重复运行之间,每次共 12 个任务的运行,resolved 数约波动 ±2
|
|
182
|
+
测量范围仅为上述 12 个 pytest 任务和 3 个 flash 执行模型;重复运行之间,每次共 12 个任务的运行,resolved 数约波动 ±2 个。按理论费用,AutoCTX 在此处比基线更贵;auto learning 仍为 beta,默认关闭。宿主还存在限制:回合中途创建的 fork 只继承已完成回合,因此审查简报须自行携带当前回合证据。
|
|
163
183
|
|
|
164
184
|
## 复现
|
|
165
185
|
|
|
@@ -173,10 +193,11 @@ python3 bench/run.py --model deepseek-v4.1-flash --arms B,C --tasks bench/tasks.
|
|
|
173
193
|
python3 bench/run.py --model deepseek-v4.1-flash --arms H --tasks bench/tasks.json --out <dir> --plugin . --parallel 3 \
|
|
174
194
|
--extra-models glm-5.3-flash,deepseek-v4-pro,claude-haiku-5-5 --max-usd 0.10
|
|
175
195
|
python3 bench/grade.py <dir> --parallel 3
|
|
196
|
+
python3 bench/theoretical_cost.py "A=<dir>/*/A" "B=<dir>/*/B" "C=<dir>/*/C"
|
|
176
197
|
```
|
|
177
198
|
|
|
178
|
-
`<dir>` 替换为输出目录。A 是 AutoCTX;frozen 与 auto 使用各自目录。对全部 3 个执行模型分别运行,每个模型做 2 次运行,独立运行使用不同目录。`glm-5.3-flash` 对应的 `--extra-models` 改为 `deepseek-v4.1-flash,deepseek-v4-pro,claude-haiku-5-5`;`deepseek-v4-flash` 使用示例中的额外模型。重跑相同命令会续跑未完成案例。`--max-usd 0.10` 是 bench observer 的每案例 $0.10
|
|
199
|
+
`<dir>` 替换为输出目录。A 是 AutoCTX;frozen 与 auto 使用各自目录。对全部 3 个执行模型分别运行,每个模型做 2 次运行,独立运行使用不同目录。`glm-5.3-flash` 对应的 `--extra-models` 改为 `deepseek-v4.1-flash,deepseek-v4-pro,claude-haiku-5-5`;`deepseek-v4-flash` 使用示例中的额外模型。重跑相同命令会续跑未完成案例。`--max-usd 0.10` 是 bench observer 的每案例 $0.10 花费上限。`bench/theoretical_cost.py` 按上文方法估算费用与缓存,输出均为理论估算,非实际账单。
|
|
179
200
|
|
|
180
201
|
## 从旧版本升级
|
|
181
202
|
|
|
182
|
-
按上面的升级步骤安装 1.2.
|
|
203
|
+
按上面的升级步骤安装 1.2.5,并对照当前配置表检查 profile 覆盖项。不要沿用旧 README 中的默认模型或安装标签;当前默认是计费的 `jev-1.13`,发布标签为 `v1.2.5`。
|
package/briefs.js
CHANGED
|
@@ -39,7 +39,28 @@ function boundedDiff(diff, maxChars) {
|
|
|
39
39
|
return { diff: kept + (maxChars >= marker.length ? marker : ''), truncated: true };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Rank complete hunks against the failure before truncating; later files can be the relevant ones.
|
|
43
|
+
function effortDiff(diff, focus, maxChars = 1500) {
|
|
44
|
+
const files = diff.split(/(?=^diff --git )/m).filter(Boolean), candidates = [];
|
|
45
|
+
let added = 0, removed = 0;
|
|
46
|
+
const terms = focus.filter(term => typeof term === 'string' && term.length > 2).slice(0, 40);
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
for (const line of file.split('\n')) {
|
|
49
|
+
if (line.startsWith('+') && !line.startsWith('+++')) added++;
|
|
50
|
+
if (line.startsWith('-') && !line.startsWith('---')) removed++;
|
|
51
|
+
}
|
|
52
|
+
const parts = file.split(/(?=^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@)/m);
|
|
53
|
+
for (const hunk of parts.length > 1 ? parts.slice(1) : ['']) {
|
|
54
|
+
const value = parts[0] + hunk;
|
|
55
|
+
candidates.push({ value, score: terms.reduce((sum, term) => sum + Number(value.includes(term)), 0) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const stat = `${files.length} files changed, ${added} insertions(+), ${removed} deletions(-)`;
|
|
59
|
+
const relevant = candidates.sort((a, b) => b.score - a.score)[0]?.value ?? '(no diff)';
|
|
60
|
+
return (stat + '\n' + relevant).slice(0, maxChars);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function workspaceDiff(cwd, { maxChars = 12000, timeoutMs = 5000, focus } = {}) {
|
|
43
64
|
// Without a workspace, git would diff whatever directory the host process runs in.
|
|
44
65
|
if (!cwd) return { diff: '', files: [], untracked: [], truncated: false, error: 'no workspace' };
|
|
45
66
|
try {
|
|
@@ -89,7 +110,8 @@ export async function workspaceDiff(cwd, { maxChars = 12000, timeoutMs = 5000 }
|
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
112
|
const bounded = boundedDiff(combined, budget(maxChars, 12000));
|
|
92
|
-
return { ...bounded, files, untracked, truncated: bounded.truncated || omitted
|
|
113
|
+
return { ...bounded, files, untracked, truncated: bounded.truncated || omitted,
|
|
114
|
+
...(focus ? { evidence: effortDiff(combined, focus) } : {}) };
|
|
93
115
|
} catch (error) {
|
|
94
116
|
return { diff: '', files: [], untracked: [], truncated: false, error: errorText(error) };
|
|
95
117
|
}
|
package/cachespace.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
4
|
+
const nonnegative = (value) => {
|
|
5
|
+
if (!Number.isFinite(value) || value < 0) throw new RangeError("Expected a finite nonnegative token count");
|
|
6
|
+
return value;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Retain digests and counts only. The second chain lets sharedPrefix ignore effort while
|
|
10
|
+
// hashes still fingerprint the complete configuration, including effort, as sent.
|
|
11
|
+
export function fingerprint({ provider, model, reasoningEffort, tools, messages }, tokensOf) {
|
|
12
|
+
const effort = reasoningEffort ?? null;
|
|
13
|
+
const config = hash(JSON.stringify({ provider, model, effort, tools }));
|
|
14
|
+
const toolsHash = hash(JSON.stringify({ tools }));
|
|
15
|
+
const hashes = [], prefixes = [], tokens = [];
|
|
16
|
+
let previous = config, prefix = toolsHash;
|
|
17
|
+
for (const message of messages) {
|
|
18
|
+
const serialized = JSON.stringify(message);
|
|
19
|
+
previous = hash(previous + serialized);
|
|
20
|
+
prefix = hash(prefix + serialized);
|
|
21
|
+
hashes.push(previous);
|
|
22
|
+
prefixes.push(prefix);
|
|
23
|
+
tokens.push(nonnegative(tokensOf(message)));
|
|
24
|
+
}
|
|
25
|
+
return { space: `${provider}|${model}|${effort ?? "default"}`, config, toolsHash, hashes, prefixes, tokens };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sharedPrefix(a, b) {
|
|
29
|
+
if (!a || !b || a.toolsHash !== b.toolsHash) return 0;
|
|
30
|
+
let k = 0;
|
|
31
|
+
while (k < Math.min(a.prefixes.length, b.prefixes.length) && a.prefixes[k] === b.prefixes[k]) k++;
|
|
32
|
+
return k;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function reusableTokens(target, latest, { now, at, ttlMs }) {
|
|
36
|
+
if (!target || now - at > ttlMs || !Number.isFinite(at)) return 0;
|
|
37
|
+
// Provider/model identity must match too; effort identifies the destination cache space.
|
|
38
|
+
const route = (value) => value.space.slice(0, value.space.lastIndexOf("|"));
|
|
39
|
+
if (route(target) !== route(latest)) return 0;
|
|
40
|
+
return latest.tokens.slice(0, sharedPrefix(target, latest)).reduce((sum, value) => sum + value, 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function switchEstimate({ latest, target, at, now, ttlMs, pendingTokens, price }) {
|
|
44
|
+
const totalTokens = latest.tokens.reduce((sum, value) => sum + value, 0) + nonnegative(pendingTokens);
|
|
45
|
+
const reused = reusableTokens(target, latest, { now, at, ttlMs });
|
|
46
|
+
const uncachedTokens = totalTokens - reused;
|
|
47
|
+
const usd = Math.round(uncachedTokens * (price.input - (price.cacheRead ?? price.input))) / 1e6;
|
|
48
|
+
return { totalTokens, reusableTokens: reused, uncachedTokens, usd };
|
|
49
|
+
}
|
package/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
DELEGATION_GUIDANCE,
|
|
27
27
|
} from "./routing.js";
|
|
28
28
|
import { newStats, applyStatsRecord } from "./stats.js";
|
|
29
|
+
import { fingerprint, sharedPrefix, reusableTokens, switchEstimate } from "./cachespace.js";
|
|
29
30
|
import { contributionFacts, learningQuestions, proposalsFrom } from "./learning.js";
|
|
30
31
|
import { segmentUnits, focusTerms } from "./segments.js";
|
|
31
32
|
import { workspaceDiff, hunkContext, failureExcerpt, adviceQuestion, advisorBrief, reviewBrief, attemptsFrom } from "./briefs.js";
|
|
@@ -189,6 +190,14 @@ export const Config = z.object({
|
|
|
189
190
|
.description(
|
|
190
191
|
"Reasoning effort used when Jev judges a phase needs it; it stays for the turn and changes only at a context checkpoint. off keeps the default",
|
|
191
192
|
),
|
|
193
|
+
effortSwitchFreeTokens: z.number().min(0).default(4096)
|
|
194
|
+
.description("Decision-point pulses with at most this estimated uncached re-read proceed without asking Jev"),
|
|
195
|
+
maxEffortDecisions: z.number().min(0).default(6)
|
|
196
|
+
.description("Separate Jev effort decision calls per root task; never spends the routing/context budget"),
|
|
197
|
+
effortPhaseRequests: z.number().min(1).default(4)
|
|
198
|
+
.description("Maximum high-effort requests after a phase trigger; a new trigger renews this window"),
|
|
199
|
+
cacheTtlSeconds: z.number().min(0).default(300)
|
|
200
|
+
.description("Conservative assumed reuse lifetime in seconds; provider cache TTL is unverified"),
|
|
192
201
|
learning: z
|
|
193
202
|
.union(["auto", "manual", "frozen"])
|
|
194
203
|
.default("frozen")
|
|
@@ -333,6 +342,14 @@ const THINKING_POLICIES = {
|
|
|
333
342
|
// Measured in dsh-bench (12 SWE-bench Verified pytest tasks, 600 s, 64 calls incl. Jev; resolved tasks).
|
|
334
343
|
const THINKING_MEASURED =
|
|
335
344
|
"dsh-bench, 12 SWE-bench Verified tasks per run: deepseek-v4.1-flash resolved 18/24 with decision_points and 15/24 with thinking off for the whole turn; glm-5.3-flash 18/24 with decision_points and 7/12 with off; always was not measured. Small samples.";
|
|
345
|
+
const EFFORT_STATE_BYTES = 8000;
|
|
346
|
+
const shortText = (value, chars) => String(value ?? "").slice(0, chars);
|
|
347
|
+
const jsonBytes = (value) => Buffer.byteLength(JSON.stringify(value));
|
|
348
|
+
const EFFORT_CHOICES = {
|
|
349
|
+
skip: "Keep the current effort/cache space; no effort change.",
|
|
350
|
+
pulse: "High effort for the next request only.",
|
|
351
|
+
phase: "High effort until the trigger is resolved or the request window/turn ends; new triggers renew the window.",
|
|
352
|
+
};
|
|
336
353
|
// Measured on deepseek-v4.1-flash through WorkBuddy (same affinity id): thinking on and off are separate
|
|
337
354
|
// provider cache spaces. Other routes were not measured.
|
|
338
355
|
const thinkingCacheFact = (model) =>
|
|
@@ -483,6 +500,17 @@ export function apply(ctx, options) {
|
|
|
483
500
|
effortSet = new Map(),
|
|
484
501
|
effortDue = new Map(),
|
|
485
502
|
effortStall = new Map(),
|
|
503
|
+
effortSkips = new Map(),
|
|
504
|
+
effortPulses = new Map(),
|
|
505
|
+
effortPhases = new Map(),
|
|
506
|
+
effortRequests = new Map(),
|
|
507
|
+
effortOutputs = new Map(),
|
|
508
|
+
effortReports = new Map(),
|
|
509
|
+
effortTools = new Map(),
|
|
510
|
+
taskHeads = new Map(),
|
|
511
|
+
turnHeads = new Map(),
|
|
512
|
+
admittedMessages = new Map(),
|
|
513
|
+
cacheSpaces = new Map(),
|
|
486
514
|
userEffort = new Set(),
|
|
487
515
|
childEffort = new Map(),
|
|
488
516
|
// Per-root accounting of the current turn: role use, cost and cache by category.
|
|
@@ -554,6 +582,7 @@ export function apply(ctx, options) {
|
|
|
554
582
|
pending: new Set(),
|
|
555
583
|
childRequests: 0,
|
|
556
584
|
decisions: 0,
|
|
585
|
+
effortDecisions: 0,
|
|
557
586
|
});
|
|
558
587
|
return budgets.get(id);
|
|
559
588
|
};
|
|
@@ -607,6 +636,8 @@ export function apply(ctx, options) {
|
|
|
607
636
|
"turn_plan",
|
|
608
637
|
"effort_checkpoint",
|
|
609
638
|
"effort_raised",
|
|
639
|
+
"effort_decision",
|
|
640
|
+
"effort_phase_end",
|
|
610
641
|
"context_deferred",
|
|
611
642
|
"learning",
|
|
612
643
|
].includes(value.kind)
|
|
@@ -815,7 +846,7 @@ export function apply(ctx, options) {
|
|
|
815
846
|
record({ kind: "jev_unavailable", session, model: config.jevModel, httpStatus: status, error: detail,
|
|
816
847
|
...(transient ? { pausedMs: JEV_PAUSE_MS } : {}) });
|
|
817
848
|
}
|
|
818
|
-
//
|
|
849
|
+
// Learning and effort calls do not compete with routing/context decisions. Effort has its own cap.
|
|
819
850
|
async function decide(agent, request, signal, { unbudgeted = false } = {}) {
|
|
820
851
|
const root = rootOf(agent),
|
|
821
852
|
state = budget(root);
|
|
@@ -833,6 +864,11 @@ export function apply(ctx, options) {
|
|
|
833
864
|
return memoFor(root)(key, async () => {
|
|
834
865
|
if (!unbudgeted && state.decisions >= config.maxDecisions)
|
|
835
866
|
throw new Error("Jev task decision budget exhausted.");
|
|
867
|
+
if (unbudgeted && request.trigger === "effort_decision") {
|
|
868
|
+
if (state.effortDecisions >= (config.maxEffortDecisions ?? 6))
|
|
869
|
+
throw Object.assign(new Error("Jev effort decision budget exhausted."), { code: "EFFORT_BUDGET" });
|
|
870
|
+
state.effortDecisions++;
|
|
871
|
+
}
|
|
836
872
|
if (!unbudgeted) state.decisions++;
|
|
837
873
|
updateStats(root);
|
|
838
874
|
const started = performance.now();
|
|
@@ -886,6 +922,11 @@ export function apply(ctx, options) {
|
|
|
886
922
|
const usd = jevCostUSD(config.jevModel, result.usage);
|
|
887
923
|
if (usd !== undefined) useOf(root).cost.jev += usd;
|
|
888
924
|
else useOf(root).unpriced++;
|
|
925
|
+
// The effort state contains raw task/failure evidence. Keep only the validated choice and
|
|
926
|
+
// billing counters even if a gateway echoes state or adds free-text explanations.
|
|
927
|
+
const logged = request.trigger === "effort_decision" ? { model: result.model, usage: result.usage,
|
|
928
|
+
answers: { thinking: { type: "choice", choice: result.answers.thinking.choice,
|
|
929
|
+
confidence: result.answers.thinking.confidence } } } : result;
|
|
889
930
|
record({
|
|
890
931
|
kind: "jev",
|
|
891
932
|
session: agent.session.id,
|
|
@@ -893,7 +934,7 @@ export function apply(ctx, options) {
|
|
|
893
934
|
httpStatus: response.status,
|
|
894
935
|
wallMs: performance.now() - started,
|
|
895
936
|
...(usd === undefined ? {} : { usd }),
|
|
896
|
-
...
|
|
937
|
+
...logged,
|
|
897
938
|
});
|
|
898
939
|
return result;
|
|
899
940
|
});
|
|
@@ -1111,14 +1152,18 @@ export function apply(ctx, options) {
|
|
|
1111
1152
|
.flatMap((test) => test.runs)
|
|
1112
1153
|
.filter((run) => run.via === undefined && run.event > since)
|
|
1113
1154
|
.at(-1);
|
|
1155
|
+
const recent = effortTools.get(id) ?? [];
|
|
1156
|
+
recent.push({ name: shortText(exec.name, 80), outcome: latest?.outcome ?? (result.isError ? "error" : "success") });
|
|
1157
|
+
effortTools.set(id, recent.slice(-6));
|
|
1158
|
+
resolveEffortPhase(exec, result, latest);
|
|
1114
1159
|
if (latest && (latest.outcome === "fail" || latest.outcome === "error")) {
|
|
1115
|
-
wantEffort(exec.agent, "test_failure");
|
|
1116
1160
|
const key = Object.entries(graphFor(id).tests).find(([, test]) => test.runs.includes(latest))?.[0];
|
|
1117
1161
|
if (key) {
|
|
1118
1162
|
if (!failureOutput.has(id)) failureOutput.set(id, new Map());
|
|
1119
1163
|
const text = (result.content ?? []).filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
1120
1164
|
failureOutput.get(id).set(key, text.slice(-40000));
|
|
1121
1165
|
}
|
|
1166
|
+
wantEffort(exec.agent, "test_failure", { testKey: key });
|
|
1122
1167
|
}
|
|
1123
1168
|
}
|
|
1124
1169
|
}
|
|
@@ -1169,6 +1214,12 @@ export function apply(ctx, options) {
|
|
|
1169
1214
|
active.delete(info.id);
|
|
1170
1215
|
advisorChildren.delete(info.id);
|
|
1171
1216
|
settled.set(info.id, info);
|
|
1217
|
+
const role = childRoles.get(info.id);
|
|
1218
|
+
if (role === "reviewer" || role === "advisor") {
|
|
1219
|
+
if (!effortReports.has(running.parent)) effortReports.set(running.parent, new Map());
|
|
1220
|
+
effortReports.get(running.parent).set(role === "reviewer" ? "review_report" : "advisor_report",
|
|
1221
|
+
shortText((info.lastAssistantMessage ?? []).filter(block => block.type === "text").map(block => block.text).join("\n"), 1500));
|
|
1222
|
+
}
|
|
1172
1223
|
if (childRoles.get(info.id) === "worker" && childModels.has(info.id)) {
|
|
1173
1224
|
const use = roleUse(running.parent, "worker", childModels.get(info.id));
|
|
1174
1225
|
if (info.stopReason === "completed") use.completed++;
|
|
@@ -1270,11 +1321,213 @@ export function apply(ctx, options) {
|
|
|
1270
1321
|
return plan;
|
|
1271
1322
|
}
|
|
1272
1323
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1324
|
+
const phaseWindow = () => Math.max(1, Math.floor(config.effortPhaseRequests ?? 4));
|
|
1325
|
+
const failingTest = (id) => Object.entries(graphFor(id).tests)
|
|
1326
|
+
.map(([key, test]) => ({ key, run: test.runs.filter(run => run.via === undefined).at(-1) }))
|
|
1327
|
+
.filter(({ run }) => ["fail", "error"].includes(run?.outcome))
|
|
1328
|
+
.sort((a, b) => b.run.event - a.run.event)[0];
|
|
1329
|
+
function phaseTrigger(id, reason, details = {}) {
|
|
1330
|
+
return { reason, testKey: details.testKey ?? (reason === "test_failure" ? failingTest(id)?.key : undefined),
|
|
1331
|
+
progress: progressKey(graphFor(id)), event: graphFor(id).sequence };
|
|
1332
|
+
}
|
|
1333
|
+
function extendEffortPhase(id, phase, trigger) {
|
|
1334
|
+
phase.triggers.set(trigger.reason + "|" + (trigger.testKey ?? ""), trigger);
|
|
1335
|
+
phase.until = (effortRequests.get(id) ?? 0) + phaseWindow();
|
|
1336
|
+
}
|
|
1337
|
+
function endEffortPhase(id, endedBy) {
|
|
1338
|
+
const phase = effortPhases.get(id);
|
|
1339
|
+
if (!phase) return;
|
|
1340
|
+
effortPhases.delete(id);
|
|
1341
|
+
record({ kind: "effort_phase_end", session: id, reason: phase.reason, requests: phase.requests, endedBy });
|
|
1342
|
+
}
|
|
1343
|
+
function resolveEffortPhase(exec, result, latest) {
|
|
1344
|
+
const id = exec.agent.session.id, phase = effortPhases.get(id);
|
|
1345
|
+
if (!phase) return;
|
|
1346
|
+
const successful = !result.isError && !["fail", "error", "unknown"].includes(latest?.outcome);
|
|
1347
|
+
let endedBy;
|
|
1348
|
+
for (const [key, trigger] of phase.triggers) {
|
|
1349
|
+
if (graphFor(id).sequence <= trigger.event) continue;
|
|
1350
|
+
if (trigger.reason === "test_failure") {
|
|
1351
|
+
const test = graphFor(id).tests[trigger.testKey];
|
|
1352
|
+
const run = test?.runs.filter(run => run.via === undefined).at(-1);
|
|
1353
|
+
if (!successful || run?.outcome !== "pass" || run.event <= trigger.event) continue;
|
|
1354
|
+
endedBy = "test_passed";
|
|
1355
|
+
} else if (["review_report", "advisor_report"].includes(trigger.reason)) {
|
|
1356
|
+
if (!successful || !(latest?.outcome === "pass" || changesFilesCall(exec.name, exec.arguments))) continue;
|
|
1357
|
+
endedBy = "report_acted_on";
|
|
1358
|
+
} else {
|
|
1359
|
+
if (!successful || progressKey(graphFor(id)) === trigger.progress) continue;
|
|
1360
|
+
endedBy = "progress";
|
|
1361
|
+
}
|
|
1362
|
+
phase.triggers.delete(key);
|
|
1363
|
+
}
|
|
1364
|
+
if (!phase.triggers.size) endEffortPhase(id, endedBy);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// A fresh trigger renews an active phase locally, without spending another Jev decision.
|
|
1368
|
+
function wantEffort(agent, reason, details = {}) {
|
|
1275
1369
|
if (agent.session.header.parentSession !== undefined) return;
|
|
1276
|
-
|
|
1277
|
-
if (
|
|
1370
|
+
const id = agent.session.id;
|
|
1371
|
+
if (turnPlans.get(id)?.thinking !== "decision_points") return;
|
|
1372
|
+
const trigger = phaseTrigger(id, reason, details), phase = effortPhases.get(id);
|
|
1373
|
+
if (phase) extendEffortPhase(id, phase, trigger);
|
|
1374
|
+
else if (!effortDue.has(id) || ["turn_start", "stall"].includes(effortDue.get(id).reason))
|
|
1375
|
+
effortDue.set(id, trigger);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function invalidateCache(id, reason) {
|
|
1379
|
+
const cache = cacheSpaces.get(id);
|
|
1380
|
+
record({ kind: "cache_invalidated", session: id, reason,
|
|
1381
|
+
spaces: cache?.spaces.size ?? 0, hadLatest: !!cache?.latest });
|
|
1382
|
+
cacheSpaces.set(id, { spaces: new Map(), latest: undefined, invalidated: reason, failed: false });
|
|
1383
|
+
}
|
|
1384
|
+
function checkCacheGeneration(agent) {
|
|
1385
|
+
const cache = cacheSpaces.get(agent.session.id), generation = agent.session.surface?.contentGeneration;
|
|
1386
|
+
if (cache?.generation !== undefined && generation !== undefined && cache.generation !== generation)
|
|
1387
|
+
invalidateCache(agent.session.id, "surface_generation_changed");
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Use the live projected messages, never an old request plus a token-count delta when available.
|
|
1391
|
+
function effortEstimate(agent, resolved, payload) {
|
|
1392
|
+
checkCacheGeneration(agent);
|
|
1393
|
+
const id = agent.session.id, cache = cacheSpaces.get(id), price = modelInfo(resolved.model)?.price;
|
|
1394
|
+
const projected = agent.session.deriveMessages?.();
|
|
1395
|
+
const messages = payload.messages ?? resolved.messages ?? (projected === undefined ? undefined :
|
|
1396
|
+
[...projected, ...(admittedMessages.get(id) ?? [])]);
|
|
1397
|
+
let current = cache?.latest, source = "latest_plus_pending", pendingTokens = 0;
|
|
1398
|
+
if (messages !== undefined) {
|
|
1399
|
+
current = fingerprint({ ...resolved, tools: payload.tools ?? resolved.tools ?? agent.session.requestHeader?.()?.tools,
|
|
1400
|
+
messages }, message => ctx.tokenMeter.estimateMessage(message));
|
|
1401
|
+
source = payload.messages !== undefined || resolved.messages !== undefined ? "request_messages" : "session.deriveMessages+admitted";
|
|
1402
|
+
} else if (current) {
|
|
1403
|
+
pendingTokens = Math.max(0, ctx.tokenMeter.measure(agent.session).totalTokens -
|
|
1404
|
+
current.tokens.reduce((sum, value) => sum + value, 0));
|
|
1405
|
+
}
|
|
1406
|
+
const totalTokens = current ? current.tokens.reduce((sum, value) => sum + value, 0) + pendingTokens : null;
|
|
1407
|
+
if (cache?.invalidated && !cache.latest)
|
|
1408
|
+
return { totalTokens, reusableTokens: 0, uncachedTokens: 0, usd: 0, source,
|
|
1409
|
+
postReplacement: true };
|
|
1410
|
+
if (!current || !price)
|
|
1411
|
+
return { totalTokens, reusableTokens: null, uncachedTokens: null, usd: null, source };
|
|
1412
|
+
const target = cache?.spaces.get(`${resolved.provider}|${resolved.model}|${config.adaptiveEffort}`);
|
|
1413
|
+
const estimate = switchEstimate({ latest: current, target: target?.fingerprint, at: target?.at,
|
|
1414
|
+
now: Date.now(), ttlMs: (config.cacheTtlSeconds ?? 300) * 1000, pendingTokens, price });
|
|
1415
|
+
// Reuse in the current space is the baseline: entering high cannot lose tokens the baseline also
|
|
1416
|
+
// has to read. After a replacement both spaces must read the new prefix, so the premium is zero.
|
|
1417
|
+
const baseline = cache?.spaces.get(`${resolved.provider}|${resolved.model}|${effortSet.get(id) ?? resolved.reasoningEffort ?? "default"}`);
|
|
1418
|
+
const baselineReuse = reusableTokens(baseline?.fingerprint, current, { now: Date.now(), at: baseline?.at,
|
|
1419
|
+
ttlMs: (config.cacheTtlSeconds ?? 300) * 1000 });
|
|
1420
|
+
estimate.uncachedTokens = Math.max(0, baselineReuse - estimate.reusableTokens);
|
|
1421
|
+
estimate.usd = round(estimate.uncachedTokens * (price.input - (price.cacheRead ?? price.input)) / 1e6);
|
|
1422
|
+
return { ...estimate, source };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function effortOptionCosts(id, resolved, estimate) {
|
|
1426
|
+
const price = modelInfo(resolved.model)?.price;
|
|
1427
|
+
const measured = effortOutputs.get(id)?.get(`${resolved.provider}|${resolved.model}`);
|
|
1428
|
+
const pulses = effortPulses.get(id) ?? { count: 0, reasons: {}, rePulsedWithin3: 0 };
|
|
1429
|
+
const average = measured?.calls ? measured.outputTokens / measured.calls : null;
|
|
1430
|
+
const reasoning = measured?.reasoningCalls ? measured.reasoningTokens / measured.reasoningCalls : null;
|
|
1431
|
+
const churn = { pulses: pulses.count, rePulsedWithin3: pulses.rePulsedWithin3,
|
|
1432
|
+
reentryIfTriggeredSoon: { uncachedTokens: estimate.uncachedTokens, usd: estimate.usd,
|
|
1433
|
+
basis: "Same prefix/target-space reuse as now; future growth and cache hits are unknown." },
|
|
1434
|
+
observed: "1.2.3: 58% of switches reverted within 3 requests" };
|
|
1435
|
+
const option = (requests) => ({ label: "estimate, cache hit not guaranteed", expectedRequests: requests,
|
|
1436
|
+
enter: { uncachedTokens: estimate.uncachedTokens, usd: estimate.usd },
|
|
1437
|
+
extraOutput: { averageTokens: average, averageReasoningTokens: reasoning, reasoningIncludedInOutput: true,
|
|
1438
|
+
tokens: average === null ? null : round(average * requests),
|
|
1439
|
+
usd: average === null || !price ? null : round(average * requests * price.output / 1e6),
|
|
1440
|
+
basis: average === null ? THINKING_MEASURED : "session high-effort output (including reasoning); upper estimate, no off-output subtraction" },
|
|
1441
|
+
churn, requestsPotentiallySaved: "Not measured; consider whether deliberate reasoning avoids later failed attempts/requests." });
|
|
1442
|
+
return { skip: { label: "estimate, cache hit not guaranteed", expectedRequests: 0,
|
|
1443
|
+
enter: { uncachedTokens: 0, usd: 0 }, extraOutput: { tokens: 0, usd: 0 } },
|
|
1444
|
+
pulse: option(1), phase: option(phaseWindow()) };
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
async function effortEvidence(agent, trigger) {
|
|
1448
|
+
const id = agent.session.id, graph = graphFor(id), reason = trigger.reason;
|
|
1449
|
+
const evidence = { task: shortText(taskHeads.get(id), 800) };
|
|
1450
|
+
if (reason === "test_failure") {
|
|
1451
|
+
const key = trigger.testKey ?? failingTest(id)?.key;
|
|
1452
|
+
const runs = graph.tests[key]?.runs.filter(run => run.via === undefined) ?? [];
|
|
1453
|
+
const raw = failureOutput.get(id)?.get(key);
|
|
1454
|
+
const excerpt = raw === undefined ? "(no raw failure output captured)" : failureExcerpt(raw, { maxChars: 1500 });
|
|
1455
|
+
const focus = focusTerms({ failingTests: key ? [key] : [],
|
|
1456
|
+
errors: excerpt.match(/\b[A-Za-z_$][\w$]*(?:Error|Exception)\b/g) ?? [],
|
|
1457
|
+
symbols: [...(key?.match(/\btest_\w+|[\w./-]+\.(?:py|js|ts|go|rs)\b/g) ?? []),
|
|
1458
|
+
...(excerpt.match(/[\w./-]+\.(?:py|js|ts|tsx|jsx|go|rs|java|c|cpp)\b/g) ?? [])] });
|
|
1459
|
+
const change = await workspaceDiff(workspaceOf(agent), { maxChars: 1500, focus });
|
|
1460
|
+
evidence.testKey = shortText(key, 400);
|
|
1461
|
+
evidence.failureExcerpt = shortText(excerpt, 1500);
|
|
1462
|
+
evidence.attempts = { count: runs.length, outcomes: runs.map(run => run.outcome),
|
|
1463
|
+
summary: shortText(attemptsFrom(graph, key), 1000) };
|
|
1464
|
+
evidence.change = shortText(change.evidence ?? (change.error ? "Diff unavailable: " + change.error : change.diff), 1500);
|
|
1465
|
+
} else if (["review_report", "advisor_report"].includes(reason)) {
|
|
1466
|
+
evidence.report = shortText(effortReports.get(id)?.get(reason), 1500);
|
|
1467
|
+
} else if (reason === "stall") {
|
|
1468
|
+
evidence.stepsWithoutProgress = graphStep(id) - (effortStall.get(id)?.step ?? graphStep(id));
|
|
1469
|
+
evidence.tools = (effortTools.get(id) ?? []).slice(-6);
|
|
1470
|
+
} else evidence.request = shortText(turnHeads.get(id), 800);
|
|
1471
|
+
return evidence;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Bound the entire serialized state (and question) in UTF-8 bytes, including JSON escaping. Preserve
|
|
1475
|
+
// the observed failure ahead of background/diff text when non-ASCII evidence needs more space.
|
|
1476
|
+
function boundEffortRequest(request) {
|
|
1477
|
+
const evidence = request.evidence;
|
|
1478
|
+
const fields = [[evidence, "task"], [evidence, "request"], [evidence, "change"],
|
|
1479
|
+
[evidence.attempts, "summary"], [evidence, "testKey"], [evidence, "report"], [evidence, "failureExcerpt"]];
|
|
1480
|
+
while (jsonBytes(request) > EFFORT_STATE_BYTES) {
|
|
1481
|
+
const field = fields.find(([object, key]) => typeof object?.[key] === "string" && object[key].length > 64);
|
|
1482
|
+
if (field) {
|
|
1483
|
+
const [object, key] = field;
|
|
1484
|
+
object[key] = object[key].slice(0, Math.max(64, Math.floor(object[key].length / 2)));
|
|
1485
|
+
} else if (evidence.tools?.length) evidence.tools.pop();
|
|
1486
|
+
else throw new Error("Effort state metadata exceeds byte budget");
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
async function decisionPointEffort(agent, resolved, trigger, payload) {
|
|
1491
|
+
const id = agent.session.id, reason = trigger.reason;
|
|
1492
|
+
let estimate = { totalTokens: null, reusableTokens: null, uncachedTokens: null, usd: null },
|
|
1493
|
+
decision = "fallback", optionCosts = effortOptionCosts(id, resolved, estimate), evidenceBytes = 0, evidenceKey;
|
|
1494
|
+
try {
|
|
1495
|
+
estimate = effortEstimate(agent, resolved, payload);
|
|
1496
|
+
optionCosts = effortOptionCosts(id, resolved, estimate);
|
|
1497
|
+
if (estimate.postReplacement) decision = "post_replacement";
|
|
1498
|
+
else if (estimate.uncachedTokens !== null && estimate.uncachedTokens <= (config.effortSwitchFreeTokens ?? 4096))
|
|
1499
|
+
decision = "cheap";
|
|
1500
|
+
else if (budget(id).effortDecisions >= (config.maxEffortDecisions ?? 6)) decision = "fallback_budget";
|
|
1501
|
+
else {
|
|
1502
|
+
const evidence = await effortEvidence(agent, trigger);
|
|
1503
|
+
const request = { trigger: "effort_decision", reason, evidence,
|
|
1504
|
+
estimate: { ...estimate, label: "estimate, cache hit not guaranteed" }, optionCosts,
|
|
1505
|
+
preference: roots.get(id)?.policy?.weights,
|
|
1506
|
+
questions: { thinking: { type: "choice", criteria: EFFORT_CHOICES, instructions:
|
|
1507
|
+
"Choose the effort justified by this evidence. Quality comes first; skip is right for a mechanical fix. Weigh entry, output, churn and requests a phase may save." } } };
|
|
1508
|
+
boundEffortRequest(request);
|
|
1509
|
+
evidenceBytes = jsonBytes(evidence);
|
|
1510
|
+
evidenceKey = createHash("sha256").update(JSON.stringify([reason, evidence, progressKey(graphFor(id))])).digest("hex");
|
|
1511
|
+
const skipped = effortSkips.get(id)?.get(reason);
|
|
1512
|
+
if (skipped === evidenceKey) decision = "skip";
|
|
1513
|
+
else {
|
|
1514
|
+
if (!config.apiKey) throw new Error("Effort decision unavailable");
|
|
1515
|
+
const timeout = AbortSignal.timeout(15000);
|
|
1516
|
+
const signal = payload.signal ? AbortSignal.any([payload.signal, timeout]) : timeout;
|
|
1517
|
+
decision = (await decide(agent, request, signal, { unbudgeted: true })).answers.thinking.choice;
|
|
1518
|
+
if (decision === "skip") {
|
|
1519
|
+
if (!effortSkips.has(id)) effortSkips.set(id, new Map());
|
|
1520
|
+
effortSkips.get(id).set(reason, evidenceKey);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
if (payload.signal?.aborted) throw error;
|
|
1526
|
+
decision = error.code === "EFFORT_BUDGET" ? "fallback_budget" : "fallback";
|
|
1527
|
+
}
|
|
1528
|
+
record({ kind: "effort_decision", session: id, reason, decision, ...estimate,
|
|
1529
|
+
optionCosts, evidenceBytes, ...(evidenceKey ? { evidenceHash: evidenceKey } : {}) });
|
|
1530
|
+
return decision;
|
|
1278
1531
|
}
|
|
1279
1532
|
|
|
1280
1533
|
// The root's sticky effort through dsh's request-config waterfall on the agent's own scoped context, as
|
|
@@ -1298,12 +1551,53 @@ export function apply(ctx, options) {
|
|
|
1298
1551
|
const root = rootOf(agent);
|
|
1299
1552
|
let effort;
|
|
1300
1553
|
if (id === root) {
|
|
1554
|
+
const requestNumber = (effortRequests.get(id) ?? 0) + 1;
|
|
1555
|
+
effortRequests.set(id, requestNumber);
|
|
1556
|
+
checkCacheGeneration(agent);
|
|
1301
1557
|
const plan = effortSupported.get(root) === true ? turnPlans.get(root) : undefined;
|
|
1302
|
-
if (plan?.thinking
|
|
1303
|
-
|
|
1304
|
-
effort =
|
|
1305
|
-
|
|
1306
|
-
|
|
1558
|
+
if (effortPhases.has(id) && plan?.thinking !== "decision_points") endEffortPhase(id, "policy_changed");
|
|
1559
|
+
if (plan?.thinking === "always") {
|
|
1560
|
+
effort = plan.effort;
|
|
1561
|
+
if (effort !== undefined)
|
|
1562
|
+
record({ kind: "effort_raised", session: id, effort, reason: "always", step: payload.step });
|
|
1563
|
+
}
|
|
1564
|
+
else if (plan?.thinking === "decision_points") {
|
|
1565
|
+
let phase = effortPhases.get(id);
|
|
1566
|
+
if (phase && requestNumber > phase.until) {
|
|
1567
|
+
endEffortPhase(id, "request_limit");
|
|
1568
|
+
phase = undefined;
|
|
1569
|
+
}
|
|
1570
|
+
const trigger = effortDue.get(id);
|
|
1571
|
+
let decision;
|
|
1572
|
+
if (!phase && trigger) {
|
|
1573
|
+
decision = await decisionPointEffort(agent, resolved, trigger, payload);
|
|
1574
|
+
if (decision === "phase") {
|
|
1575
|
+
phase = { reason: trigger.reason, requests: 0, triggers: new Map(),
|
|
1576
|
+
until: requestNumber + phaseWindow() - 1 };
|
|
1577
|
+
phase.triggers.set(trigger.reason + "|" + (trigger.testKey ?? ""), trigger);
|
|
1578
|
+
effortPhases.set(id, phase);
|
|
1579
|
+
}
|
|
1580
|
+
effortDue.delete(id);
|
|
1581
|
+
}
|
|
1582
|
+
if (phase) {
|
|
1583
|
+
effort = config.adaptiveEffort;
|
|
1584
|
+
phase.requests++;
|
|
1585
|
+
record({ kind: "effort_raised", session: id, effort, reason: phase.reason, step: payload.step, phase: true });
|
|
1586
|
+
} else if (decision === "skip") {
|
|
1587
|
+
// A skip leaves the currently selected space intact, even if the previous pulse was high.
|
|
1588
|
+
effort = effortSet.get(id) ?? resolved.reasoningEffort;
|
|
1589
|
+
if (effort === config.adaptiveEffort)
|
|
1590
|
+
record({ kind: "effort_raised", session: id, effort, reason: trigger.reason, step: payload.step });
|
|
1591
|
+
} else if (decision !== undefined) {
|
|
1592
|
+
effort = config.adaptiveEffort;
|
|
1593
|
+
const pulses = effortPulses.get(id) ?? { count: 0, reasons: {}, rePulsedWithin3: 0 };
|
|
1594
|
+
if (pulses.lastRequest !== undefined && requestNumber - pulses.lastRequest <= 3) pulses.rePulsedWithin3++;
|
|
1595
|
+
pulses.count++;
|
|
1596
|
+
pulses.lastRequest = requestNumber;
|
|
1597
|
+
pulses.reasons[trigger.reason] = (pulses.reasons[trigger.reason] ?? 0) + 1;
|
|
1598
|
+
effortPulses.set(id, pulses);
|
|
1599
|
+
record({ kind: "effort_raised", session: id, effort, reason: trigger.reason, step: payload.step });
|
|
1600
|
+
}
|
|
1307
1601
|
}
|
|
1308
1602
|
} else {
|
|
1309
1603
|
// A same-model child keeps the effort its branch started with (only an always-thinking parent
|
|
@@ -1533,6 +1827,10 @@ export function apply(ctx, options) {
|
|
|
1533
1827
|
// LGTM ends the turn as is: steering the report back reopened it, and flash executors chased
|
|
1534
1828
|
// the reviewer's caveats until the request cap.
|
|
1535
1829
|
if (!lgtm) {
|
|
1830
|
+
if (!effortReports.has(id)) effortReports.set(id, new Map());
|
|
1831
|
+
const received = (settled.get(run.id)?.lastAssistantMessage ?? [])
|
|
1832
|
+
.filter(block => block.type === "text").map(block => block.text).join("\n");
|
|
1833
|
+
effortReports.get(id).set("review_report", shortText(received || report, 1500));
|
|
1536
1834
|
agent.steer(note(reviewMessage(report)));
|
|
1537
1835
|
wantEffort(agent, "review_report");
|
|
1538
1836
|
return true;
|
|
@@ -1561,6 +1859,7 @@ export function apply(ctx, options) {
|
|
|
1561
1859
|
if (!steered && modeOf(agent) === "auto")
|
|
1562
1860
|
await settleTurn(agent.session, turn).catch((error) =>
|
|
1563
1861
|
record({ kind: "learning_failed", session: agent.session.id, error: String(error) }));
|
|
1862
|
+
if (!steered) endEffortPhase(agent.session.id, "turn_end");
|
|
1564
1863
|
} finally {
|
|
1565
1864
|
await saved;
|
|
1566
1865
|
}
|
|
@@ -2015,6 +2314,10 @@ export function apply(ctx, options) {
|
|
|
2015
2314
|
.join("\n")
|
|
2016
2315
|
.trim();
|
|
2017
2316
|
if (!reply) throw new Error("Advisor returned no text.");
|
|
2317
|
+
if (!effortReports.has(id)) effortReports.set(id, new Map());
|
|
2318
|
+
const received = (settled.get(run.id)?.lastAssistantMessage ?? [])
|
|
2319
|
+
.filter(block => block.type === "text").map(block => block.text).join("\n");
|
|
2320
|
+
effortReports.get(id).set("advisor_report", shortText(received || reply, 1500));
|
|
2018
2321
|
record({ kind: "advisor_end", session: id, model: model.id, chars: reply.length });
|
|
2019
2322
|
const use = roleUse(id, "advisor", model.id);
|
|
2020
2323
|
use.completed++;
|
|
@@ -2079,7 +2382,12 @@ export function apply(ctx, options) {
|
|
|
2079
2382
|
graphFor(id);
|
|
2080
2383
|
const newTurn = !isChild && turnStartSteps.get(id)?.turn !== payload.turn;
|
|
2081
2384
|
if (newTurn) {
|
|
2385
|
+
endEffortPhase(id, "turn_end");
|
|
2082
2386
|
turnStartSteps.set(id, { turn: payload.turn, step: graphStep(id) });
|
|
2387
|
+
effortPulses.delete(id);
|
|
2388
|
+
effortSkips.delete(id);
|
|
2389
|
+
effortRequests.delete(id);
|
|
2390
|
+
effortDue.delete(id);
|
|
2083
2391
|
if (turnUses.get(id)?.turn !== payload.turn) turnUses.set(id, newUse(payload.turn));
|
|
2084
2392
|
}
|
|
2085
2393
|
const reopened = !isChild && turnUses.get(id);
|
|
@@ -2151,6 +2459,16 @@ export function apply(ctx, options) {
|
|
|
2151
2459
|
)
|
|
2152
2460
|
tasks.set(id, tasks.get(id) + "\n\nFollow-up assignment:\n" + text(message));
|
|
2153
2461
|
}
|
|
2462
|
+
if (!isChild && (!taskHeads.has(id) || newTurn)) {
|
|
2463
|
+
const previous = agent.session.snapshotEvents()
|
|
2464
|
+
.filter(event => event.type === "user/message")
|
|
2465
|
+
.map(event => agent.session.deriveEventMessage(event))
|
|
2466
|
+
.filter(message => message?.source.kind === "user");
|
|
2467
|
+
const pending = messages.filter(message => message.source.kind === "user");
|
|
2468
|
+
const first = previous[0] ?? pending[0], current = pending.at(-1) ?? previous.at(-1);
|
|
2469
|
+
if (first && !taskHeads.has(id)) taskHeads.set(id, shortText(text(first), 800));
|
|
2470
|
+
if (current && newTurn) turnHeads.set(id, shortText(text(current), 800));
|
|
2471
|
+
}
|
|
2154
2472
|
if (
|
|
2155
2473
|
!isChild &&
|
|
2156
2474
|
(!roots.has(id) || roots.get(id).configuredMode !== (config.apiKey ? config.mode : "native"))
|
|
@@ -2232,7 +2550,9 @@ export function apply(ctx, options) {
|
|
|
2232
2550
|
const admitted = await next();
|
|
2233
2551
|
if (admitted.kind === "reject") return admitted;
|
|
2234
2552
|
graphSteps.set(id, graphStep(id) + 1);
|
|
2235
|
-
|
|
2553
|
+
const withNotes = appendNotes(agent, admitted, { advisor, finalRequest, staleNotes });
|
|
2554
|
+
if (!isChild) admittedMessages.set(id, withNotes.messages ?? []);
|
|
2555
|
+
return withNotes;
|
|
2236
2556
|
});
|
|
2237
2557
|
ctx.on("llm/stream", async function* (options, next) {
|
|
2238
2558
|
const agent = options.sessionId ? ctx.agents.get(options.sessionId) : undefined;
|
|
@@ -2248,7 +2568,36 @@ export function apply(ctx, options) {
|
|
|
2248
2568
|
state.childRequests++;
|
|
2249
2569
|
}
|
|
2250
2570
|
const started = performance.now();
|
|
2251
|
-
let usage;
|
|
2571
|
+
let usage, cacheFingerprint, prefixReuse;
|
|
2572
|
+
const rootExecution = agent && agent.session.header.parentSession === undefined && options.purpose === undefined;
|
|
2573
|
+
const fingerprintFailed = () => {
|
|
2574
|
+
const cache = cacheSpaces.get(agent.session.id);
|
|
2575
|
+
// The last successfully fingerprinted request may no longer describe the current prefix.
|
|
2576
|
+
cache.latest = undefined;
|
|
2577
|
+
if (!cache.failed) {
|
|
2578
|
+
cache.failed = true;
|
|
2579
|
+
record({ kind: "cache_fingerprint_failed", session: agent.session.id });
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
if (rootExecution) {
|
|
2583
|
+
// The host has now committed these messages. Retries derive them from the surface itself.
|
|
2584
|
+
admittedMessages.delete(agent.session.id);
|
|
2585
|
+
checkCacheGeneration(agent);
|
|
2586
|
+
if (!cacheSpaces.has(agent.session.id))
|
|
2587
|
+
cacheSpaces.set(agent.session.id, { spaces: new Map(), latest: undefined, failed: false });
|
|
2588
|
+
try {
|
|
2589
|
+
cacheFingerprint = fingerprint(options, (message) => ctx.tokenMeter.estimateMessage(message));
|
|
2590
|
+
const previous = cacheSpaces.get(agent.session.id).spaces.get(cacheFingerprint.space);
|
|
2591
|
+
prefixReuse = { space: cacheFingerprint.space,
|
|
2592
|
+
sharedMessages: sharedPrefix(previous?.fingerprint, cacheFingerprint),
|
|
2593
|
+
estReusedTokens: reusableTokens(previous?.fingerprint, cacheFingerprint, {
|
|
2594
|
+
now: Date.now(), at: previous?.at, ttlMs: (config.cacheTtlSeconds ?? 300) * 1000,
|
|
2595
|
+
}) };
|
|
2596
|
+
} catch {
|
|
2597
|
+
cacheFingerprint = undefined;
|
|
2598
|
+
fingerprintFailed();
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2252
2601
|
const dispatchEstimate = agent ? ctx.tokenMeter.measure(agent.session).totalTokens : undefined;
|
|
2253
2602
|
if (agent) updateStats(rootOf(agent), { nativeEstimate: dispatchEstimate });
|
|
2254
2603
|
try {
|
|
@@ -2256,9 +2605,34 @@ export function apply(ctx, options) {
|
|
|
2256
2605
|
if (chunk.type === "usage") usage = chunk.usage;
|
|
2257
2606
|
yield chunk;
|
|
2258
2607
|
}
|
|
2608
|
+
if (cacheFingerprint) {
|
|
2609
|
+
try {
|
|
2610
|
+
const cache = cacheSpaces.get(agent.session.id);
|
|
2611
|
+
cache.latest = cacheFingerprint;
|
|
2612
|
+
delete cache.invalidated;
|
|
2613
|
+
cache.generation = agent.session.surface?.contentGeneration;
|
|
2614
|
+
cache.spaces.delete(cacheFingerprint.space);
|
|
2615
|
+
cache.spaces.set(cacheFingerprint.space, { fingerprint: cacheFingerprint, at: Date.now() });
|
|
2616
|
+
if (cache.spaces.size > 4) cache.spaces.delete(cache.spaces.keys().next().value);
|
|
2617
|
+
} catch {
|
|
2618
|
+
fingerprintFailed();
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2259
2621
|
} finally {
|
|
2260
2622
|
const wallMs = performance.now() - started;
|
|
2261
2623
|
const spent = agent ? account(agent, options, usage, wallMs) : undefined;
|
|
2624
|
+
if (rootExecution && options.reasoningEffort === config.adaptiveEffort && Number.isFinite(usage?.outputTokens)) {
|
|
2625
|
+
const id = agent.session.id, route = `${options.provider}|${options.model}`;
|
|
2626
|
+
if (!effortOutputs.has(id)) effortOutputs.set(id, new Map());
|
|
2627
|
+
const sample = effortOutputs.get(id).get(route) ?? { calls: 0, outputTokens: 0, reasoningTokens: 0, reasoningCalls: 0 };
|
|
2628
|
+
sample.calls++;
|
|
2629
|
+
sample.outputTokens += usage.outputTokens;
|
|
2630
|
+
if (Number.isFinite(usage.reasoningTokens)) {
|
|
2631
|
+
sample.reasoningTokens += usage.reasoningTokens;
|
|
2632
|
+
sample.reasoningCalls++;
|
|
2633
|
+
}
|
|
2634
|
+
effortOutputs.get(id).set(route, sample);
|
|
2635
|
+
}
|
|
2262
2636
|
if (agent && options.purpose === undefined && usage) lastCached.set(agent.session.id, usage.cacheReadTokens ?? 0);
|
|
2263
2637
|
if (agent && options.purpose === undefined) {
|
|
2264
2638
|
if (Number.isFinite(usage?.totalTokens) && Number.isFinite(usage?.outputTokens))
|
|
@@ -2277,6 +2651,7 @@ export function apply(ctx, options) {
|
|
|
2277
2651
|
wallMs,
|
|
2278
2652
|
...(spent ?? {}),
|
|
2279
2653
|
...(usage === undefined ? {} : { usage }),
|
|
2654
|
+
...(prefixReuse === undefined ? {} : { prefixReuse }),
|
|
2280
2655
|
});
|
|
2281
2656
|
}
|
|
2282
2657
|
});
|
|
@@ -2286,6 +2661,13 @@ export function apply(ctx, options) {
|
|
|
2286
2661
|
pendingLedgers.add(session.id);
|
|
2287
2662
|
}
|
|
2288
2663
|
if (session.header.parentSession !== undefined) return;
|
|
2664
|
+
if (event.type === "compaction/summary" || event.type === "compaction/prune" ||
|
|
2665
|
+
event.type === "image/offload" || event.surfaceOp?.op === "replace")
|
|
2666
|
+
invalidateCache(session.id, event.surfaceOp?.op === "replace" ? "surface_replaced" : event.type);
|
|
2667
|
+
if (event.type === "turn/end") {
|
|
2668
|
+
endEffortPhase(session.id, "turn_end");
|
|
2669
|
+
effortDue.delete(session.id);
|
|
2670
|
+
}
|
|
2289
2671
|
// Learning settles once per completed root turn, outside the turn: it never delays the user.
|
|
2290
2672
|
if (event.type === "turn/end" && event.data?.reason?.kind === "completed")
|
|
2291
2673
|
settleTurn(session, event.data.turn).catch((error) =>
|
|
@@ -2513,6 +2895,7 @@ export function apply(ctx, options) {
|
|
|
2513
2895
|
agent,
|
|
2514
2896
|
signal,
|
|
2515
2897
|
);
|
|
2898
|
+
if (agent.session.header.parentSession === undefined) invalidateCache(id, "context_replaced:" + reason);
|
|
2516
2899
|
record({
|
|
2517
2900
|
kind: "context_replaced",
|
|
2518
2901
|
session: id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-autoctx",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"index.js",
|
|
13
13
|
"client.js",
|
|
14
14
|
"stats.js",
|
|
15
|
+
"cachespace.js",
|
|
15
16
|
"decisions.js",
|
|
16
17
|
"routing.js",
|
|
17
18
|
"graph.js",
|
package/stats.js
CHANGED
|
@@ -6,7 +6,7 @@ export function newStats(mode,maxDecisions,pressureTokens) {
|
|
|
6
6
|
// USD by category (unpriced counts calls without a catalog price) and provider-reported cache by role.
|
|
7
7
|
cost:{executor:0,children:0,jev:0,compaction:0},unpriced:0,
|
|
8
8
|
cache:{},historySeeded:0,forks:0,deferrals:0,
|
|
9
|
-
plan:null,choices:{},learning:null,
|
|
9
|
+
plan:null,choices:{},learning:null,effortDecisions:{},effortPhases:0,effortPhaseRequests:0,estUncachedTokensAvoided:0,
|
|
10
10
|
updatedAt:0,pendingSaved:{}};
|
|
11
11
|
}
|
|
12
12
|
export function applyStatsRecord(stats,record,decisions=stats.decisions) {
|
|
@@ -51,7 +51,15 @@ export function applyStatsRecord(stats,record,decisions=stats.decisions) {
|
|
|
51
51
|
case 'turn_plan': next.plan={tag:record.tag,thinking:record.thinking??null,effort:record.effort,via:record.via,
|
|
52
52
|
probabilities:record.probabilities??null,raised:0}; break;
|
|
53
53
|
case 'effort_checkpoint': next.plan={...(stats.plan??{}),thinking:record.to,via:'checkpoint'}; break;
|
|
54
|
-
case 'effort_raised': if (stats.plan) next.plan={...stats.plan,raised:(stats.plan.raised??0)+1};
|
|
54
|
+
case 'effort_raised': if (stats.plan) next.plan={...stats.plan,raised:(stats.plan.raised??0)+1};
|
|
55
|
+
if (record.phase) next.effortPhaseRequests=(stats.effortPhaseRequests??0)+1; break;
|
|
56
|
+
case 'effort_decision':
|
|
57
|
+
next.effortDecisions={...stats.effortDecisions,
|
|
58
|
+
[record.decision]:(stats.effortDecisions?.[record.decision]??0)+1};
|
|
59
|
+
if (record.decision==='phase') next.effortPhases=(stats.effortPhases??0)+1;
|
|
60
|
+
if (record.decision==='skip' && Number.isFinite(record.uncachedTokens))
|
|
61
|
+
next.estUncachedTokensAvoided=(stats.estUncachedTokensAvoided??0)+record.uncachedTokens;
|
|
62
|
+
break;
|
|
55
63
|
case 'context_deferred': next.deferrals=stats.deferrals+1; break;
|
|
56
64
|
case 'learning': next.learning={mode:record.mode,version:record.version??null,proposed:record.proposed,
|
|
57
65
|
applied:record.applied??[],pending:record.pending??null,reason:record.reason??null}; break;
|