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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 决策点注册表。
|
|
3
|
+
*
|
|
4
|
+
* 一个「决策点」= 一个已经设计好问题与输出映射的适配器,而不是一个裸 hook。
|
|
5
|
+
* 裸 hook 名对用户没有意义:勾上 `agent/request` 之后要回答「换成哪个模型」,
|
|
6
|
+
* 勾上 `agent/pre-step` 要回答「注入什么消息」——这些答案必须由适配器给出。
|
|
7
|
+
*
|
|
8
|
+
* 因此配置页列的是这里定义的决策点,hook 只是它的实现细节。
|
|
9
|
+
*
|
|
10
|
+
* 字段说明:
|
|
11
|
+
* id 稳定标识,写进配置(pointAuthorize)与遥测(point 字段)
|
|
12
|
+
* hook 底层挂载的 DSH 事件名(或它替代的宿主服务)
|
|
13
|
+
* hotPath true = 每步/每次模型调用都触发,延迟敏感,需要预取或触发条件
|
|
14
|
+
* available false = 尚未实装,配置页显示为「即将支持」;本版本全部为 true
|
|
15
|
+
* defaultMode 三态默认值
|
|
16
|
+
* gate 触发闸说明:说明为什么不是「每次都问」,是冷却/采样/条件判断哪一种
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 三态:off 不参与;observe 只记录不生效;enforce 真正生效。 */
|
|
20
|
+
export const POINT_MODES = ["off", "observe", "enforce"];
|
|
21
|
+
|
|
22
|
+
/** @type {ReadonlyArray<object>} */
|
|
23
|
+
export const DECISION_POINTS = [
|
|
24
|
+
{
|
|
25
|
+
id: "authorize",
|
|
26
|
+
key: "pointAuthorize",
|
|
27
|
+
hook: "tools/pre-execute",
|
|
28
|
+
hotPath: false,
|
|
29
|
+
available: true,
|
|
30
|
+
defaultMode: "observe",
|
|
31
|
+
trigger: "每次工具调用",
|
|
32
|
+
triggerEn: "every tool call",
|
|
33
|
+
label: "工具授权",
|
|
34
|
+
labelEn: "Tool authorization",
|
|
35
|
+
summary:
|
|
36
|
+
"判断这次工具调用能否放行。可替代 auto-review——后者把完整会话历史发给主模型,每次工具调用都跑一遍。",
|
|
37
|
+
summaryEn:
|
|
38
|
+
"Decide whether this tool call may proceed. Can replace auto-review, which sends the full session history to the main model on every call.",
|
|
39
|
+
saving: "替代 auto-review:每次约省 60k token",
|
|
40
|
+
savingEn: "Replaces auto-review: saves ~60k tokens per call",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "route",
|
|
44
|
+
key: "pointRoute",
|
|
45
|
+
hook: "agent/request",
|
|
46
|
+
hotPath: true,
|
|
47
|
+
available: true,
|
|
48
|
+
defaultMode: "off",
|
|
49
|
+
gate: "同一轮内同一 provider/model 只问一次;机械步骤按命令族签名复用裁决",
|
|
50
|
+
trigger: "每次模型请求",
|
|
51
|
+
triggerEn: "every model request",
|
|
52
|
+
label: "模型路由",
|
|
53
|
+
labelEn: "Model routing",
|
|
54
|
+
summary:
|
|
55
|
+
"逐步判断这一步值不值得用强模型:机械步骤(读结果、改格式、重试)下沉到便宜模型。",
|
|
56
|
+
summaryEn:
|
|
57
|
+
"Decide per step whether the strong model is warranted; route mechanical steps to a cheaper one.",
|
|
58
|
+
saving: "主通道约省 20–40% token",
|
|
59
|
+
savingEn: "~20–40% of main-channel tokens",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "delegate",
|
|
63
|
+
key: "pointDelegate",
|
|
64
|
+
hook: "agent/pre-step",
|
|
65
|
+
hotPath: true,
|
|
66
|
+
available: true,
|
|
67
|
+
defaultMode: "off",
|
|
68
|
+
gate: "仅每轮第一步且带新用户输入时判断(分派决策只在这一刻可能改变)",
|
|
69
|
+
trigger: "每步",
|
|
70
|
+
triggerEn: "every step",
|
|
71
|
+
label: "子任务分派",
|
|
72
|
+
labelEn: "Subtask delegation",
|
|
73
|
+
summary:
|
|
74
|
+
"判断该自己接着做、开 subagent、还是组 agent team。每开一个子代理都等于新开一整个上下文。",
|
|
75
|
+
summaryEn:
|
|
76
|
+
"Decide whether to continue inline, spawn a subagent, or form an agent team. Each spawn costs a whole fresh context.",
|
|
77
|
+
saving: "少开一次 = 省一整个上下文",
|
|
78
|
+
savingEn: "One avoided spawn = one whole context saved",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "compact",
|
|
82
|
+
key: "pointCompact",
|
|
83
|
+
hook: "agent/pre-step → ctx.compaction",
|
|
84
|
+
hotPath: false,
|
|
85
|
+
available: true,
|
|
86
|
+
defaultMode: "off",
|
|
87
|
+
gate: "仅当实测 token 已达阈值触发线的 70% 才问(compaction-basic 的 0.8 阈值)",
|
|
88
|
+
trigger: "接近上下文阈值",
|
|
89
|
+
triggerEn: "approaching the context limit",
|
|
90
|
+
label: "上下文压缩",
|
|
91
|
+
labelEn: "Context compaction",
|
|
92
|
+
summary:
|
|
93
|
+
"判断现在该摘要压缩,还是只丢掉几条已经没用的工具结果——后者不花摘要的钱。",
|
|
94
|
+
summaryEn:
|
|
95
|
+
"Decide whether to summarize now, or simply drop a few stale tool results without paying for a summary.",
|
|
96
|
+
saving: "省掉一次摘要调用",
|
|
97
|
+
savingEn: "Skips one summarisation call",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "turnStop",
|
|
101
|
+
key: "pointTurnStop",
|
|
102
|
+
hook: "agent/turn-stopping",
|
|
103
|
+
hotPath: false,
|
|
104
|
+
available: true,
|
|
105
|
+
defaultMode: "off",
|
|
106
|
+
gate: "同一轮最多续一次(steer 次数上限),避免无限自续",
|
|
107
|
+
trigger: "每轮结束",
|
|
108
|
+
triggerEn: "every turn end",
|
|
109
|
+
label: "轮次延续",
|
|
110
|
+
labelEn: "Turn continuation",
|
|
111
|
+
summary: "判断再来一轮还有没有增量价值,避免空转一整轮。",
|
|
112
|
+
summaryEn: "Decide whether another round adds value, avoiding a wasted full turn.",
|
|
113
|
+
saving: "省一整个 turn 的上下文",
|
|
114
|
+
savingEn: "Saves one full turn of context",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "retrieval",
|
|
118
|
+
key: "pointRetrieval",
|
|
119
|
+
hook: "tools/pre-execute",
|
|
120
|
+
hotPath: false,
|
|
121
|
+
available: true,
|
|
122
|
+
defaultMode: "off",
|
|
123
|
+
gate: "仅检索类工具(web_search/web_fetch/research 等)才问;其余工具零开销",
|
|
124
|
+
trigger: "每次搜索/抓取",
|
|
125
|
+
triggerEn: "every search or fetch",
|
|
126
|
+
label: "检索必要性",
|
|
127
|
+
labelEn: "Retrieval necessity",
|
|
128
|
+
summary: "判断这条信息是否已经知道、或能否直接推断,避免不必要的搜索与抓取。",
|
|
129
|
+
summaryEn: "Decide whether the information is already known or inferable, avoiding needless fetches.",
|
|
130
|
+
saving: "省搜索与结果 token",
|
|
131
|
+
savingEn: "Saves search and result tokens",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "skillPick",
|
|
135
|
+
key: "pointSkillPick",
|
|
136
|
+
hook: "tools/pre-execute",
|
|
137
|
+
hotPath: false,
|
|
138
|
+
available: true,
|
|
139
|
+
defaultMode: "off",
|
|
140
|
+
gate: "仅 `skill` 工具加载时问,且候选清单非空才问",
|
|
141
|
+
trigger: "每次加载技能",
|
|
142
|
+
triggerEn: "every skill load",
|
|
143
|
+
label: "技能选择",
|
|
144
|
+
labelEn: "Skill selection",
|
|
145
|
+
summary: "判断该装哪个技能。装错再换等于付两遍技能正文的 token。",
|
|
146
|
+
summaryEn: "Decide which skill to load; a wrong pick pays the skill body twice.",
|
|
147
|
+
saving: "省 2–5k / 次误加载",
|
|
148
|
+
savingEn: "Saves 2–5k per wrong load",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "cotTranslate",
|
|
152
|
+
key: "pointCotTranslate",
|
|
153
|
+
hook: "cotTranslation.translate()",
|
|
154
|
+
hotPath: true,
|
|
155
|
+
available: true,
|
|
156
|
+
defaultMode: "off",
|
|
157
|
+
gate: "超长文本采样头部判断;同一文本摘要命中缓存即复用",
|
|
158
|
+
trigger: "每条消息",
|
|
159
|
+
triggerEn: "every message",
|
|
160
|
+
label: "思维链翻译门槛",
|
|
161
|
+
labelEn: "COT translation gate",
|
|
162
|
+
summary: "判断这条内容是否真的需要翻译,避免逐条调用翻译模型。",
|
|
163
|
+
summaryEn: "Decide whether content truly needs translation, avoiding a model call per message.",
|
|
164
|
+
saving: "按条省翻译调用",
|
|
165
|
+
savingEn: "Saves per-message translation calls",
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
/** 按 id 取决策点。 */
|
|
170
|
+
export function findPoint(id) {
|
|
171
|
+
return DECISION_POINTS.find((point) => point.id === id);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 把配置里的 `pointXxx` 字段读成一张 id → 模式 的表。
|
|
176
|
+
*
|
|
177
|
+
* 未知值一律退回 off:宁可少判断一次,也不要因为配置写错而在热路径上
|
|
178
|
+
* 意外开始发请求。
|
|
179
|
+
*
|
|
180
|
+
* @param cfg - 已合并的配置。
|
|
181
|
+
* @returns 记录每个决策点当前模式的普通对象。
|
|
182
|
+
*/
|
|
183
|
+
export function readPointModes(cfg) {
|
|
184
|
+
/** @type {Record<string, string>} */
|
|
185
|
+
const modes = {};
|
|
186
|
+
for (const point of DECISION_POINTS) {
|
|
187
|
+
const raw = cfg?.[point.key];
|
|
188
|
+
modes[point.id] = POINT_MODES.includes(raw) ? raw : "off";
|
|
189
|
+
}
|
|
190
|
+
return modes;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 某个决策点此刻是否应当真的调用决策模型。
|
|
195
|
+
* observe 也调用(否则无从记录「本该怎样」),只是不改变行为。
|
|
196
|
+
*/
|
|
197
|
+
export function shouldInvoke(cfg, id) {
|
|
198
|
+
const mode = readPointModes(cfg)[id];
|
|
199
|
+
return mode === "observe" || mode === "enforce";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 某个决策点此刻是否允许改变行为。 */
|
|
203
|
+
export function shouldEnforce(cfg, id) {
|
|
204
|
+
return readPointModes(cfg)[id] === "enforce";
|
|
205
|
+
}
|
package/lib/engine.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 决策点执行引擎。
|
|
3
|
+
*
|
|
4
|
+
* 「工具授权」那一个点是手写的,因为它要处理 ask/deny 这类改变行为的返回。
|
|
5
|
+
* 另外八个点如果各写一遍「查模式 → 判预算 → 发请求 → 校验答案 → 写遥测」,
|
|
6
|
+
* 八份重复代码里迟早有一份漏掉降级分支——而漏掉降级分支的后果是工具调用被卡住。
|
|
7
|
+
*
|
|
8
|
+
* 所以这里把那条链路收成一个 `ask()`:
|
|
9
|
+
*
|
|
10
|
+
* 模式 off → 不发请求,返回 { mode: 'off' }
|
|
11
|
+
* 预算/冷却/no-key → client 已降级为 null,这里记一条 degraded 遥测
|
|
12
|
+
* 答案形状不符 → 记 answer-shape-mismatch,按「不改变行为」处理
|
|
13
|
+
* 有裁决 → 记一条 action 遥测,返回 action 交给适配器执行
|
|
14
|
+
*
|
|
15
|
+
* 适配器只负责两件事:给出 state/questions(spec.build),以及把 action 变成
|
|
16
|
+
* 真实的副作用。所有决策点共用同一套超时、预算、缓存与降级语义。
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-decision-engine/engine
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** 一个决策点在某次触发下「什么都不做」的结果。 */
|
|
22
|
+
const NOOP = Object.freeze({ mode: "off", action: undefined });
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param options.getConfig - 返回当前合并配置的取值函数(配置每次现读)。
|
|
26
|
+
* @param options.client - StartluxClient 实例。
|
|
27
|
+
* @param options.logger - 宿主 logger,可缺省。
|
|
28
|
+
* @param options.modeOf - `(cfg, id) => 'off'|'observe'|'enforce'`。
|
|
29
|
+
* @param options.log - `(entry) => Promise<void>`,写 telemetry.jsonl。
|
|
30
|
+
* @param options.specOf - `(id) => spec`,从注册表取 build/parse/decide。
|
|
31
|
+
*/
|
|
32
|
+
export function createDecisionRunner(options) {
|
|
33
|
+
const { getConfig, client, logger, modeOf, log, specOf } = options;
|
|
34
|
+
|
|
35
|
+
/** 决策点当前模式。读不到即 off —— 宁可少判断一次,也不要在热路径上意外发请求。 */
|
|
36
|
+
function modeOfPoint(id) {
|
|
37
|
+
try {
|
|
38
|
+
return modeOf(getConfig(), id) ?? "off";
|
|
39
|
+
} catch {
|
|
40
|
+
return "off";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 问一次决策模型并给出处置。
|
|
46
|
+
*
|
|
47
|
+
* @param id - 决策点 id(同时是遥测里的 `point` 字段)。
|
|
48
|
+
* @param input - 传给 spec.build 的现场数据。
|
|
49
|
+
* @param extra - 可选:signal / tool / degradeHint。
|
|
50
|
+
* @returns
|
|
51
|
+
* `{ mode: 'off' }` 该点没开启,未发请求;
|
|
52
|
+
* `{ mode, action, ... }` 有裁决(observe 时 action 仍返回,供只记录不执行);
|
|
53
|
+
* `{ mode, degraded: reason }` 降级,调用方必须按原生行为继续。
|
|
54
|
+
*/
|
|
55
|
+
async function ask(id, input, extra = {}) {
|
|
56
|
+
const mode = modeOfPoint(id);
|
|
57
|
+
if (mode === "off") return NOOP;
|
|
58
|
+
|
|
59
|
+
const spec = specOf(id);
|
|
60
|
+
if (spec === undefined) {
|
|
61
|
+
// 注册表里有、规格却没实现,属于接线错误:按降级处理并留痕,绝不抛。
|
|
62
|
+
logger?.warn?.(`dsh-decision-engine: 决策点 ${id} 缺少规格实现,已按不介入处理`);
|
|
63
|
+
return { mode, degraded: "no-spec" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cfg = getConfig();
|
|
67
|
+
let built;
|
|
68
|
+
try {
|
|
69
|
+
built = spec.build(input);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger?.warn?.(
|
|
72
|
+
`dsh-decision-engine: ${id} 构造 state 失败(${error instanceof Error ? error.message : String(error)})`,
|
|
73
|
+
);
|
|
74
|
+
return { mode, degraded: "build-error" };
|
|
75
|
+
}
|
|
76
|
+
if (built === undefined || built.questions === undefined) return { mode, degraded: "no-questions" };
|
|
77
|
+
|
|
78
|
+
const result = await client.classify({
|
|
79
|
+
point: id,
|
|
80
|
+
tag: id,
|
|
81
|
+
mode: cfg.mode,
|
|
82
|
+
...(extra.tool === undefined ? {} : { tool: extra.tool }),
|
|
83
|
+
state: built.state,
|
|
84
|
+
questions: built.questions,
|
|
85
|
+
...(built.signatureText === undefined ? {} : { signatureText: built.signatureText }),
|
|
86
|
+
...(extra.signal === undefined ? {} : { signal: extra.signal }),
|
|
87
|
+
...(extra.softCacheable === undefined ? {} : { softCacheable: extra.softCacheable }),
|
|
88
|
+
});
|
|
89
|
+
// classify 返回 null 就是已经降级并写过遥测了,这里不能再记一遍。
|
|
90
|
+
if (result === null) return { mode, degraded: "classify" };
|
|
91
|
+
|
|
92
|
+
const parsed = spec.parse(result.answers);
|
|
93
|
+
if (parsed === undefined) {
|
|
94
|
+
await log({
|
|
95
|
+
ts: new Date().toISOString(),
|
|
96
|
+
point: id,
|
|
97
|
+
tag: id,
|
|
98
|
+
mode: cfg.mode,
|
|
99
|
+
...(extra.tool === undefined ? {} : { tool: extra.tool }),
|
|
100
|
+
action: "degraded",
|
|
101
|
+
detail: "answer-shape-mismatch",
|
|
102
|
+
latencyMs: result.latencyMs,
|
|
103
|
+
model: result.model,
|
|
104
|
+
});
|
|
105
|
+
return { mode, degraded: "answer-shape-mismatch" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const action = spec.decide(parsed, cfg, input);
|
|
109
|
+
await log({
|
|
110
|
+
ts: new Date().toISOString(),
|
|
111
|
+
point: id,
|
|
112
|
+
tag: id,
|
|
113
|
+
mode: cfg.mode,
|
|
114
|
+
...(extra.tool === undefined ? {} : { tool: extra.tool }),
|
|
115
|
+
model: result.model,
|
|
116
|
+
action: action.kind,
|
|
117
|
+
detail: action.reason ?? action.kind,
|
|
118
|
+
latencyMs: result.latencyMs,
|
|
119
|
+
cached: result.cached,
|
|
120
|
+
...(result.inputTokens === undefined ? {} : { inputTokens: result.inputTokens }),
|
|
121
|
+
scores: parsed,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return { mode, action, parsed, latencyMs: result.latencyMs, cached: result.cached };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
ask,
|
|
129
|
+
modeOf: modeOfPoint,
|
|
130
|
+
/** 该点此刻是否允许真正改变行为。observe 只记录。 */
|
|
131
|
+
enforces(id) {
|
|
132
|
+
return modeOfPoint(id) === "enforce";
|
|
133
|
+
},
|
|
134
|
+
/** 该点此刻是否参与(observe 或 enforce)。 */
|
|
135
|
+
participates(id) {
|
|
136
|
+
const mode = modeOfPoint(id);
|
|
137
|
+
return mode === "observe" || mode === "enforce";
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|