dsh-agentic-router 1.3.0 → 1.4.1
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 +27 -18
- package/README.md +50 -20
- package/docs/DESIGN.md +3 -1
- package/docs/MAINTAINING.md +74 -0
- package/dsh/index.js +271 -79
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
DeepSeek Harness (DSH) plugin: a **learning agentic router with a data flywheel**.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
At the start of every turn (`agent/inbox/claimed`) it routes the request to the most appropriate model tier by task type and complexity, asks four experts in parallel and lets an EXP3 meta-selector decide who to trust. Every turn is scored with **quality-proxy signals** (tool failures, model retries, cost, latency) and the reward feeds back into the strategies — the router gets better as it runs. Every decision and reward is persisted locally: auditable and replayable.
|
|
8
|
+
|
|
9
|
+
**Real switching since v1.4.0**: v1.3.0 and earlier swapped models by returning a replaced config from the `agent/request` waterfall, but DSH's built-in model-selection listener overrides that value, so the swap never took effect. v1.4.0 instead appends a `request/header` event (reason: `router`) to the session log at turn start, going through DSH's official model-selection precedence chain (in-process selection → latest logged session `request/header` → default model).
|
|
8
10
|
|
|
9
11
|
## Architecture
|
|
10
12
|
|
|
11
13
|
```
|
|
12
|
-
user input → classify (rules, <1ms) → 4 expert recommendations → EXP3 meta-pick →
|
|
13
|
-
|
|
14
|
-
UCB/LinUCB/k-NN/EXP3 updates ← quality-proxy reward ← turn end (tool errors/retries/latency)
|
|
14
|
+
user input → classify (rules, <1ms) → 4 expert recommendations → EXP3 meta-pick → request/header append (real switch)
|
|
15
|
+
↑ │
|
|
16
|
+
UCB/LinUCB/k-NN/EXP3 updates ← quality-proxy reward ← turn end (tool errors/retries/cost/latency)
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
| Expert | Algorithm | Status |
|
|
@@ -30,7 +32,26 @@ Design rationale and roadmap: [docs/DESIGN.md](./docs/DESIGN.md). The implementa
|
|
|
30
32
|
dsh plugin --profile web add dsh-agentic-router
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
> `--profile` is mandatory. Restart the
|
|
35
|
+
> `--profile` is mandatory. Restart the web profile after installing so the tool schemas join prompt assembly.
|
|
36
|
+
|
|
37
|
+
### Switch to real routing (active)
|
|
38
|
+
|
|
39
|
+
Default mode is **shadow** (records and learns, never swaps). To enable real switching, the easiest way is: after restarting, tell the agent in any DSH session:
|
|
40
|
+
|
|
41
|
+
> Switch the agentic router to active mode
|
|
42
|
+
|
|
43
|
+
The agent calls `agentic_router_set_mode` (mode=active) and persists it to
|
|
44
|
+
`~/.dsh/storages/dsh-agentic-router/policy.json` — it stays active across restarts.
|
|
45
|
+
|
|
46
|
+
Alternatively, start active on first load by adding config in the profile's `cordis.patch.yml`:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
- id: agentic-router
|
|
50
|
+
config:
|
|
51
|
+
mode: active
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
(Pick one; once a mode has been saved via the tool, `policy.json` takes precedence over `config.mode`.)
|
|
34
55
|
|
|
35
56
|
## Tools
|
|
36
57
|
|
|
@@ -78,19 +99,7 @@ the `agentic_router_set_prices` tool (persisted to policy.json) or at install vi
|
|
|
78
99
|
- Feedback only works where the deployment's Web UI actually writes it (this plugin only reads)
|
|
79
100
|
- LinUCB / k-NN need dozens of turns before they graduate into the meta pool
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```sh
|
|
84
|
-
npm test # node --test, zero dependencies
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Publish
|
|
88
|
-
|
|
89
|
-
```sh
|
|
90
|
-
npm publish # requires npm account
|
|
91
|
-
git push origin main # after making the GitHub repo public
|
|
92
|
-
# directory listing: add the dsh-plugin topic, or submit at dshmarketplace.dev/submit
|
|
93
|
-
```
|
|
102
|
+
**Development, testing and release flow (maintainers): see [docs/MAINTAINING.md](./docs/MAINTAINING.md).**
|
|
94
103
|
|
|
95
104
|
## License
|
|
96
105
|
|
package/README.md
CHANGED
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
DeepSeek Harness(DSH)插件:**学习型智能路由(agentic router)+ 数据飞轮**。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
每回合开始时(`agent/inbox/claimed`)按任务类型与复杂度把请求路由到最合适的模型档位;
|
|
8
|
+
四个专家并行推荐、EXP3 元选择器决定听谁的;每回合按**质量代理信号**
|
|
9
|
+
(工具失败、模型重试、成本、延迟)计算奖励并回传,路由策略随使用越学越准。
|
|
10
|
+
全部决策与奖励落盘,可审计、可重放。
|
|
11
|
+
|
|
12
|
+
**v1.4.0 起切换真实生效**:v1.3.0 及之前通过 `agent/request` 返回替换后的配置切模型,
|
|
13
|
+
但该返回值会被 DSH 内置的模型选择监听器强制改回,实际不生效;v1.4.0 改为在回合开始
|
|
14
|
+
向会话日志追加 `request/header` 事件(reason: `router`),走 DSH 官方模型选择优先级链
|
|
15
|
+
(本进程选择 → 会话日志最新 header → 默认模型)真实切换。
|
|
8
16
|
|
|
9
17
|
## 架构
|
|
10
18
|
|
|
11
19
|
```
|
|
12
|
-
用户输入 → 分类(规则,<1ms) → 四专家推荐 → EXP3 元选择 →
|
|
20
|
+
用户输入 → 分类(规则,<1ms) → 四专家推荐 → EXP3 元选择 → request/header 追加(真实切换)
|
|
13
21
|
↑ │
|
|
14
|
-
UCB/LinUCB/k-NN/EXP3 更新 ← 质量代理奖励 ← 回合收尾(
|
|
22
|
+
UCB/LinUCB/k-NN/EXP3 更新 ← 质量代理奖励 ← 回合收尾(工具失败/重试/成本/延迟)
|
|
15
23
|
```
|
|
16
24
|
|
|
17
25
|
| 专家 | 算法 | 状态 |
|
|
@@ -22,7 +30,7 @@ DeepSeek Harness(DSH)插件:**学习型智能路由(agentic router)+
|
|
|
22
30
|
| k-NN | 经验池最近邻(k=5)聚合 | 影子,30 经验毕业 |
|
|
23
31
|
| EXP3 | 元选择器,重要性加权更新 | 常驻,冷启动偏 rule(权重 2.5) |
|
|
24
32
|
|
|
25
|
-
设计细节与路线图见 [docs/DESIGN.md](./docs/DESIGN.md)
|
|
33
|
+
设计细节与路线图见 [docs/DESIGN.md](./docs/DESIGN.md)。
|
|
26
34
|
|
|
27
35
|
## 安装
|
|
28
36
|
|
|
@@ -30,7 +38,43 @@ DeepSeek Harness(DSH)插件:**学习型智能路由(agentic router)+
|
|
|
30
38
|
dsh plugin --profile web add dsh-agentic-router
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
> `--profile`
|
|
41
|
+
> `--profile` 必填。安装后重启 Web,工具 schema 才会进入 prompt 组装。
|
|
42
|
+
|
|
43
|
+
### 切换到真实切换(active)
|
|
44
|
+
|
|
45
|
+
默认是 **shadow 模式**(只记录与学习,不切换模型)。切到真实切换,最省事的方式:重启后在任意 DSH 会话里对 agent 说:
|
|
46
|
+
|
|
47
|
+
> 把 agentic router 切成 active 模式
|
|
48
|
+
|
|
49
|
+
agent 会调用 `agentic_router_set_mode`(mode=active),并把模式持久化到
|
|
50
|
+
`~/.dsh/storages/dsh-agentic-router/policy.json`——**重启后依然 active**,无需每次设置。
|
|
51
|
+
|
|
52
|
+
也可以不走会话、首次启动即 active:在 profile 的 `cordis.patch.yml` 里给
|
|
53
|
+
`agentic-router` 条目加配置:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
- id: agentic-router
|
|
57
|
+
config:
|
|
58
|
+
mode: active
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(两种方式选其一;一旦用工具存过模式,`policy.json` 的取值优先于 `config.mode`。)
|
|
62
|
+
|
|
63
|
+
## 切换机制(v1.4.0)
|
|
64
|
+
|
|
65
|
+
DSH 的 agent 每步请求最终由模型选择逻辑决定,读取优先级为:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
本进程选择 → 会话日志最新 request/header → 默认模型
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- 直接改 `agent/request` waterfall 的返回值:会被 `dsh-agent` 自带监听器强制改回「选定模型」——无效(v1.3.0 及之前的缺陷)
|
|
72
|
+
- 改 `llm/stream`:agent-loop 请求到达时已深度冻结,无法改写
|
|
73
|
+
|
|
74
|
+
v1.4.0 的正确做法:在 `agent/inbox/claimed`(回合开始、prompt 组装之前)向会话日志追加
|
|
75
|
+
`request/header` 事件,下一轮组装自然读到新模型。`agent/request` 只做只读观测
|
|
76
|
+
(记录每步真实出网模型);插件中途热加载时由 `llm/stream` 在循环自身 header 落盘后补一次应用。
|
|
77
|
+
切到 fast/mid 档会去掉 `reasoningEffort`,让目标模型走自身默认 effort。
|
|
34
78
|
|
|
35
79
|
## 工具
|
|
36
80
|
|
|
@@ -80,21 +124,7 @@ dsh plugin --profile web add dsh-agentic-router
|
|
|
80
124
|
- 反馈依赖部署的 Web UI 实际产生数据(本插件只读取,不写入)
|
|
81
125
|
- 毕业策略(LinUCB/k-NN)需要数十个回合样本才会进元层池
|
|
82
126
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```sh
|
|
86
|
-
npm test # node --test,零依赖
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
回归用例覆盖实测缺陷:qa 祈使句、mid 档回退、UCB 冷启动轮转、agent 键隔离、奖励 JSON 合法性、飞轮重放恢复等。
|
|
90
|
-
|
|
91
|
-
## 发布
|
|
92
|
-
|
|
93
|
-
```sh
|
|
94
|
-
npm publish # 需 npm 账号
|
|
95
|
-
git push origin main # GitHub 仓库公开后
|
|
96
|
-
# 目录站:给仓库加 dsh-plugin topic 自动收录,或到 dshmarketplace.dev/submit 提交
|
|
97
|
-
```
|
|
127
|
+
**开发、测试与发布流程(维护者)见 [docs/MAINTAINING.md](./docs/MAINTAINING.md)。**
|
|
98
128
|
|
|
99
129
|
## License
|
|
100
130
|
|
package/docs/DESIGN.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
> 目标:在 DeepSeek Harness 里做一个**学习型智能路由**插件——监听用户输入,按任务类型与复杂度把请求路由到最合适的模型 / 子 agent / 工具组合;每次路由的**决策与结果全部落盘**,形成数据飞轮,让路由判断随使用越来越准、越来越快。
|
|
4
4
|
|
|
5
|
+
> ⚠️ **v1.4.0 机制修正**:下文 1.1 的 `agent/request` 返回替换配置方案已废弃——实测该返回值会被 `dsh-agent` 内置的模型选择监听器强制改回「选定模型」,不生效。v1.4.0 改为在 `agent/inbox/claimed` 向会话日志追加 `request/header`(reason: `router`),走官方模型选择优先级链真实切换;`agent/request` 降级为只读观测,`llm/stream` 提供中途热加载的兜底应用。实现见 `dsh/index.js` 顶部注释与 README「切换机制」一节。
|
|
6
|
+
|
|
5
7
|
## 0. 一句话架构
|
|
6
8
|
|
|
7
9
|
```
|
|
8
|
-
用户输入 → 特征提取(快) → 路由器决策 →
|
|
10
|
+
用户输入 → 特征提取(快) → 路由器决策 → request/header 追加(真实切换) / 派发子 agent
|
|
9
11
|
↑ ↓
|
|
10
12
|
策略(规则表+学习权重) ← 评分与在线学习 ← 结果回传(质量/成本/延迟落盘)
|
|
11
13
|
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 维护者手册
|
|
2
|
+
|
|
3
|
+
> 读者:本仓库的维护者/贡献者。安装使用者请看根目录 README。
|
|
4
|
+
> 原则:**main 同时是 `github:` 安装源与目录站文档源,不要直接推 main;npm 版本不可变、可精确回滚,是正式发布门禁。**
|
|
5
|
+
|
|
6
|
+
## 一次性设置
|
|
7
|
+
|
|
8
|
+
### 1. GitHub 分支保护
|
|
9
|
+
|
|
10
|
+
仓库 Settings → Branches → Add branch protection rule:
|
|
11
|
+
|
|
12
|
+
- Branch name pattern:`main`
|
|
13
|
+
- ✅ Require a pull request before merging
|
|
14
|
+
- ✅ Require status checks to pass before merging → 选 `ci`
|
|
15
|
+
- ✅ Require linear history(可选,保持历史干净)
|
|
16
|
+
|
|
17
|
+
### 2. CI 发布令牌
|
|
18
|
+
|
|
19
|
+
`.github/workflows/release.yml` 在 `v*` tag 上自动测试并 `npm publish`,需要一个 npm 令牌:
|
|
20
|
+
|
|
21
|
+
1. npmjs.com → Access Tokens → Generate New Token → **Granular Access Token**
|
|
22
|
+
2. 权限选本包 **Read and write**;Token 类型必须选 **Automation**(可绕过 2FA,官方为 CI 设计)
|
|
23
|
+
3. GitHub 仓库 Settings → Secrets and variables → Actions → New repository secret
|
|
24
|
+
- Name:`NPM_TOKEN`
|
|
25
|
+
- Value:`npm_xxxxxxxx...`
|
|
26
|
+
|
|
27
|
+
### 3. 目录站收录
|
|
28
|
+
|
|
29
|
+
仓库 About → Topics 添加 `dsh-plugin`(目录站周期同步自动收录);也可主动到
|
|
30
|
+
<https://dshmarketplace.dev/submit> 提交仓库 URL(人工审核)。
|
|
31
|
+
|
|
32
|
+
## 开发与测试
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm test # node --test,零依赖
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
回归用例覆盖实测缺陷:qa 祈使句、mid 档回退、UCB 冷启动轮转、agent 键隔离、
|
|
39
|
+
奖励 JSON 合法性、飞轮重放恢复、反馈结算时序、多 provider 价格解析等。
|
|
40
|
+
|
|
41
|
+
背景:核心逻辑先在真实运行时的动态原型上逐阶段验证(P0 监听落盘 → P1 真实切换 →
|
|
42
|
+
P2 三策略飞轮 → P3 质量/成本/反馈奖励),共发现并修复 8 个实测缺陷后才沉淀为本仓库,
|
|
43
|
+
每个缺陷都对应一条回归测试。改动后请先本地 `npm test` 全绿再开 PR。
|
|
44
|
+
|
|
45
|
+
## 日常发布流程
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# 1. 功能分支开发(不要直接在 main 提交)
|
|
49
|
+
git checkout -b feat/xxx
|
|
50
|
+
git push -u origin feat/xxx # 开 PR,ci 检查绿了再合并
|
|
51
|
+
|
|
52
|
+
# 2. 合并到 main 后发布
|
|
53
|
+
git checkout main && git pull
|
|
54
|
+
npm version patch # bump 版本 + 自动打 vX.Y.Z tag + 提交
|
|
55
|
+
git push origin main --follow-tags # 推送后 CI 自动跑测试并发布 npm
|
|
56
|
+
|
|
57
|
+
# 重大变更用 minor(npm version minor),不兼容用 major
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 发布回滚
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# npm:版本不可变,出问题直接回滚 tag 指向旧版本
|
|
64
|
+
npm dist-tag add dsh-agentic-router@1.2.0 latest
|
|
65
|
+
|
|
66
|
+
# 源码:直接 revert 再走一遍发布流程
|
|
67
|
+
git revert <坏提交> && npm version patch && git push origin main --follow-tags
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 版本号约定(SemVer)
|
|
71
|
+
|
|
72
|
+
- **patch**:bug 修复、文档、测试(用户无感知)
|
|
73
|
+
- **minor**:新功能(如新增工具、新奖励信号)
|
|
74
|
+
- **major**:破坏性变更(工具改名、数据格式不兼容)
|
package/dsh/index.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
// dsh-agentic-router —— DeepSeek Harness 插件(Host 侧)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// 学习型智能路由(v1.4.0 修复版):按任务类型与复杂度在模型档位(fast/mid/strong)
|
|
4
|
+
// 之间路由;四个专家(规则表 / UCB / LinUCB / k-NN)并行推荐,EXP3 元选择器决定
|
|
5
|
+
// 听谁的;回合结束时按质量代理信号(工具失败 + 模型重试 + 成本 + 延迟)计算奖励
|
|
6
|
+
// 并回传飞轮;人类 👍/👎 反馈在结算窗口后修正奖励。
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// ── 切换机制(v1.4.0 修复)────────────────────────────────────────────
|
|
9
|
+
// v1.3.0 及之前通过 `agent/request` waterfall 返回替换后的 LlmCallConfig 来切模型,
|
|
10
|
+
// 但该返回值会被 dsh-agent 内置的模型选择监听器强制改回「选定模型」,实际不生效。
|
|
11
|
+
// 正确做法是走 dsh 官方的模型选择优先级链:
|
|
12
|
+
//
|
|
13
|
+
// 本进程选择 → 会话日志最新 request/header → 默认模型
|
|
14
|
+
//
|
|
15
|
+
// 在 `agent/inbox/claimed`(回合开始、prompt 组装之前)向会话日志追加一条
|
|
16
|
+
// `request/header` 事件(reason: 'router'),下一轮组装就会读到新模型,真正生效。
|
|
17
|
+
// `agent/request` 只做只读观测(记录每步实际出网模型);插件中途热加载时,
|
|
18
|
+
// `llm/stream` 在循环自身 header 落盘后补一次应用(deferred apply)。
|
|
19
|
+
//
|
|
20
|
+
// 其余架构依据 docs/architecture.md;所有监听经 ctx.on 注册,随插件卸载自动回收。
|
|
12
21
|
// 飞轮数据落盘 ${DSH_HOME:-~/.dsh}/storages/dsh-agentic-router/,重启后重放恢复。
|
|
13
22
|
|
|
14
23
|
import { classify, featuresOf } from './classify.js'
|
|
@@ -29,6 +38,22 @@ const KNN_GRADUATION = 30 // k-NN 毕业经验线
|
|
|
29
38
|
const SETTLE_DELAY_MS = 45000 // 反馈结算窗口:回合结束 45s 后查询 messageFeedback
|
|
30
39
|
const SWEEP_MS = 15000 // 结算轮询周期
|
|
31
40
|
|
|
41
|
+
// 同步兜底档位表:从 DEFAULT_PRICES 已知的 provider→model 推导档位,
|
|
42
|
+
// 让插件在 llm.listModels 异步加载完成之前也能立即决策与切换(不 await、不延期)。
|
|
43
|
+
// 加载完成后由 refreshModelTiers 用权威目录覆盖。
|
|
44
|
+
function fallbackTiers() {
|
|
45
|
+
const tiers = { fast: [], mid: [], strong: [] }
|
|
46
|
+
const providers = []
|
|
47
|
+
for (const provider of Object.keys(DEFAULT_PRICES)) {
|
|
48
|
+
if (provider === '*') continue
|
|
49
|
+
const models = Object.keys(DEFAULT_PRICES[provider] || {})
|
|
50
|
+
if (!models.length) continue
|
|
51
|
+
providers.push({ provider, models })
|
|
52
|
+
for (const n of models) tiers[tierOf(n)].push(provider + '/' + n)
|
|
53
|
+
}
|
|
54
|
+
return { providers, tiers }
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
function freshState() {
|
|
33
58
|
return {
|
|
34
59
|
mode: 'shadow',
|
|
@@ -36,11 +61,13 @@ function freshState() {
|
|
|
36
61
|
decisions: [],
|
|
37
62
|
forced: null,
|
|
38
63
|
pending: new Map(),
|
|
39
|
-
modelTiers:
|
|
64
|
+
modelTiers: fallbackTiers(),
|
|
40
65
|
turnDecision: {},
|
|
41
66
|
openTurns: new Map(),
|
|
42
67
|
pendingFeedback: [],
|
|
43
68
|
recByTurn: new Map(),
|
|
69
|
+
headerAppends: 0,
|
|
70
|
+
lastActual: null,
|
|
44
71
|
ucb: { total: 0, arms: {} },
|
|
45
72
|
lin: {
|
|
46
73
|
total: 0,
|
|
@@ -87,6 +114,20 @@ function textOfMessages(messages) {
|
|
|
87
114
|
return parts.join('\n').trim()
|
|
88
115
|
}
|
|
89
116
|
|
|
117
|
+
function extractText(message) {
|
|
118
|
+
if (message == null) return ''
|
|
119
|
+
if (typeof message.text === 'string') return message.text
|
|
120
|
+
const blocks = message.content
|
|
121
|
+
if (Array.isArray(blocks)) {
|
|
122
|
+
let out = ''
|
|
123
|
+
for (const b of blocks) {
|
|
124
|
+
if (b && typeof b === 'object' && b.type === 'text' && typeof b.text === 'string') out += b.text + '\n'
|
|
125
|
+
}
|
|
126
|
+
return out
|
|
127
|
+
}
|
|
128
|
+
return ''
|
|
129
|
+
}
|
|
130
|
+
|
|
90
131
|
function isErrorResult(r) {
|
|
91
132
|
if (r == null || typeof r !== 'object') return false
|
|
92
133
|
if (r.ok === false || r.success === false) return true
|
|
@@ -110,6 +151,57 @@ function execAgentKey(exec) {
|
|
|
110
151
|
return null
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
// 会话句柄:agent 的 session(inbox / request 路径)
|
|
155
|
+
function sessionOf(payload) {
|
|
156
|
+
try {
|
|
157
|
+
const a = payload && payload.agent
|
|
158
|
+
const s = a && a.session
|
|
159
|
+
if (s && typeof s.append === 'function' && typeof s.requestHeader === 'function' && typeof s.id === 'string') return s
|
|
160
|
+
} catch {}
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function headerConfig(session) {
|
|
165
|
+
try {
|
|
166
|
+
const h = session.requestHeader()
|
|
167
|
+
if (h && h.config && typeof h.config === 'object') return h.config
|
|
168
|
+
} catch {}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function currentModel(session) {
|
|
173
|
+
const c = headerConfig(session)
|
|
174
|
+
return c && typeof c.model === 'string' ? c.model : null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function currentProvider(session) {
|
|
178
|
+
const c = headerConfig(session)
|
|
179
|
+
return c && typeof c.provider === 'string' ? c.provider : ''
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 核心切换:向会话日志追加 request/header(reason: 'router'),
|
|
183
|
+
// 下一轮 prompt 组装即读到新模型。fast/mid 档去掉 reasoningEffort,
|
|
184
|
+
// 让目标模型走自身默认 effort(避免 flash 不支持 max 而整步失败)。
|
|
185
|
+
function appendRouterHeader(session, targetModel) {
|
|
186
|
+
try {
|
|
187
|
+
const last = session.requestHeader()
|
|
188
|
+
const header = {}
|
|
189
|
+
if (last && typeof last === 'object') {
|
|
190
|
+
for (const k of Object.keys(last)) header[k] = last[k]
|
|
191
|
+
}
|
|
192
|
+
const prevConfig = header.config && typeof header.config === 'object' ? header.config : {}
|
|
193
|
+
const config = {}
|
|
194
|
+
for (const k of Object.keys(prevConfig)) config[k] = prevConfig[k]
|
|
195
|
+
config.model = targetModel
|
|
196
|
+
if (tierOf(targetModel) !== 'strong') delete config.reasoningEffort
|
|
197
|
+
header.config = config
|
|
198
|
+
session.append('request/header', { header, reason: 'router' })
|
|
199
|
+
return true
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
113
205
|
async function refreshModelTiers(llm) {
|
|
114
206
|
if (!llm || typeof llm.listProviders !== 'function') return null
|
|
115
207
|
const providers = await llm.listProviders()
|
|
@@ -209,7 +301,115 @@ export function apply(ctx, config = {}) {
|
|
|
209
301
|
timer.interval(() => { settleFeedback().catch((e) => log('settle sweep failed: ' + String(e))) }, SWEEP_MS)
|
|
210
302
|
}
|
|
211
303
|
|
|
304
|
+
// ---- 决策流水线(inbox 与 request 兜底共用) ----
|
|
305
|
+
// 返回 { key, td, record, target };已决策过本回合则返回 null。
|
|
306
|
+
// 必须同步执行:档位表用 fallbackTiers() 兜底(见 freshState),
|
|
307
|
+
// 不 await、不延期——否则追加 header 会落到 prompt 组装之后,被 loop 覆盖。
|
|
308
|
+
function decideAndRecord(payload, text, current, provider, source) {
|
|
309
|
+
const key = fullKey(payload)
|
|
310
|
+
if (state.turnDecision[key]) return null
|
|
311
|
+
const cls = classify(text)
|
|
312
|
+
const f = featuresOf(cls, text)
|
|
313
|
+
const recs = {
|
|
314
|
+
rule: recommendRule(cls),
|
|
315
|
+
ucb: recommendUcb(cls, state.ucb),
|
|
316
|
+
lin: recommendLin(cls, f, state.lin),
|
|
317
|
+
knn: recommendKnn(cls, f, state.knn),
|
|
318
|
+
}
|
|
319
|
+
if (state.forced) recs.rule = { tier: 'forced', reason: 'forced', conf: 1 }
|
|
320
|
+
const exp3 = {
|
|
321
|
+
...state.exp3,
|
|
322
|
+
linGraduated: state.lin.total >= LIN_GRADUATION,
|
|
323
|
+
knnGraduated: state.knn.pool.length >= KNN_GRADUATION,
|
|
324
|
+
}
|
|
325
|
+
const meta = metaPick(recs, exp3)
|
|
326
|
+
const chosenRec = meta.byName[meta.chosen] || recs.rule
|
|
327
|
+
const resolved = state.forced
|
|
328
|
+
? { target: state.forced, used: 'forced', note: 'forced' }
|
|
329
|
+
: resolveTier(chosenRec.tier, provider, state.modelTiers)
|
|
330
|
+
const target = resolved.target
|
|
331
|
+
state.seq += 1
|
|
332
|
+
const td = {
|
|
333
|
+
clsType: cls.type, f, recs, meta, chosen: meta.chosen,
|
|
334
|
+
finalized: false, t0: now(), toolErrors: 0, retries: 0,
|
|
335
|
+
currentModel: current, provider, costWeight, costCap,
|
|
336
|
+
estInputTokens: Math.ceil(text.length / 1.5),
|
|
337
|
+
target, applied: false, needsApply: false, actualModel: current,
|
|
338
|
+
}
|
|
339
|
+
const record = {
|
|
340
|
+
seq: state.seq,
|
|
341
|
+
turn: key,
|
|
342
|
+
mode: state.mode,
|
|
343
|
+
taskType: cls.type,
|
|
344
|
+
complexity: cls.complexity,
|
|
345
|
+
inputPreview: text.slice(0, 100),
|
|
346
|
+
currentModel: current,
|
|
347
|
+
recs: {
|
|
348
|
+
rule: recs.rule.tier + '(' + recs.rule.conf + ')',
|
|
349
|
+
ucb: recs.ucb.tier + '(' + recs.ucb.conf + ')',
|
|
350
|
+
lin: recs.lin.tier + '(' + recs.lin.conf + ')',
|
|
351
|
+
knn: recs.knn.tier + '(' + recs.knn.conf + ')',
|
|
352
|
+
},
|
|
353
|
+
metaChosen: meta.chosen,
|
|
354
|
+
metaProbs: meta.ps,
|
|
355
|
+
metaPool: meta.pool,
|
|
356
|
+
resolvedTier: resolved.used + (resolved.note ? ' [' + resolved.note + ']' : ''),
|
|
357
|
+
targetModel: state.mode === 'active' ? target : null,
|
|
358
|
+
applied: false,
|
|
359
|
+
appliedAt: source,
|
|
360
|
+
swapped: false,
|
|
361
|
+
}
|
|
362
|
+
td.rec = record
|
|
363
|
+
state.turnDecision[key] = td
|
|
364
|
+
state.openTurns.set(agentKey(payload), key)
|
|
365
|
+
return { key, td, record, target }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 决策记录落盘(在 applyDecision 之后调用,保证 applied/swapped 已是最终值)
|
|
369
|
+
function commitRecord(record) {
|
|
370
|
+
store.appendJsonl('decisions.jsonl', record)
|
|
371
|
+
state.decisions.push(record)
|
|
372
|
+
if (state.decisions.length > 200) state.decisions.shift()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 应用切换:active 模式且目标与当前不同 → 追加 request/header。
|
|
376
|
+
// 返回是否真实追加成功。
|
|
377
|
+
function applyDecision(session, key, td, record, target) {
|
|
378
|
+
if (state.mode !== 'active' || !target) return false
|
|
379
|
+
const base = td.actualModel || td.currentModel
|
|
380
|
+
if (base && target === base) return false
|
|
381
|
+
const ok = session ? appendRouterHeader(session, target) : false
|
|
382
|
+
td.applied = ok
|
|
383
|
+
if (record) { record.applied = ok; record.swapped = ok }
|
|
384
|
+
if (ok) {
|
|
385
|
+
state.headerAppends++
|
|
386
|
+
log('SWAP -> ' + target + ' | ' + td.chosen + ' | ' + (td.recs && td.recs[td.chosen] ? td.recs[td.chosen].reason : ''))
|
|
387
|
+
}
|
|
388
|
+
return ok
|
|
389
|
+
}
|
|
390
|
+
|
|
212
391
|
// ---- 事件挂载 ----
|
|
392
|
+
// 回合开始(prompt 组装之前):捕获用户消息并决策,active 模式立即应用切换。
|
|
393
|
+
// 这是主路径——零延迟,回合第一步就按目标模型执行。
|
|
394
|
+
ctx.on('agent/inbox/claimed', (payload) => {
|
|
395
|
+
try {
|
|
396
|
+
const session = sessionOf(payload)
|
|
397
|
+
const text = extractText(payload && payload.message)
|
|
398
|
+
if (!text) return
|
|
399
|
+
const current = session ? currentModel(session) : null
|
|
400
|
+
const provider = session ? currentProvider(session) : ''
|
|
401
|
+
const r = decideAndRecord(payload, text, current, provider, 'inbox')
|
|
402
|
+
if (!r) return
|
|
403
|
+
applyDecision(session, r.key, r.td, r.record, r.target)
|
|
404
|
+
commitRecord(r.record)
|
|
405
|
+
log('decision #' + state.seq + ' type=' + r.td.clsType + ' c=' + r.td.f[1] +
|
|
406
|
+
' meta=' + r.td.chosen + ' tier=' + r.record.resolvedTier +
|
|
407
|
+
' applied=' + r.td.applied + ' target=' + (r.target || '-'))
|
|
408
|
+
} catch (e) {
|
|
409
|
+
log('inbox decide failed: ' + String(e))
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
213
413
|
ctx.on('agent/pre-step', (payload, next) => {
|
|
214
414
|
try {
|
|
215
415
|
const text = textOfMessages(payload && payload.messages)
|
|
@@ -220,79 +420,35 @@ export function apply(ctx, config = {}) {
|
|
|
220
420
|
return next()
|
|
221
421
|
})
|
|
222
422
|
|
|
423
|
+
// 只读观测 + 兜底决策。观测:记录每步真实出网模型(供 stats 验证切换生效)。
|
|
424
|
+
// 兜底:inbox 未触发(插件中途热加载)时,在首个请求用 pending 文本决策,
|
|
425
|
+
// 但不再替换配置——改由 llm/stream 在循环 header 落盘后补应用。
|
|
223
426
|
ctx.on('agent/request', async (payload, next) => {
|
|
224
427
|
const cfg = await next()
|
|
225
428
|
if (!cfg || typeof cfg !== 'object') return cfg
|
|
226
429
|
try {
|
|
227
430
|
if (!timer || typeof timer.interval !== 'function') settleFeedback().catch(() => {})
|
|
228
431
|
const key = fullKey(payload)
|
|
229
|
-
|
|
432
|
+
const model = typeof cfg.model === 'string' ? cfg.model : null
|
|
433
|
+
const td = state.turnDecision[key]
|
|
434
|
+
if (td) {
|
|
435
|
+
if (model) {
|
|
436
|
+
td.actualModel = model
|
|
437
|
+
state.lastActual = { model, turn: payload.turn, step: payload.step, key }
|
|
438
|
+
}
|
|
439
|
+
return cfg
|
|
440
|
+
}
|
|
230
441
|
const pkey = agentKey(payload) + ':' + payload.turn
|
|
231
442
|
const text = state.pending.get(pkey) || ''
|
|
232
443
|
state.pending.delete(pkey)
|
|
233
|
-
const cls = classify(text)
|
|
234
|
-
const f = featuresOf(cls, text)
|
|
235
|
-
const current = typeof cfg.model === 'string' ? cfg.model : '(unknown)'
|
|
236
444
|
const provider = typeof cfg.provider === 'string' ? cfg.provider : ''
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (state.forced) recs.rule = { tier: 'forced', reason: 'forced', conf: 1 }
|
|
244
|
-
const exp3 = {
|
|
245
|
-
...state.exp3,
|
|
246
|
-
linGraduated: state.lin.total >= LIN_GRADUATION,
|
|
247
|
-
knnGraduated: state.knn.pool.length >= KNN_GRADUATION,
|
|
248
|
-
}
|
|
249
|
-
const meta = metaPick(recs, exp3)
|
|
250
|
-
const chosenRec = meta.byName[meta.chosen] || recs.rule
|
|
251
|
-
const resolved = state.forced
|
|
252
|
-
? { target: state.forced, used: 'forced', note: 'forced' }
|
|
253
|
-
: resolveTier(chosenRec.tier, provider, state.modelTiers)
|
|
254
|
-
const target = resolved.target
|
|
255
|
-
state.seq += 1
|
|
256
|
-
const record = {
|
|
257
|
-
seq: state.seq,
|
|
258
|
-
turn: key,
|
|
259
|
-
mode: state.mode,
|
|
260
|
-
taskType: cls.type,
|
|
261
|
-
complexity: cls.complexity,
|
|
262
|
-
inputPreview: text.slice(0, 100),
|
|
263
|
-
currentModel: current,
|
|
264
|
-
recs: {
|
|
265
|
-
rule: recs.rule.tier + '(' + recs.rule.conf + ')',
|
|
266
|
-
ucb: recs.ucb.tier + '(' + recs.ucb.conf + ')',
|
|
267
|
-
lin: recs.lin.tier + '(' + recs.lin.conf + ')',
|
|
268
|
-
knn: recs.knn.tier + '(' + recs.knn.conf + ')',
|
|
269
|
-
},
|
|
270
|
-
metaChosen: meta.chosen,
|
|
271
|
-
metaProbs: meta.ps,
|
|
272
|
-
metaPool: meta.pool,
|
|
273
|
-
resolvedTier: resolved.used + (resolved.note ? ' [' + resolved.note + ']' : ''),
|
|
274
|
-
targetModel: state.mode === 'active' ? target : null,
|
|
275
|
-
swapped: false,
|
|
276
|
-
}
|
|
277
|
-
state.turnDecision[key] = {
|
|
278
|
-
clsType: cls.type, f, recs, meta, chosen: meta.chosen,
|
|
279
|
-
finalized: false, t0: now(), toolErrors: 0, retries: 0,
|
|
280
|
-
currentModel: current, provider, costWeight, costCap,
|
|
281
|
-
estInputTokens: Math.ceil(text.length / 1.5),
|
|
282
|
-
}
|
|
283
|
-
state.openTurns.set(agentKey(payload), key)
|
|
284
|
-
store.appendJsonl('decisions.jsonl', record)
|
|
285
|
-
if (state.mode === 'active' && target && target !== current) {
|
|
286
|
-
record.swapped = true
|
|
287
|
-
state.decisions.push(record)
|
|
288
|
-
if (state.decisions.length > 200) state.decisions.shift()
|
|
289
|
-
log('SWAP -> ' + target + ' | ' + meta.chosen + ' | ' + chosenRec.reason + ' | ' + resolved.note)
|
|
290
|
-
return { ...cfg, model: target }
|
|
445
|
+
const r = decideAndRecord(payload, text, model, provider, 'request')
|
|
446
|
+
if (r) {
|
|
447
|
+
r.td.needsApply = state.mode === 'active' && r.target && r.target !== r.td.currentModel
|
|
448
|
+
commitRecord(r.record)
|
|
449
|
+
if (r.td.needsApply) log('decision #' + state.seq + ' -> apply deferred to llm/stream')
|
|
450
|
+
else log('decision #' + state.seq + ' type=' + r.td.clsType + ' meta=' + r.td.chosen + ' tier=' + r.record.resolvedTier)
|
|
291
451
|
}
|
|
292
|
-
state.decisions.push(record)
|
|
293
|
-
if (state.decisions.length > 200) state.decisions.shift()
|
|
294
|
-
log('decision #' + state.seq + ' type=' + cls.type + ' c=' + cls.complexity +
|
|
295
|
-
' meta=' + meta.chosen + ' tier=' + resolved.used + ' recs=' + JSON.stringify(record.recs))
|
|
296
452
|
} catch (e) {
|
|
297
453
|
log('decide failed: ' + String(e))
|
|
298
454
|
}
|
|
@@ -313,7 +469,8 @@ export function apply(ctx, config = {}) {
|
|
|
313
469
|
})
|
|
314
470
|
|
|
315
471
|
// 成本信号:透传每次流式模型调用,累加 usage chunk 到当前 open turn;
|
|
316
|
-
// 无 usage 的 provider 用文本长度估算(estimated=true
|
|
472
|
+
// 无 usage 的 provider 用文本长度估算(estimated=true)。
|
|
473
|
+
// 同时:request 路径的兜底决策在这里补应用切换(循环自身 header 已落盘,无竞态)。
|
|
317
474
|
ctx.on('llm/stream', function (options, next) {
|
|
318
475
|
return (async function* () {
|
|
319
476
|
let usage = null
|
|
@@ -330,7 +487,19 @@ export function apply(ctx, config = {}) {
|
|
|
330
487
|
const key = sid ? state.openTurns.get(sid) : null
|
|
331
488
|
const td = key ? state.turnDecision[key] : null
|
|
332
489
|
if (td && !td.finalized) {
|
|
490
|
+
// 兜底应用:request 路径决策未应用时,在 header 落盘后补切换
|
|
491
|
+
if (td.needsApply && !td.applied) {
|
|
492
|
+
let session = null
|
|
493
|
+
try {
|
|
494
|
+
const sessions = ctx.get('sessions')
|
|
495
|
+
if (sessions && typeof sessions.get === 'function') session = sessions.get(sid) || null
|
|
496
|
+
} catch (e) { session = null }
|
|
497
|
+
if (session) applyDecision(session, key, td, td.rec, td.target)
|
|
498
|
+
}
|
|
333
499
|
const u = td.usage || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, reasoningTokens: 0, estimated: false }
|
|
500
|
+
const model = (options && typeof options.model === 'string' && options.model) || td.currentModel || ''
|
|
501
|
+
const tier = tierOf(model)
|
|
502
|
+
const provider = (options && typeof options.provider === 'string' && options.provider) || td.provider || ''
|
|
334
503
|
if (usage && typeof usage.inputTokens === 'number') {
|
|
335
504
|
u.inputTokens += usage.inputTokens
|
|
336
505
|
u.outputTokens += usage.outputTokens || 0
|
|
@@ -343,9 +512,6 @@ export function apply(ctx, config = {}) {
|
|
|
343
512
|
u.estimated = true
|
|
344
513
|
}
|
|
345
514
|
td.usage = u
|
|
346
|
-
const model = typeof td.currentModel === 'string' ? td.currentModel : ''
|
|
347
|
-
const tier = tierOf(model)
|
|
348
|
-
const provider = (options && typeof options.provider === 'string' && options.provider) || td.provider || ''
|
|
349
515
|
const rp = resolvePrice(provider, model, tier, prices)
|
|
350
516
|
td.costYuan = turnCostYuan(u, provider, model, tier, prices)
|
|
351
517
|
td.pricingSource = rp.source
|
|
@@ -419,10 +585,13 @@ export function apply(ctx, config = {}) {
|
|
|
419
585
|
ctx.on('agent/turn-stopping', (payload) => finalize(payload, 'clean-close'))
|
|
420
586
|
ctx.on('agent/error', (payload) => finalize(payload, 'error'))
|
|
421
587
|
|
|
422
|
-
|
|
423
|
-
|
|
588
|
+
// 权威档位表异步加载,就绪后覆盖同步兜底表(fallbackTiers 保证首回合即可决策)。
|
|
589
|
+
// 仅在刷新成功时覆盖,避免把兜底表清成 null。
|
|
590
|
+
const tiersReady = refreshModelTiers(llm)
|
|
591
|
+
tiersReady.then((t) => {
|
|
592
|
+
if (t) state.modelTiers = t
|
|
424
593
|
log('model tiers: ' + JSON.stringify(t))
|
|
425
|
-
})
|
|
594
|
+
}).catch((e) => log('model tiers refresh failed: ' + String(e)))
|
|
426
595
|
|
|
427
596
|
// ---- 工具 ----
|
|
428
597
|
const OUT = (label) => ({
|
|
@@ -440,7 +609,7 @@ export function apply(ctx, config = {}) {
|
|
|
440
609
|
|
|
441
610
|
register({
|
|
442
611
|
name: 'agentic_router_stats',
|
|
443
|
-
description: '查看 agentic router(UCB/LinUCB/k-NN 三策略 + EXP3 元选择器 +
|
|
612
|
+
description: '查看 agentic router(UCB/LinUCB/k-NN 三策略 + EXP3 元选择器 + 质量代理奖励)的决策记录、策略权重、真实切换与飞轮学习状态。',
|
|
444
613
|
parameters: { type: 'object', properties: {} },
|
|
445
614
|
output: OUT('agentic router 状态'),
|
|
446
615
|
isConcurrencySafe: () => true,
|
|
@@ -451,6 +620,9 @@ export function apply(ctx, config = {}) {
|
|
|
451
620
|
totalDecisions: state.seq,
|
|
452
621
|
modelTiers: state.modelTiers,
|
|
453
622
|
prices,
|
|
623
|
+
mechanism: 'request/header append (agent/inbox/claimed)',
|
|
624
|
+
lastActual: state.lastActual,
|
|
625
|
+
headerAppends: state.headerAppends,
|
|
454
626
|
flywheel: {
|
|
455
627
|
ucb: state.ucb,
|
|
456
628
|
lin: { total: state.lin.total, tierN: state.lin.tierN, tierW: state.lin.tierW },
|
|
@@ -461,6 +633,7 @@ export function apply(ctx, config = {}) {
|
|
|
461
633
|
recentRewards: store.loadJsonl('rewards.jsonl').slice(-20),
|
|
462
634
|
},
|
|
463
635
|
recent: state.decisions.slice(-20),
|
|
636
|
+
note: '切换真实性验证:headerAppends 为本进程追加的 request/header 次数;lastActual 为最近一步真实出网模型;decisions[].applied=true 且 swapped=true 即真实切换成功',
|
|
464
637
|
}
|
|
465
638
|
},
|
|
466
639
|
})
|
|
@@ -501,6 +674,23 @@ export function apply(ctx, config = {}) {
|
|
|
501
674
|
if (args && typeof args.model === 'string') {
|
|
502
675
|
state.forced = args.model
|
|
503
676
|
store.savePolicy({ mode: state.mode, forced: args.model, prices })
|
|
677
|
+
// 立即对存活会话应用(下一请求生效)
|
|
678
|
+
if (state.mode === 'active') {
|
|
679
|
+
try {
|
|
680
|
+
const agents = ctx.get('agents')
|
|
681
|
+
if (agents && typeof agents.list === 'function') {
|
|
682
|
+
for (const agent of agents.list()) {
|
|
683
|
+
try {
|
|
684
|
+
const session = sessionOf({ agent })
|
|
685
|
+
if (session && currentModel(session) !== args.model) {
|
|
686
|
+
const ok = appendRouterHeader(session, args.model)
|
|
687
|
+
if (ok) state.headerAppends++
|
|
688
|
+
}
|
|
689
|
+
} catch (e) { /* per-agent best effort */ }
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
} catch (e) { /* best effort */ }
|
|
693
|
+
}
|
|
504
694
|
return { forced: args.model }
|
|
505
695
|
}
|
|
506
696
|
return { forced: state.forced, error: '需要 model 或 clear=true' }
|
|
@@ -552,6 +742,8 @@ export function apply(ctx, config = {}) {
|
|
|
552
742
|
state.exp3 = { total: 0, weights: { ...DEFAULT_WEIGHTS } }
|
|
553
743
|
state.seq = 0
|
|
554
744
|
state.decisions = []
|
|
745
|
+
state.headerAppends = 0
|
|
746
|
+
state.lastActual = null
|
|
555
747
|
return { reset: true, home: store.home }
|
|
556
748
|
},
|
|
557
749
|
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentic-router",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DeepSeek Harness plugin: learning agentic router — rule + UCB + LinUCB + k-NN experts under an EXP3 meta-selector, with a durable quality-reward flywheel",
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "DeepSeek Harness plugin: learning agentic router — rule + UCB + LinUCB + k-NN experts under an EXP3 meta-selector, with a durable quality-reward flywheel and REAL model switching via the session request-header precedence chain (v1.4.1, cold-start fix)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "lhl",
|