dsh-subagent-profile 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -29
- package/README.zh.md +38 -29
- package/index.mjs +188 -62
- package/lib/client.js +1240 -47
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +433 -0
- package/lib/core/delegation.mjs +106 -48
- package/lib/core/dispatch-gates.mjs +146 -0
- package/lib/core/dispatch-guard.mjs +150 -0
- package/lib/core/dispatch-schema.mjs +98 -14
- package/lib/core/dispatch-tool.mjs +208 -199
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-ledger.mjs +289 -0
- package/lib/core/evolution-summary.mjs +432 -0
- package/lib/core/http-routes.mjs +155 -40
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +256 -136
- package/lib/core/profile-provider.mjs +41 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/shims.mjs +56 -75
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +2 -3
- package/presets/orchestrator/agent.cordis.yml +243 -271
- package/presets/orchestrator/NOTICE +0 -3
|
@@ -19,6 +19,49 @@ import { TOOL_ZH, TOOL_CATEGORY } from './catalog.mjs';
|
|
|
19
19
|
|
|
20
20
|
const DEFAULT_TTL_MS = 60000;
|
|
21
21
|
|
|
22
|
+
// --- 工具分组/中文名的薄覆盖层(schema 声明优先,缺失回退)------------------------
|
|
23
|
+
//
|
|
24
|
+
// 工具目录里每一条的 group(分类)与 zh(中文名)优先取工具 schema 自带声明字段,
|
|
25
|
+
// 其次才回退数据表/前缀/预设名。宿主 tools.schemas() 当前只投影 name/description/
|
|
26
|
+
// parameters(无 category/group/zh 字段);此层在宿主未来为 schema 补上
|
|
27
|
+
// 分组/中文声明时自动优先采用,缺失时按既有回退链兜底——不新增任何硬编码条目。
|
|
28
|
+
|
|
29
|
+
// 读取 schema 声明的分组候选(可接受的字段名集合内部固定,非外部注入)。
|
|
30
|
+
function schemaDeclaredGroup(schema) {
|
|
31
|
+
if (schema === null || typeof schema !== 'object') return undefined;
|
|
32
|
+
if (typeof schema.category === 'string' && schema.category.length > 0) return schema.category;
|
|
33
|
+
if (typeof schema.group === 'string' && schema.group.length > 0) return schema.group;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 读取 schema 声明的中文名候选(宿主有 zh/label 声明时优先)。
|
|
38
|
+
function schemaDeclaredZh(schema) {
|
|
39
|
+
if (schema === null || typeof schema !== 'object') return undefined;
|
|
40
|
+
if (typeof schema.zh === 'string' && schema.zh.length > 0) return schema.zh;
|
|
41
|
+
if (typeof schema.label === 'string' && schema.label.length > 0) return schema.label;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// core 层无数据表条目时的缺省分组。
|
|
46
|
+
const DEFAULT_CORE_GROUP = '其他';
|
|
47
|
+
|
|
48
|
+
// 工具的分组解析链:schema 声明 → 数据表(core)/ 前缀(plugin)/ 预设名(custom)。
|
|
49
|
+
// `source` 是工具来源('global' 或 preset id),`layer` 为 layerOf(source)。
|
|
50
|
+
export function resolveToolCategory(schema, name, source, layer) {
|
|
51
|
+
const declared = schemaDeclaredGroup(schema);
|
|
52
|
+
if (declared !== undefined) return declared;
|
|
53
|
+
if (layer === 'core') return TOOL_CATEGORY[name] ?? DEFAULT_CORE_GROUP;
|
|
54
|
+
if (layer === 'plugin') return name.includes('_') ? name.split('_')[0] : name;
|
|
55
|
+
return source;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 工具中文名解析链:schema 声明 → 数据表 → 空串(沿用原缺省)。
|
|
59
|
+
export function resolveToolZh(schema, name) {
|
|
60
|
+
const declared = schemaDeclaredZh(schema);
|
|
61
|
+
if (declared !== undefined) return declared;
|
|
62
|
+
return TOOL_ZH[name] ?? '';
|
|
63
|
+
}
|
|
64
|
+
|
|
22
65
|
// --- 目录构建(无缓存;行为与 http-routes 原 collector 逐字一致)----------------
|
|
23
66
|
|
|
24
67
|
// llm 目录:providers + 每 provider models + 每模型 efforts。listProviders 失败
|
|
@@ -96,17 +139,12 @@ async function collectToolsDirectory(getTools, getAgentPresets, logger) {
|
|
|
96
139
|
if (OFFICIAL_PRESETS.includes(source)) return 'core';
|
|
97
140
|
return 'custom';
|
|
98
141
|
};
|
|
99
|
-
const groupOf = (name, source) =>
|
|
100
|
-
const layer = layerOf(source);
|
|
101
|
-
if (layer === 'core') return TOOL_CATEGORY[name] ?? '其他';
|
|
102
|
-
if (layer === 'plugin') return name.includes('_') ? name.split('_')[0] : name;
|
|
103
|
-
return source;
|
|
104
|
-
};
|
|
142
|
+
const groupOf = (schema, name, source) => resolveToolCategory(schema, name, source, layerOf(source));
|
|
105
143
|
const push = (schemas, source) => {
|
|
106
144
|
for (const s of (Array.isArray(schemas) ? schemas : [])) {
|
|
107
145
|
if (!s || typeof s.name !== 'string' || s.name === 'run_code' || seen.has(s.name)) continue;
|
|
108
146
|
seen.add(s.name);
|
|
109
|
-
tools.push({ name: s.name, description: typeof s.description === 'string' ? s.description : '', zh:
|
|
147
|
+
tools.push({ name: s.name, description: typeof s.description === 'string' ? s.description : '', zh: resolveToolZh(s, s.name), source, layer: layerOf(source), group: groupOf(s, s.name, source) });
|
|
110
148
|
}
|
|
111
149
|
};
|
|
112
150
|
const toolsService = typeof getTools === 'function' ? getTools() : undefined;
|
package/lib/core/catalog.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// lib/core/catalog.mjs —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// lib/core/catalog.mjs — 设置页数据表,从 index.mjs 逐字拆出
|
|
2
|
+
// (import-free,无 @deepseek-ai 依赖)。/options 工具目录构建器在
|
|
3
|
+
// lib/core/catalog-cache.mjs:它读取 ctx.tools/agentPresets(非纯函数),
|
|
4
|
+
// 不属于本模块。
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// 工具名 → 中文说明 map,toolFilter 选择器里显示在原始工具名旁。
|
|
7
|
+
// 未收录的工具回退显示原始名。
|
|
8
8
|
export const TOOL_ZH = {
|
|
9
9
|
'bash': '终端命令',
|
|
10
10
|
'pwsh': 'PowerShell 命令',
|
package/lib/core/cost-guard.mjs
CHANGED
|
@@ -2,30 +2,28 @@
|
|
|
2
2
|
// 仅引用 lib/core/pure.mjs 的 assertHardLimits;无 @deepseek-ai 依赖。
|
|
3
3
|
//
|
|
4
4
|
// 运行时推导的 cost guard,两部分:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// to verify and always passes (valid in a headless deployment).
|
|
5
|
+
// * always-on hard caps(assertHardLimits,在 lib/core/pure.mjs)——maxTokens /
|
|
6
|
+
// maxDepth 是硬性委派上限,与 `llm` 服务无关,llm 缺失时绝不能停止生效
|
|
7
|
+
// (历史教训:早期版本在 llm 缺失时直接 return,连带跳过硬上限,属缺陷——
|
|
8
|
+
// 硬上限永远前置)。
|
|
9
|
+
// * llm 能力校验——对 live provider 目录校验 provider / model /
|
|
10
|
+
// reasoningEffort。`llm` 服务缺失或其 provider 目录为空(无发现能力的
|
|
11
|
+
// adapter)时能力无法核验:按 `allowFailOpen`(迁移开关)二选一——
|
|
12
|
+
// fail-open 兼容(warn + 跳过;v1 数据迁移中)或 fail-loud 拒绝。profile
|
|
13
|
+
// 若 provider/model/reasoningEffort 都未请求,无校验项,恒通过(无头部署
|
|
14
|
+
// 下合法)。
|
|
16
15
|
//
|
|
17
16
|
// 目录读取改走共享 catalog 快照(lib/core/catalog-cache.mjs):assertCostGuard 的
|
|
18
17
|
// `catalog` 参数是缓存实例,`catalog.getSnapshot(llm)` 返回缓存过的 providers /
|
|
19
18
|
// modelsByProvider。缓存只提速目录解析、不复检安全门——provider/model/effort 的
|
|
20
19
|
// 校验逻辑与 fail-loud 文案逐字不变;reasoningEffort 走直连 resolveCallConfig
|
|
21
20
|
// (校验调用,非目录读取,不入缓存)。
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// module-scoped and cannot reach the apply closure's `allowFailOpen`/`ctx.logger`.
|
|
21
|
+
// 本函数同时供 provider 权威校验与 dispatch 工具预检使用。`allowFailOpen`/`logger`
|
|
22
|
+
// 经注入传入:函数是模块级作用域,够不到 apply 闭包里的 `allowFailOpen`/`ctx.logger`。
|
|
25
23
|
|
|
26
24
|
import { assertHardLimits } from './pure.mjs';
|
|
27
25
|
|
|
28
|
-
//
|
|
26
|
+
// 仅当 profile 请求了需核验能力面的字段时才进入 llm 校验(无头/headless 部署下
|
|
29
27
|
// persona-only / toolFilter-only 的 profile 合法,不应被 fail-loud 拒绝)。
|
|
30
28
|
function needsLlmCheck(profile) {
|
|
31
29
|
return ['provider', 'model', 'reasoningEffort'].some(
|
|
@@ -42,23 +40,22 @@ function handleUnverifiable(allowFailOpen, logger) {
|
|
|
42
40
|
throw new Error('dispatch: 模型能力不可验证:fail-loud 拒绝(可在配置中显式开启兼容模式)');
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
//
|
|
43
|
+
// provider 注册校验(目录非空时才能判定「不在目录」)。providers 来自 catalog 快照。
|
|
46
44
|
function assertProviderRegistered(dir, profile) {
|
|
47
45
|
if (typeof profile.provider !== 'string' || profile.provider.length === 0) return;
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const providers = Array.isArray(dir.providers) ? dir.providers : [];
|
|
47
|
+
if (!providers.some((provider) => provider && provider.id === profile.provider)) {
|
|
48
|
+
throw new Error(`dispatch: provider "${profile.provider}" 未注册(不在当前 provider 目录中,可在设置页改用已注册的 provider)`);
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// modelsByProvider 来自快照;快照未覆盖的 provider(undefined / 不在名册)
|
|
61
|
-
// 走直连 listModels 兜底,保持原 fail-loud 文案逐字。
|
|
52
|
+
// model 校验。resolveModelInfo 不拒绝未知模型(目录成员关系只是建议性的),
|
|
53
|
+
// 所以改为对照已发布的目录校验。空目录(无发现能力的 adapter)无法核验,
|
|
54
|
+
// 跳过——目录级空集已在 llm 门走 allowFailOpen 分支,此处仅兜底
|
|
55
|
+
// per-provider 空目录。非空目录未发布该模型则 fail-loud。不可核验的查询
|
|
56
|
+
// (provider 未知时 listModels(undefined))变成干净的 fail-loud 错误,而不是
|
|
57
|
+
// 泄漏 "undefined"。modelsByProvider 来自快照;快照未覆盖的 provider
|
|
58
|
+
// (undefined / 不在名册)走直连 listModels 兜底,保持原 fail-loud 文案逐字。
|
|
62
59
|
async function assertModelAdvertised(llm, profile, effectiveProvider, modelsByProvider) {
|
|
63
60
|
if (typeof profile.model !== 'string' || profile.model.length === 0) return;
|
|
64
61
|
const cached = modelsByProvider !== undefined ? modelsByProvider[String(effectiveProvider)] : undefined;
|
|
@@ -75,48 +72,78 @@ async function assertModelAdvertised(llm, profile, effectiveProvider, modelsByPr
|
|
|
75
72
|
}
|
|
76
73
|
}
|
|
77
74
|
if (modelsError !== undefined) {
|
|
78
|
-
throw new Error(`dispatch:
|
|
75
|
+
throw new Error(`dispatch: 无法校验模型 "${profile.model}":provider 目录查询失败(${modelsError instanceof Error ? modelsError.message : String(modelsError)}),可稍后重试或在设置页关闭模型能力校验`, { cause: modelsError });
|
|
79
76
|
}
|
|
80
77
|
const listed = models ?? [];
|
|
81
78
|
const known = listed.length > 0 && listed.some((model) => model && (model.id === profile.model || model.name === profile.model));
|
|
82
79
|
if (listed.length > 0 && !known) {
|
|
83
|
-
throw new Error(`dispatch:
|
|
80
|
+
throw new Error(`dispatch: 模型 "${profile.model}" 不在 provider "${String(effectiveProvider)}" 的目录中(可换用该 provider 已发布的模型)`);
|
|
84
81
|
}
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
//
|
|
84
|
+
// reasoningEffort 校验。
|
|
88
85
|
async function assertReasoningSupported(llm, profile, effectiveProvider, effectiveModel) {
|
|
89
86
|
if (typeof profile.reasoningEffort !== 'string' || profile.reasoningEffort.length === 0) return;
|
|
90
87
|
try {
|
|
91
88
|
await llm.resolveCallConfig({ provider: effectiveProvider, model: effectiveModel, reasoningEffort: profile.reasoningEffort });
|
|
92
89
|
} catch (error) {
|
|
93
|
-
throw new Error(`dispatch: reasoningEffort "${profile.reasoningEffort}"
|
|
90
|
+
throw new Error(`dispatch: reasoningEffort "${profile.reasoningEffort}" 不受 provider "${String(effectiveProvider)}" 的模型 "${String(effectiveModel)}" 支持(${error instanceof Error ? error.message : String(error)}),可改用一个该模型支持的 effort 值`, { cause: error });
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// 能力面逐项校验的统一接线:判定通过回调 verdict:'pass';失败先回调 verdict:'fail'
|
|
95
|
+
// (含 error.message 作 detail)再 rethrow 原错误(文案不变)。
|
|
96
|
+
async function runChecked(check, field, checkedAgainst, fn) {
|
|
97
|
+
try {
|
|
98
|
+
await fn();
|
|
99
|
+
check({ field, checkedAgainst, verdict: 'pass' });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
check({ field, checkedAgainst, verdict: 'fail', detail: error.message });
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// llm 门的逐项记录:verdict 对应实际走向,detail 注明 fail-open/fail-loud。
|
|
107
|
+
function checkLlmGate(check, allowFailOpen, detailFailLoud, detailFailOpen) {
|
|
108
|
+
check({ field: 'llm', verdict: allowFailOpen === true ? 'pass' : 'fail', detail: allowFailOpen === true ? detailFailOpen : detailFailLoud });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function assertCostGuard(parent, profile, allowFailOpen, logger, catalog, onCheck) {
|
|
112
|
+
// 可选逐项回调;onCheck 缺省时归一为 no-op,行为逐字不变(现有调用点不加参数,零漂移)。
|
|
113
|
+
const check = typeof onCheck === 'function' ? onCheck : () => {};
|
|
114
|
+
|
|
115
|
+
// 硬上限 always-on(不依赖 llm)。
|
|
99
116
|
assertHardLimits(profile.maxTokens, profile.maxDepth);
|
|
100
117
|
|
|
101
|
-
//
|
|
102
|
-
if (!needsLlmCheck(profile))
|
|
118
|
+
// 无 llm 能力面字段 → 无可核验,直接通过(headless 部署合法)。
|
|
119
|
+
if (!needsLlmCheck(profile)) {
|
|
120
|
+
check({ field: 'llm', verdict: 'pass', detail: '无需核验(未请求 provider/model/reasoningEffort)' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
103
123
|
|
|
104
124
|
const llm = parent.ctx.get('llm');
|
|
105
|
-
//
|
|
106
|
-
if (llm === undefined)
|
|
125
|
+
// llm 缺失 → 无法核验:按 allowFailOpen 决定 fail-open / fail-loud。
|
|
126
|
+
if (llm === undefined) {
|
|
127
|
+
checkLlmGate(check, allowFailOpen, 'llm 缺失:fail-loud 拒绝', 'llm 缺失:fail-open 跳过核验');
|
|
128
|
+
return handleUnverifiable(allowFailOpen, logger);
|
|
129
|
+
}
|
|
107
130
|
|
|
108
131
|
// llm 存在:目录读取走共享 catalog 快照(缓存只提速目录解析,校验逻辑不变)。
|
|
109
132
|
const snapshot = await catalog.getSnapshot(llm);
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
// M10:快照形状守卫——畸形快照(llm 缺失 / providers 非数组)不得被静默放行,
|
|
134
|
+
// 归一为「目录为空」走同一 fail-loud / fail-open 门。
|
|
135
|
+
const dir = (snapshot !== null && typeof snapshot === 'object' && snapshot.llm !== null && typeof snapshot.llm === 'object') ? snapshot.llm : {};
|
|
136
|
+
const providers = Array.isArray(dir.providers) ? dir.providers : [];
|
|
137
|
+
if (dir.providersError !== undefined || providers.length === 0) {
|
|
138
|
+
checkLlmGate(check, allowFailOpen, 'provider 目录为空:fail-loud 拒绝', 'provider 目录为空:fail-open 跳过核验');
|
|
112
139
|
return handleUnverifiable(allowFailOpen, logger);
|
|
113
140
|
}
|
|
114
141
|
|
|
115
|
-
//
|
|
116
|
-
assertProviderRegistered(dir, profile);
|
|
142
|
+
// provider 注册校验。
|
|
143
|
+
await runChecked(check, 'provider', 'catalogProviders', () => assertProviderRegistered(dir, profile));
|
|
117
144
|
const effectiveProvider = profile.provider !== undefined ? profile.provider : parent.options.provider;
|
|
118
145
|
const effectiveModel = profile.model !== undefined ? profile.model : parent.options.model;
|
|
119
|
-
//
|
|
120
|
-
await assertModelAdvertised(llm, profile, effectiveProvider, dir.modelsByProvider);
|
|
121
|
-
await assertReasoningSupported(llm, profile, effectiveProvider, effectiveModel);
|
|
146
|
+
// model 校验 + reasoningEffort 校验。
|
|
147
|
+
await runChecked(check, 'model', 'providerModelList', () => assertModelAdvertised(llm, profile, effectiveProvider, dir.modelsByProvider));
|
|
148
|
+
await runChecked(check, 'reasoningEffort', 'resolveCallConfig', () => assertReasoningSupported(llm, profile, effectiveProvider, effectiveModel));
|
|
122
149
|
}
|