dsh-decision-engine 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +489 -2
- package/cordis.patch.yml +14 -0
- package/icon.svg +17 -0
- package/lib/adapters/agent.js +307 -0
- package/lib/adapters/route.js +145 -0
- package/lib/adapters/tools.js +183 -0
- package/lib/adapters/translate.js +138 -0
- package/lib/budget.js +92 -0
- package/lib/client.js +1199 -0
- package/lib/config-file.js +136 -0
- package/lib/credentials.js +187 -0
- package/lib/decision-cache.js +182 -0
- package/lib/decision-points.js +205 -0
- package/lib/engine.js +140 -0
- package/lib/index.js +2007 -0
- package/lib/points/spec.js +598 -0
- package/lib/providers.js +491 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +82 -4
- package/scripts/migrate-profile.mjs +102 -0
- package/startlux.example.yml +218 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 更名迁移助手:把 profile 里的旧包名换成 dsh-decision-engine。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要这一步:本项目从 `dsh-startlux-decision` 更名为 `dsh-decision-engine`。
|
|
5
|
+
* profile 的 `package.json` 里有两处按名字引用插件——
|
|
6
|
+
*
|
|
7
|
+
* dsh.profile.bundles[] 决定「加载哪些 bundle」
|
|
8
|
+
* dependencies{} 决定「从哪里装」
|
|
9
|
+
*
|
|
10
|
+
* 只改其中一处,DSH 就找不到插件(表现为插件从列表里消失,或启动报
|
|
11
|
+
* 无法解析模块)。包名与依赖 key 必须同时改,且 key 必须与新包名一致。
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* node scripts/migrate-profile.mjs # 预览要改什么(默认,不写文件)
|
|
15
|
+
* node scripts/migrate-profile.mjs --write # 真正写入(会先备份)
|
|
16
|
+
*
|
|
17
|
+
* 只处理 `dsh.profile.bundles` 与 `dependencies`/`devDependencies` 里的
|
|
18
|
+
* `dsh-startlux-decision`,其它字段一概不动。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { copyFile, readFile, writeFile } from "node:fs/promises";
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
const OLD_NAME = "dsh-startlux-decision";
|
|
27
|
+
const NEW_NAME = "dsh-decision-engine";
|
|
28
|
+
const WRITE = process.argv.includes("--write");
|
|
29
|
+
|
|
30
|
+
/** 候选 profile 路径:优先 desktop,其次是配置里能找到的其它 profile。 */
|
|
31
|
+
function candidateProfiles() {
|
|
32
|
+
const base = join(homedir(), ".dsh", "profiles");
|
|
33
|
+
return ["desktop", "web", "cli"].map((name) => join(base, name, "package.json"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 找到第一个存在的 profile package.json。 */
|
|
37
|
+
async function pickProfile() {
|
|
38
|
+
for (const path of candidateProfiles()) {
|
|
39
|
+
if (existsSync(path)) return path;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 计算改动。刻意不解析成 AST:profile 的 package.json 结构固定,
|
|
46
|
+
* 按文本替换既能保留原有格式与缩进,也不会把注释(JSONC)弄丢。
|
|
47
|
+
*
|
|
48
|
+
* @param text - package.json 原文。
|
|
49
|
+
* @returns `{ next, notes }`;notes 说明改了哪些位置。
|
|
50
|
+
*/
|
|
51
|
+
export function migrateProfileJson(text) {
|
|
52
|
+
const notes = [];
|
|
53
|
+
let next = text;
|
|
54
|
+
|
|
55
|
+
// 1) bundles 数组里的一项
|
|
56
|
+
const bundleLine = new RegExp(`^(\\s*)"${OLD_NAME}"(,?)$`, "m");
|
|
57
|
+
if (bundleLine.test(next)) {
|
|
58
|
+
next = next.replace(bundleLine, `$1"${NEW_NAME}"$2`);
|
|
59
|
+
notes.push(`dsh.profile.bundles: "${OLD_NAME}" → "${NEW_NAME}"`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2) 依赖键(值不变:仍指向同一份本地目录或同一版本)
|
|
63
|
+
const depLine = new RegExp(`^(\\s*)"${OLD_NAME}"(\\s*:\\s*)"([^"]*)"(,?)$`, "m");
|
|
64
|
+
if (depLine.test(next)) {
|
|
65
|
+
next = next.replace(depLine, `$1"${NEW_NAME}"$2"$3"$4`);
|
|
66
|
+
notes.push(`dependencies: "${OLD_NAME}" → "${NEW_NAME}"`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (notes.length === 0) {
|
|
70
|
+
notes.push(`没有找到 "${OLD_NAME}",profile 可能已经迁移过`);
|
|
71
|
+
}
|
|
72
|
+
return { next, notes };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const profilePath = await pickProfile();
|
|
76
|
+
if (profilePath === undefined) {
|
|
77
|
+
console.error(`未找到 profile 的 package.json(找过:${candidateProfiles().join(", ")})`);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const before = await readFile(profilePath, "utf8");
|
|
82
|
+
const { next, notes } = migrateProfileJson(before);
|
|
83
|
+
|
|
84
|
+
console.log(`profile: ${profilePath}`);
|
|
85
|
+
for (const note of notes) console.log(` - ${note}`);
|
|
86
|
+
|
|
87
|
+
if (next === before) {
|
|
88
|
+
console.log("\n无需改动。");
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!WRITE) {
|
|
93
|
+
console.log("\n以上为预览。确认无误后加 --write 真正写入(会先备份 .bak)。");
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const backup = `${profilePath}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
98
|
+
await copyFile(profilePath, backup);
|
|
99
|
+
await writeFile(profilePath, next, "utf8");
|
|
100
|
+
console.log(`\n已写入。原文件备份在:${backup}`);
|
|
101
|
+
console.log("接着执行:cd " + join(profilePath, "..") + " && pnpm install");
|
|
102
|
+
console.log("然后重启 DSH。配置(startlux.yml)不需要重填,插件会自动读取旧数据目录。");
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# dsh-decision-engine 配置模板
|
|
2
|
+
#
|
|
3
|
+
# 用法:复制到数据目录,按需修改。
|
|
4
|
+
# mkdir -p ~/.dsh/dsh-decision-engine
|
|
5
|
+
# cp startlux.example.yml ~/.dsh/dsh-decision-engine/startlux.yml
|
|
6
|
+
#
|
|
7
|
+
# 文件名刻意仍叫 startlux.yml:老版本就叫这个名字,改名会让已保存的配置
|
|
8
|
+
# (含 API key 与决策点开关)失效。插件会自动读取以下位置,取第一个存在的:
|
|
9
|
+
# ~/.dsh/dsh-decision-engine/startlux.yml (首选位置)
|
|
10
|
+
# ~/.dsh/dsh-startlux-decision/startlux.yml (上一代;首次启动会自动拷到新目录)
|
|
11
|
+
# ~/.dsh/plugins/dsh-startlux-decision/startlux.yml
|
|
12
|
+
# ~/.dsh/plugins/dsh-startlux-guard/startlux.yml
|
|
13
|
+
#
|
|
14
|
+
# 数据目录(插件自动读取):
|
|
15
|
+
# <上述目录>/startlux.yml 配置
|
|
16
|
+
# <上述目录>/telemetry.jsonl 决策记录
|
|
17
|
+
#
|
|
18
|
+
# 改完立即生效:每次工具调用 / 每一步前都会检查 mtime,无需重启 DSH。
|
|
19
|
+
|
|
20
|
+
# 总开关:true 才开始工作(默认 false,不会擅自介入你的会话)
|
|
21
|
+
enabled: false
|
|
22
|
+
|
|
23
|
+
# shadow = 只记录判断结果,绝不拦截(建议先用它观察一段时间)
|
|
24
|
+
# enforce = 真正执行各决策点的处置
|
|
25
|
+
#
|
|
26
|
+
# 注意:这一项现在只是「工具授权」这一个决策点的历史开关。
|
|
27
|
+
# 新写法是给每个决策点单独指定模式,见下面的 pointXxx。
|
|
28
|
+
# pointAuthorize 留 auto 时,就沿用这里的 mode(shadow→observe、enforce→enforce)。
|
|
29
|
+
mode: shadow
|
|
30
|
+
|
|
31
|
+
# ============================================================================
|
|
32
|
+
# 服务提供方(provider)
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# 三选一:
|
|
35
|
+
# bamboonode 内置。StartLux 官方端点 POST /v1/decisions,本插件的原有默认值。
|
|
36
|
+
# systemone 内置。任意 System One 协议服务 POST /v1/systemone,
|
|
37
|
+
# 默认指向 TypeSafe(https://api.typesafe.ai/v1)。
|
|
38
|
+
# custom 自己填 base URL,接任何 system-one 风格服务
|
|
39
|
+
# (自建 llama.cpp / vLLM、企业网关、第三方中转)。
|
|
40
|
+
#
|
|
41
|
+
# 不写这一项 = bamboonode,老配置行为完全不变。
|
|
42
|
+
provider: bamboonode
|
|
43
|
+
|
|
44
|
+
# ---- bamboonode 预设的地址覆盖 ----------------------------------------------
|
|
45
|
+
# 只在 provider=bamboonode 时生效。老版本唯一的地址字段,继续支持,因此
|
|
46
|
+
# 已有的自建 bamboonode 兼容地址不会因为升级被改回官方。
|
|
47
|
+
# 接受多种写法(等价):
|
|
48
|
+
# https://api.bamboonode.dev/v1
|
|
49
|
+
# https://api.bamboonode.dev/v1/decisions
|
|
50
|
+
# api.bamboonode.dev/v1 (会补 https://)
|
|
51
|
+
# 末尾斜杠会被规范化,不会拼出 //decisions 或 /v1/v1/decisions。
|
|
52
|
+
baseUrl: https://api.bamboonode.dev/v1
|
|
53
|
+
|
|
54
|
+
# ---- 模型(按 provider 分开存,切换时互不覆盖)------------------------------
|
|
55
|
+
# bamboonode 的模型。留空 = 自动(由服务端选当前默认决策模型),
|
|
56
|
+
# 这样服务商将来换模型名时,本配置不会失效。
|
|
57
|
+
model:
|
|
58
|
+
|
|
59
|
+
# system one 的模型。留空 = 服务端默认(jev-latest)。
|
|
60
|
+
# 与上面的 model 分开:否则切到 system one 会把 bamboonode 的模型名发过去,
|
|
61
|
+
# 得到 400 model not served。
|
|
62
|
+
systemoneModel: ""
|
|
63
|
+
|
|
64
|
+
# ---- custom 预设(provider=custom 时才生效)--------------------------------
|
|
65
|
+
# base URL 必填,且必须是 http/https 地址;不带 scheme 会被拒绝(内置预设
|
|
66
|
+
# 允许省略 scheme,custom 不允许——自定义地址必须显式,避免猜错协议)。
|
|
67
|
+
# customBaseUrl: https://api.example.com/v1
|
|
68
|
+
#
|
|
69
|
+
# 端点风格:
|
|
70
|
+
# auto 地址已带 /decisions 或 /systemone 就原样用,否则按 decisions 拼
|
|
71
|
+
# decisions POST <base>/decisions
|
|
72
|
+
# systemone POST <base>/systemone
|
|
73
|
+
customEndpointStyle: auto
|
|
74
|
+
#
|
|
75
|
+
# custom 自己的模型(留空 = 服务端默认)。与内置预设的 model 分开存,
|
|
76
|
+
# 这样在两者之间来回切换不会互相覆盖。
|
|
77
|
+
customModel: ""
|
|
78
|
+
# custom 的 key;留空则读 customApiKeyEnv(环境变量或 .credentials.yaml 的 refs)
|
|
79
|
+
customApiKey: ""
|
|
80
|
+
customApiKeyEnv: ""
|
|
81
|
+
# 附加请求头,格式 `名称: 值`,多项用分号分隔。
|
|
82
|
+
# authorization / content-type / host 由插件固定,写了也会被忽略。
|
|
83
|
+
customHeaders: ""
|
|
84
|
+
|
|
85
|
+
# ============================================================================
|
|
86
|
+
# 密钥
|
|
87
|
+
# ============================================================================
|
|
88
|
+
# 每个 provider 有**自己的** key 名,各自保存各自的 key:
|
|
89
|
+
#
|
|
90
|
+
# system one -> TYPESAFE_API_KEY
|
|
91
|
+
# bamboonode -> BAMBOO_API_KEY
|
|
92
|
+
# custom -> customApiKeyEnv(没填时用 DECISION_ENGINE_CUSTOM_KEY)
|
|
93
|
+
#
|
|
94
|
+
# 三者互不覆盖,可以同时配好;切换 provider 不会串用别人的 key。
|
|
95
|
+
|
|
96
|
+
# 密钥存到哪里:
|
|
97
|
+
# credentials = ~/.dsh/.credentials.yaml 的 refs 段(默认,与其他服务统一托管)
|
|
98
|
+
# config = 本文件(自包含,便于随目录备份;注意这里是明文)
|
|
99
|
+
keyStorage: credentials
|
|
100
|
+
|
|
101
|
+
# 变量名可以覆盖(留空即用上面的默认名):
|
|
102
|
+
# systemoneApiKeyEnv: ""
|
|
103
|
+
# bamboonodeApiKeyEnv: ""
|
|
104
|
+
# customApiKeyEnv: ""
|
|
105
|
+
|
|
106
|
+
# 直接写明文 key 也可以(各 provider 一格,互不影响)。
|
|
107
|
+
# 若 keyStorage=credentials,在设置界面粘贴的 key 会写进 .credentials.yaml,
|
|
108
|
+
# 不会落到这里;只有 keyStorage=config 时下面这些字段才会被写入。
|
|
109
|
+
# systemoneApiKey: ""
|
|
110
|
+
# bamboonodeApiKey: ""
|
|
111
|
+
# customApiKey: ""
|
|
112
|
+
|
|
113
|
+
# 历史字段:老版本的单一 key 槽位,仅对 bamboonode 生效(保留兼容,不建议再用)。
|
|
114
|
+
apiKey: ""
|
|
115
|
+
apiKeyEnv: BAMBOO_API_KEY
|
|
116
|
+
|
|
117
|
+
# .credentials.yaml 里的形态就是 refs 段:
|
|
118
|
+
# refs:
|
|
119
|
+
# BAMBOO_API_KEY: sk-你的key
|
|
120
|
+
# TYPESAFE_API_KEY: sk-另一个key
|
|
121
|
+
|
|
122
|
+
# 超时预算(毫秒)。超时自动降级放行,连续失败进入冷却。
|
|
123
|
+
timeoutMs: 8000
|
|
124
|
+
|
|
125
|
+
# never = 不思考,最省 token(默认);auto = 仅在不确定时思考
|
|
126
|
+
thinkMode: never
|
|
127
|
+
|
|
128
|
+
# ============================================================================
|
|
129
|
+
# 决策模型参与点
|
|
130
|
+
# ============================================================================
|
|
131
|
+
# 三态:off 不参与 / observe 只记录不干预 / enforce 真正生效。
|
|
132
|
+
# 九个点全部已实装。新点默认 off —— 升级不会改变现有行为。
|
|
133
|
+
#
|
|
134
|
+
# 建议:先 observe 跑一段时间,对着 telemetry.jsonl 确认与人工判断一致,
|
|
135
|
+
# 再逐点切 enforce。热路径的点(route / delegate / cotTranslate)尤其如此。
|
|
136
|
+
|
|
137
|
+
# ---- 1. 工具授权:每次工具调用前判断能否放行 -------------------------------
|
|
138
|
+
# 替代 auto-review——后者把完整会话历史发给主模型,每次调用都跑一遍。
|
|
139
|
+
pointAuthorize: auto
|
|
140
|
+
|
|
141
|
+
# ---- 2. 模型路由:机械步骤下沉到便宜模型 -----------------------------------
|
|
142
|
+
# 热路径。三道闸控量:每轮最多 routeMaxStepsPerTurn 步、路由未变不问、
|
|
143
|
+
# 新用户输入后的步骤不问。
|
|
144
|
+
# 必须同时配好 routeCheapProvider + routeCheapModel 才会真的换模型;
|
|
145
|
+
# 两者留空时即便打开此点也只会记录「本该换」。
|
|
146
|
+
pointRoute: off
|
|
147
|
+
routeCheapProvider: ""
|
|
148
|
+
routeCheapModel: ""
|
|
149
|
+
# routeMaxStepsPerTurn: 3
|
|
150
|
+
# routeMaxScore: 0.4 # 能力需求归一化后超过此值就保留强模型
|
|
151
|
+
# routeMechanicalFloor: 0.6 # 「机械步骤」的最低置信
|
|
152
|
+
# routeRiskFloor: 0.3 # 风险达线一律保留强模型
|
|
153
|
+
|
|
154
|
+
# ---- 3. 子任务分派:接着做 / 开 subagent / 组 agent team -------------------
|
|
155
|
+
# 热路径。只在每轮第一步且带新用户输入时判断(分派只在这一刻可能改变)。
|
|
156
|
+
# 生效时注入一条提示,不替模型决定——工具选择仍归模型。
|
|
157
|
+
pointDelegate: off
|
|
158
|
+
# delegateConfidenceFloor: 0.5 # 三选项均分 0.33,实测需 ≥0.5 才算有倾向
|
|
159
|
+
# delegateBlockingFloor: 0.6 # 被本地事实阻塞时继续内联
|
|
160
|
+
# delegateParallelFloor: 0.5 # 组队还需真并行才有意义
|
|
161
|
+
|
|
162
|
+
# ---- 4. 上下文压缩:摘要 vs 只丢陈旧工具结果 -------------------------------
|
|
163
|
+
# 非热路径。只在实测 token 达触发线(compactThresholdRatio × 窗口)才问。
|
|
164
|
+
# 生效方式:先跑宿主的确定性工具结果裁剪;compaction-basic 随后重新测压,
|
|
165
|
+
# 低于阈值就跳过摘要——这才是真正省下的那一次调用。
|
|
166
|
+
pointCompact: off
|
|
167
|
+
# compactContextWindow: 0 # 0 = 用 compactTriggerTokens 绝对阈值
|
|
168
|
+
# compactThresholdRatio: 0.7 # 提前于 compaction-basic 的 0.8 介入
|
|
169
|
+
# compactTriggerTokens: 100000
|
|
170
|
+
# compactToolResultChars: 8192
|
|
171
|
+
# compactDistilledFloor: 0.6 # 结论已落地 → 允许只裁剪
|
|
172
|
+
# compactVerbatinFloor: 0.6 # 仍需逐字原文 → 一律摘要(安全否决优先)
|
|
173
|
+
|
|
174
|
+
# ---- 5. 轮次延续:再来一轮有没有增量价值 -----------------------------------
|
|
175
|
+
# 非热路径。生效方式是用 agent.steer() 续一轮,每轮最多 turnStopMaxSteers 次。
|
|
176
|
+
pointTurnStop: off
|
|
177
|
+
# turnStopMaxSteers: 1
|
|
178
|
+
# turnStopClosedFloor: 0.6
|
|
179
|
+
# turnStopUnfinishedFloor: 0.6
|
|
180
|
+
# turnStopValueFloor: 0.35 # 只作弱确认;主判据是 unfinished
|
|
181
|
+
|
|
182
|
+
# ---- 6. 检索必要性:这条信息是不是已经知道 ---------------------------------
|
|
183
|
+
# 非热路径。只对检索类工具生效,其余工具零开销。
|
|
184
|
+
pointRetrieval: off
|
|
185
|
+
# retrievalTools: [web_search, web_fetch, research, multi_search, advanced_search]
|
|
186
|
+
# retrievalCoveredFloor: 0.5 # 上下文已覆盖该事实
|
|
187
|
+
# retrievalWouldAddCeiling: 0.8 # 且再搜基本没有新信息
|
|
188
|
+
|
|
189
|
+
# ---- 7. 技能选择:该装哪个技能 ---------------------------------------------
|
|
190
|
+
# 非热路径。只对 skill 工具、且候选 ≥2 时判断。
|
|
191
|
+
# 生效时拦下不匹配的加载并指路,避免「装错再换」付两遍正文 token。
|
|
192
|
+
pointSkillPick: off
|
|
193
|
+
# skillPickCandidates: 8 # 送给模型挑选的候选上限(只带名称+描述)
|
|
194
|
+
# skillPickConfidenceFloor: 0.5
|
|
195
|
+
|
|
196
|
+
# ---- 8. 思维链翻译门槛:这条内容真的需要翻译吗 -----------------------------
|
|
197
|
+
# 热路径(每条消息)。本地先做免费预筛(去代码块/URL 后判断有无正文),
|
|
198
|
+
# 预筛不过的直接跳过,不发请求。生效时判为「不需要」则返回原文。
|
|
199
|
+
pointCotTranslate: off
|
|
200
|
+
# cotProseFloor: 0.5
|
|
201
|
+
# cotSameLanguageFloor: 0.8
|
|
202
|
+
|
|
203
|
+
# ============================================================================
|
|
204
|
+
# 自身开销护栏
|
|
205
|
+
# ============================================================================
|
|
206
|
+
# 决策模型自己也烧 token(实测约 1.9k/次,p50 约 0.5s)。
|
|
207
|
+
# 这是每小时上限,超出后所有决策点自动退回启发式并放行——
|
|
208
|
+
# 省钱的东西不能反过来变成主要消耗方。0 表示不限制。
|
|
209
|
+
budgetTokens: 400000
|
|
210
|
+
budgetWindowMs: 3600000
|
|
211
|
+
|
|
212
|
+
# ---- 以下为可选调优项 ----
|
|
213
|
+
# cacheSize: 256 # 缓存条数上限
|
|
214
|
+
# cacheTtlMs: 600000 # 裁决存活时间
|
|
215
|
+
# recentMessages: 4 # 带给模型的上下文条数
|
|
216
|
+
# guardReadOnlyTools: [read, grep, glob, todo_write]
|
|
217
|
+
# guardDenyConfidence: 0.85
|
|
218
|
+
# guardIrreversibleCeiling: 0.2
|