dsh-subagent-profile 0.3.4 → 0.5.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/README.md +13 -8
- package/README.zh.md +13 -8
- package/index.mjs +99 -95
- package/lib/client.js +1288 -189
- package/lib/core/decision-trace.mjs +20 -1
- package/lib/core/delegation.mjs +3 -2
- package/lib/core/dispatch-gates.mjs +122 -9
- package/lib/core/dispatch-schema.mjs +45 -7
- package/lib/core/dispatch-tool.mjs +24 -25
- package/lib/core/evolution-advice.mjs +154 -34
- package/lib/core/evolution-assets.mjs +167 -0
- package/lib/core/evolution-canary.mjs +427 -0
- package/lib/core/evolution-draft.mjs +333 -0
- package/lib/core/evolution-engine.mjs +413 -0
- package/lib/core/evolution-generate.mjs +180 -0
- package/lib/core/evolution-lag.mjs +78 -0
- package/lib/core/evolution-ledger.mjs +135 -27
- package/lib/core/evolution-persistence.mjs +213 -0
- package/lib/core/evolution-renewal.mjs +64 -0
- package/lib/core/evolution-routes.mjs +234 -0
- package/lib/core/http-helpers.mjs +35 -0
- package/lib/core/http-routes.mjs +35 -39
- package/lib/core/model-policy.mjs +77 -0
- package/lib/core/presets-sync.mjs +24 -2
- package/lib/core/profile-directory.mjs +17 -0
- package/lib/core/profile-provider.mjs +9 -3
- package/lib/core/profiles-query.mjs +178 -0
- package/lib/core/profiles-store.mjs +56 -8
- package/lib/core/pure.mjs +1 -1
- package/lib/core/session-read.mjs +17 -0
- package/lib/core/shims.mjs +2 -1
- package/package.json +2 -2
- package/presets/orchestrator-v2/NOTICE +7 -0
- package/presets/{orchestrator → orchestrator-v2}/agent.cordis.yml +78 -7
- package/presets/orchestrator-v2/custom-bash.mjs +213 -0
- package/presets/orchestrator-v2/preset.yml +2 -0
- package/presets/orchestrator-v2/tool-bootstrap.mjs +620 -0
- package/presets/orchestrator/preset.yml +0 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// lib/core/profiles-query.mjs — profiles_query 工具(派发档案按需下钻):入参全可选
|
|
2
|
+
// (id / tier / provider+model 成对 / 无参全量),未知键由宿主 DSL 默认拒绝(键集闭合
|
|
3
|
+
// 断言在 test/profiles-query.test.mjs)。数据源分布:profileDirectoryRows(档案)/
|
|
4
|
+
// catalog-cache 快照(官方模型 + efforts,TTL 60s)/ summaries.json(统计,
|
|
5
|
+
// mtime+指纹,fail-soft)。官方目录没有的 model 只标「未在册」(advisory,不判失效);
|
|
6
|
+
// 「未授权」联动官方模型选择投影(model-policy):本会话投影配置了 allowedModels 且档案绑定路由不在
|
|
7
|
+
// 其中时追加标注。本工具不复制任何策略逻辑——授权性以官方为准(工具描述首行已指向
|
|
8
|
+
// list_subagent_models)。
|
|
9
|
+
|
|
10
|
+
import { defineTool } from './shims.mjs';
|
|
11
|
+
import { profileDirectoryRows, profileStatsFromSummaries } from './profile-directory.mjs';
|
|
12
|
+
import { readModelSelectionPolicy } from './model-policy.mjs';
|
|
13
|
+
import { readSummaries } from './evolution-advice.mjs';
|
|
14
|
+
|
|
15
|
+
// 入参 schema:全可选;provider+model 成对约束在 execute 内校验(schema 层无法表达
|
|
16
|
+
// 成对)。键集闭合 = 白名单 4 键(与 percall-spec 同口径:多出的键没有接受路径)。
|
|
17
|
+
export const PROFILES_QUERY_PARAMETERS = {
|
|
18
|
+
id: { type: 'string', description: '按 id 查单个派发档案明细(其余入参被忽略)' },
|
|
19
|
+
tier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '按成本/深度档位过滤档案' },
|
|
20
|
+
provider: { type: 'string', description: '与 model 成对使用:先查该模型的官方 reasoning efforts,再列出绑定该模型的档案' },
|
|
21
|
+
model: { type: 'string', description: '与 provider 成对使用:官方模型目录中的模型 id' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// 输出 schema:模型可见面只有 text 一行(render 把 text 包成内容块)。
|
|
25
|
+
export const PROFILES_QUERY_OUTPUT_SCHEMA = {
|
|
26
|
+
type: 'object',
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
text: { type: 'string', required: true, description: '模型可见的查询结果文本' },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 列表行描述截断长度(单档案明细不截断)。
|
|
34
|
+
const LIST_DESC_CHARS = 80;
|
|
35
|
+
|
|
36
|
+
// 档案绑定的 provider/model 路由(两者齐备才绑定);未绑定返回 null。
|
|
37
|
+
export function boundRouteOf(row) {
|
|
38
|
+
const provider = row !== null && typeof row === 'object' && typeof row.provider === 'string' && row.provider !== '' ? row.provider : undefined;
|
|
39
|
+
const model = row !== null && typeof row === 'object' && typeof row.model === 'string' && row.model !== '' ? row.model : undefined;
|
|
40
|
+
return provider !== undefined && model !== undefined ? { provider, model } : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 官方状态标注:在册(catalog models 含 provider+model)/ 未在册 / 未绑定。未在册只
|
|
44
|
+
// 标不判失效(advisory)。
|
|
45
|
+
export function catalogStatusOf(row, models) {
|
|
46
|
+
const route = boundRouteOf(row);
|
|
47
|
+
if (route === null) return '未绑定';
|
|
48
|
+
const listed = Array.isArray(models) && models.some((m) => m !== null && typeof m === 'object' && m.provider === route.provider && m.id === route.model);
|
|
49
|
+
return listed ? '在册' : '未在册';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 未授权标注:投影配置且绑定路由不在 allowedModels → '未授权';投影未配置或未绑定
|
|
53
|
+
// → undefined(不加注)。授权性判定只读官方投影,不复制策略逻辑。
|
|
54
|
+
export function unauthorizedMarkOf(row, policy) {
|
|
55
|
+
const route = boundRouteOf(row);
|
|
56
|
+
if (route === null || policy === undefined || policy === null || policy.configured !== true) return undefined;
|
|
57
|
+
const allowed = Array.isArray(policy.allowedModels) && policy.allowedModels.some((r) => r !== null && typeof r === 'object' && r.provider === route.provider && r.model === route.model);
|
|
58
|
+
return allowed ? undefined : '未授权';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// efforts 条件式文案:绑定 → 该模型官方 reasoning efforts(目录无数据 → 占位);
|
|
62
|
+
// 未绑定 → 继承父模型、档位以父为准。
|
|
63
|
+
export function effortsTextOf(row, effortsByModel) {
|
|
64
|
+
const route = boundRouteOf(row);
|
|
65
|
+
if (route === null) return '继承父模型,档位以父为准';
|
|
66
|
+
const efforts = effortsByModel !== null && typeof effortsByModel === 'object' ? effortsByModel[route.model] : undefined;
|
|
67
|
+
if (!Array.isArray(efforts) || efforts.length === 0) return '(目录无该模型明细)';
|
|
68
|
+
return efforts.map((effort) => (effort !== null && typeof effort === 'object' && typeof effort.id === 'string' ? effort.id : String(effort))).join('/');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 单档案明细(两行:主行 + efforts 行)。stats 为 profileStatsFromSummaries 的
|
|
72
|
+
// Map;truncate=true 时描述截断 80 字符 + …(列表口径,单档案明细不截断)。
|
|
73
|
+
export function profileDetailLines(row, { stats, models, effortsByModel, policy, truncate = false } = {}) {
|
|
74
|
+
const desc = row !== null && typeof row === 'object' && typeof row.description === 'string' && row.description !== '' ? row.description : '(无描述)';
|
|
75
|
+
const shown = truncate && desc.length > LIST_DESC_CHARS ? `${desc.slice(0, LIST_DESC_CHARS)}…` : desc;
|
|
76
|
+
const route = boundRouteOf(row);
|
|
77
|
+
const binding = route === null ? '继承父' : `${route.provider}/${route.model}`;
|
|
78
|
+
const tier = row !== null && typeof row === 'object' && typeof row.tokenTier === 'string' && row.tokenTier !== '' ? row.tokenTier : '未标注';
|
|
79
|
+
const stat = stats instanceof Map ? stats.get(row?.id) : (stats !== null && typeof stats === 'object' ? stats[row?.id] : undefined);
|
|
80
|
+
const cost = stat !== null && typeof stat === 'object' && typeof stat.avgCost === 'number' ? `平均 ¥${stat.avgCost}` : undefined;
|
|
81
|
+
const rate = stat !== null && typeof stat === 'object' && typeof stat.successRate === 'number'
|
|
82
|
+
? `成功率 ${stat.successRate}${typeof stat.n === 'number' ? ` (N=${stat.n})` : ''}`
|
|
83
|
+
: undefined;
|
|
84
|
+
const status = catalogStatusOf(row, models);
|
|
85
|
+
const unauthorized = unauthorizedMarkOf(row, policy);
|
|
86
|
+
const meta = [binding, `tier ${tier}`, cost, rate, `官方状态:${status}${unauthorized !== undefined ? `·${unauthorized}` : ''}`].filter((part) => part !== undefined).join(' · ');
|
|
87
|
+
return [`${row?.id}: ${shown} — ${meta}`, `· efforts: ${effortsTextOf(row, effortsByModel)}`];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 单次查询组装(纯函数):id 单档案 > tier 过滤 > provider+model 成对 > 无参全量。
|
|
91
|
+
// provider/model 只传其一 → fail-loud(成对约束)。
|
|
92
|
+
export function buildProfilesQueryText({ args, rows, stats, models, effortsByModel, policy }) {
|
|
93
|
+
const hasProvider = typeof args?.provider === 'string' && args.provider !== '';
|
|
94
|
+
const hasModel = typeof args?.model === 'string' && args.model !== '';
|
|
95
|
+
if (hasProvider !== hasModel) throw new Error('profiles_query: provider 与 model 须成对提供');
|
|
96
|
+
const details = (row, truncate = false) => profileDetailLines(row, { stats, models, effortsByModel, policy, truncate });
|
|
97
|
+
if (typeof args?.id === 'string' && args.id !== '') {
|
|
98
|
+
const row = rows.find((r) => r !== null && typeof r === 'object' && r.id === args.id);
|
|
99
|
+
if (row === undefined) return { text: `未找到 profile "${args.id}"。可用 id:${rows.map((r) => r?.id).join('、') || '(暂无)'}` };
|
|
100
|
+
return { text: details(row).join('\n') };
|
|
101
|
+
}
|
|
102
|
+
if (typeof args?.tier === 'string' && args.tier !== '') {
|
|
103
|
+
const tierRows = rows.filter((r) => r !== null && typeof r === 'object' && r.tokenTier === args.tier);
|
|
104
|
+
if (tierRows.length === 0) return { text: `无 tier=${args.tier} 的方案` };
|
|
105
|
+
const lines = tierRows.flatMap((row) => details(row, true));
|
|
106
|
+
return { text: `tier=${args.tier} 的方案(${tierRows.length}):\n${lines.join('\n')}` };
|
|
107
|
+
}
|
|
108
|
+
if (hasModel) {
|
|
109
|
+
const listed = Array.isArray(models) && models.some((m) => m !== null && typeof m === 'object' && m.provider === args.provider && m.id === args.model);
|
|
110
|
+
const rawEfforts = effortsByModel !== null && typeof effortsByModel === 'object' ? effortsByModel[args.model] : undefined;
|
|
111
|
+
const modelEfforts = Array.isArray(rawEfforts) && rawEfforts.length > 0
|
|
112
|
+
? rawEfforts.map((effort) => (effort !== null && typeof effort === 'object' && typeof effort.id === 'string' ? effort.id : String(effort))).join('/')
|
|
113
|
+
: undefined;
|
|
114
|
+
const header = `模型 ${args.provider}/${args.model} 官方 reasoning efforts:${modelEfforts ?? '(目录无该模型明细)'}`;
|
|
115
|
+
const bound = rows.filter((r) => {
|
|
116
|
+
const route = boundRouteOf(r);
|
|
117
|
+
return route !== null && route.provider === args.provider && route.model === args.model;
|
|
118
|
+
});
|
|
119
|
+
if (bound.length === 0) return { text: `${header}\n无绑定该模型的方案${listed ? '' : '(该模型不在官方目录,未在册)'}` };
|
|
120
|
+
const lines = bound.flatMap((row) => details(row, true));
|
|
121
|
+
return { text: `${header}\n${lines.join('\n')}` };
|
|
122
|
+
}
|
|
123
|
+
if (rows.length === 0) return { text: '暂无可用派发档案(可在设置页「子 Agent 方案」创建)' };
|
|
124
|
+
const lines = rows.flatMap((row) => details(row, true));
|
|
125
|
+
return { text: `派发档案目录(${rows.length}):\n${lines.join('\n')}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 查询主体:catalog 快照(含官方 efforts)→ summaries 统计(fail-soft)→ 官方
|
|
129
|
+
// 模型选择投影(未授权标注)→ 纯函数组装文本。
|
|
130
|
+
async function runProfilesQuery(args, exec, deps) {
|
|
131
|
+
const agent = exec !== null && typeof exec === 'object' ? exec.agent : undefined;
|
|
132
|
+
const snapshot = await deps.catalog.getSnapshot(typeof agent?.ctx?.get === 'function' ? agent.ctx.get('llm') : undefined);
|
|
133
|
+
const models = snapshot !== null && typeof snapshot === 'object' && Array.isArray(snapshot.models) ? snapshot.models : [];
|
|
134
|
+
const effortsByModel = snapshot !== null && typeof snapshot === 'object' && snapshot.efforts !== null && typeof snapshot.efforts === 'object' ? snapshot.efforts : {};
|
|
135
|
+
let summaries = null;
|
|
136
|
+
try { summaries = readSummaries(deps.summariesFile, deps.logger, { onLoss: deps.onLoss }); } catch { /* 统计只增强,查询不因统计失败而失败 */ }
|
|
137
|
+
const stats = profileStatsFromSummaries(deps.store, summaries);
|
|
138
|
+
const policy = readModelSelectionPolicy(agent?.ctx, agent?.session);
|
|
139
|
+
return buildProfilesQueryText({ args, rows: profileDirectoryRows(deps.store), stats, models, effortsByModel, policy });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 工厂返回 { syncTool, dispose }:与 dispatch 同款 syncTool 机制(随 enabled 开关
|
|
143
|
+
// 注册/注销,工具列表自动可见,不依赖静态段)。
|
|
144
|
+
export function createProfilesQueryTool({ register, store, catalog, summariesFile, onLoss, logger, getEnabled }) {
|
|
145
|
+
const deps = { store, catalog, summariesFile, onLoss, logger, getEnabled };
|
|
146
|
+
const tool = defineTool({
|
|
147
|
+
name: 'profiles_query',
|
|
148
|
+
description: '按方案查询派发档案与明细(值不值:描述/档位/统计);授权路由(能不能用)请用官方 list_subagent_models。入参全可选:id 查单个档案;tier 按档位过滤;provider+model 成对查询(先查官方 efforts 再列绑定方案);无参返回全量目录。',
|
|
149
|
+
parameters: PROFILES_QUERY_PARAMETERS,
|
|
150
|
+
output: {
|
|
151
|
+
schema: PROFILES_QUERY_OUTPUT_SCHEMA,
|
|
152
|
+
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
153
|
+
},
|
|
154
|
+
isConcurrencySafe: () => true,
|
|
155
|
+
execute: (args, exec) => runProfilesQuery(args, exec, deps),
|
|
156
|
+
});
|
|
157
|
+
let disposeTool;
|
|
158
|
+
function syncTool() {
|
|
159
|
+
if (getEnabled() && disposeTool === undefined) {
|
|
160
|
+
disposeTool = register(tool);
|
|
161
|
+
} else if (!getEnabled() && disposeTool !== undefined) {
|
|
162
|
+
const dispose = disposeTool;
|
|
163
|
+
disposeTool = undefined;
|
|
164
|
+
if (typeof dispose === 'function') dispose();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
syncTool();
|
|
168
|
+
return {
|
|
169
|
+
syncTool,
|
|
170
|
+
dispose() {
|
|
171
|
+
if (disposeTool !== undefined) {
|
|
172
|
+
const dispose = disposeTool;
|
|
173
|
+
disposeTool = undefined;
|
|
174
|
+
if (typeof dispose === 'function') dispose();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -34,15 +34,30 @@ export function dshHome() {
|
|
|
34
34
|
// 1. Profile 注册表(per-instance 状态;bundle 行是进程级,故本 Map 是单例
|
|
35
35
|
// store,与动态插件完全一致)。内置种子带 `builtin: true`,reset/remove 可
|
|
36
36
|
// 识别它们,改过的内置方案与纯用户 profile 可区分。
|
|
37
|
-
// description 是语义化的:一行定位 +
|
|
37
|
+
// description 是语义化的:一行定位 + 何时用,帮助模型选择;description
|
|
38
|
+
// 须含适用场景词(落库契约,见 assertProfileDescription)。reasoningEffort
|
|
38
39
|
// 档位经 llm-deepseek adapter 核验(off/high/max;见 resolveModel 对
|
|
39
40
|
// connection.defaults.thinking 的门控——本部署未设 thinking,
|
|
40
41
|
// 故对 deepseek-v4-flash 全档位宣传)。
|
|
41
42
|
export const BUILTIN_SEEDS = [
|
|
42
|
-
{ id: 'swap-standard', name: '标准编码', description: '
|
|
43
|
-
{ id: 'researcher', name: '调研检索', description: '
|
|
43
|
+
{ id: 'swap-standard', name: '标准编码', description: '写代码与重构场景:切换到 standard 预设的完整编码工具集,适合多文件改动、修复与测试;当父会话不是 standard、但子任务需要完整编码能力时选用。', preset: 'standard', tokenTier: 'balanced', builtin: true },
|
|
44
|
+
{ id: 'researcher', name: '调研检索', description: '调研与汇总场景:关闭深度推理省 token,继承父工具;适合查资料、检索、背景调研与只读分析,不适合改代码。', reasoningEffort: 'off', persona: 'You are a research subagent: search, read, and summarize only. Do not modify code or files.', tokenTier: 'cheap', builtin: true }
|
|
44
45
|
];
|
|
45
46
|
|
|
47
|
+
// description 落库契约词表:description 必须包含至少一个适用场景词(常量,可微调)。
|
|
48
|
+
export const DESCRIPTION_SCENARIO_WORDS = ['查', '调研', '检索', '汇总', '代码', '重构', '修复', '审查', '只读', '审计', '生成', '测试', '翻译', '编写', '改', '脚本', '分析'];
|
|
49
|
+
|
|
50
|
+
// description 落库校验(纯函数,zero-dep):非空(trim)+ 长度 ≥10 字符 + 含适用
|
|
51
|
+
// 场景词,任一不满足 fail-loud(文案定稿)。返回 trim 后的文本供调用方落库。
|
|
52
|
+
export function assertProfileDescription(description) {
|
|
53
|
+
const text = typeof description === 'string' ? description.trim() : '';
|
|
54
|
+
const valid = text.length >= 10 && DESCRIPTION_SCENARIO_WORDS.some((word) => text.includes(word));
|
|
55
|
+
if (!valid) {
|
|
56
|
+
throw new Error('description 需写明适用场景(如:查资料/周报汇总、多文件重构、只读审计),当前描述不含可用场景词');
|
|
57
|
+
}
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
// --- 模块级 store 函数(从工厂拆出;per-apply 状态经 `state` 注入)---------------
|
|
47
62
|
|
|
48
63
|
function resolveProfile(profiles, id) {
|
|
@@ -167,11 +182,19 @@ function persistProfiles(state, logger) {
|
|
|
167
182
|
|
|
168
183
|
// 插件开关态(state.json):enabled(默认开,禁用即关派发工具)+ evolutionAdvice
|
|
169
184
|
// (派发优化建议注入,默认关,需显式开)+ escapeEnabled(逃生舱放行,默认关——信任
|
|
170
|
-
// 底板,仅设置页显式 opt-in
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
// 底板,仅设置页显式 opt-in 可放开)+ applyEnabled(进化候选生成与应用写路径,默认
|
|
186
|
+
// 关——应用能力只由人显式开启)+ candidateMode/candidateTtlH(候选方案更新策略与
|
|
187
|
+
// 有效期,用户可配置)。开关共用同一 state.json,读写整份对象以互不覆盖;
|
|
188
|
+
// 缺失文件回退默认值(全新部署),损坏文件 fail-closed(全关,绝不静默回退
|
|
189
|
+
// enabled:true —— 安全禁用会被撤销),内存态仍驱动本进程。
|
|
190
|
+
const STATE_DEFAULTS = { enabled: true, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24 };
|
|
191
|
+
const STATE_FAIL_CLOSED = { enabled: false, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24 };
|
|
192
|
+
|
|
193
|
+
// 候选有效期小时数归一:0(不自动过期)或 1~8760(一年内)的整数原样保留,
|
|
194
|
+
// 其余值回退默认 24 小时(读取与写入共用同一口径,防坏值落盘)。
|
|
195
|
+
function validCandidateTtlH(value) {
|
|
196
|
+
return typeof value === 'number' && Number.isInteger(value) && (value === 0 || (value >= 1 && value <= 8760)) ? value : 24;
|
|
197
|
+
}
|
|
175
198
|
|
|
176
199
|
function loadState(state) {
|
|
177
200
|
if (!existsSync(state.stateFile)) return { ...STATE_DEFAULTS };
|
|
@@ -182,6 +205,9 @@ function loadState(state) {
|
|
|
182
205
|
enabled: parsed.enabled !== false,
|
|
183
206
|
evolutionAdvice: parsed.evolutionAdvice === true,
|
|
184
207
|
escapeEnabled: parsed.escapeEnabled === true,
|
|
208
|
+
applyEnabled: parsed.applyEnabled === true,
|
|
209
|
+
candidateMode: parsed.candidateMode === 'manual' ? 'manual' : 'auto',
|
|
210
|
+
candidateTtlH: validCandidateTtlH(parsed.candidateTtlH),
|
|
185
211
|
};
|
|
186
212
|
} catch (error) {
|
|
187
213
|
// fail-closed(S2):损坏的 state.json 若回退 enabled:true,会把「安全禁用」静默撤销。
|
|
@@ -212,6 +238,20 @@ function persistState(state, patch, onGovernanceFailure) {
|
|
|
212
238
|
}
|
|
213
239
|
}
|
|
214
240
|
|
|
241
|
+
// 落库保存路径:description 值发生变化(新建或编辑改写)必须过契约校验;
|
|
242
|
+
// 与原值相同(存量未动)或未携带 description 时跳过——存量宽容不迁移。
|
|
243
|
+
// 校验失败 fail-loud,不落 Map。
|
|
244
|
+
function saveProfile(state, profile) {
|
|
245
|
+
if (profile !== null && typeof profile === 'object' && profile.description !== undefined) {
|
|
246
|
+
const existing = state.profiles.get(profile.id);
|
|
247
|
+
if (existing === undefined || existing.description !== profile.description) {
|
|
248
|
+
assertProfileDescription(profile.description);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
state.profiles.set(profile.id, profile);
|
|
252
|
+
return profile;
|
|
253
|
+
}
|
|
254
|
+
|
|
215
255
|
// 构造一个 store 实例。`dshHome` 是已解析的主目录字符串(来自导出的
|
|
216
256
|
// dshHome());`logger` 是 apply 时的 ctx.logger。`onGovernanceFailure`(可选)
|
|
217
257
|
// 是任何 profile/state 持久化写失败时触发的审计钩子——一次治理审计丢失事件。
|
|
@@ -233,6 +273,7 @@ export function createProfileStore({ dshHome, logger, onGovernanceFailure = () =
|
|
|
233
273
|
profiles,
|
|
234
274
|
deletedBuiltins,
|
|
235
275
|
resolveProfile: (id) => resolveProfile(profiles, id),
|
|
276
|
+
saveProfile: (profile) => saveProfile(state, profile),
|
|
236
277
|
loadProfiles: () => loadProfiles(state, logger),
|
|
237
278
|
persistProfiles: () => {
|
|
238
279
|
const result = persistProfiles(state, logger);
|
|
@@ -246,6 +287,13 @@ export function createProfileStore({ dshHome, logger, onGovernanceFailure = () =
|
|
|
246
287
|
persistEvolutionAdvice: (value) => persistState(state, { evolutionAdvice: value === true }, onGovernanceFailure),
|
|
247
288
|
loadEscapeEnabled: () => loadState(state).escapeEnabled,
|
|
248
289
|
persistEscapeEnabled: (value) => persistState(state, { escapeEnabled: value === true }, onGovernanceFailure),
|
|
290
|
+
loadApplyEnabled: () => loadState(state).applyEnabled,
|
|
291
|
+
persistApplyEnabled: (value) => persistState(state, { applyEnabled: value === true }, onGovernanceFailure),
|
|
292
|
+
// 候选更新策略与有效期(state.json 同源读写,坏值归一默认)。
|
|
293
|
+
loadCandidateMode: () => loadState(state).candidateMode,
|
|
294
|
+
persistCandidateMode: (value) => persistState(state, { candidateMode: value === 'manual' ? 'manual' : 'auto' }, onGovernanceFailure),
|
|
295
|
+
loadCandidateTtlH: () => loadState(state).candidateTtlH,
|
|
296
|
+
persistCandidateTtlH: (value) => persistState(state, { candidateTtlH: validCandidateTtlH(value) }, onGovernanceFailure),
|
|
249
297
|
// getAllowFailOpen:cost guard 必须在派发时读取迁移标志;
|
|
250
298
|
// loadProfiles/persistProfiles 持有该写入。
|
|
251
299
|
getAllowFailOpen: () => state.allowFailOpen,
|
package/lib/core/pure.mjs
CHANGED
|
@@ -369,7 +369,7 @@ export function sanitizeProfile(profile, options = {}) {
|
|
|
369
369
|
case 'name':
|
|
370
370
|
case 'description':
|
|
371
371
|
// id/preset 与 name/description 同走文本采样:压平换行,阻断经 \n 注入
|
|
372
|
-
// orchestrator systemPrompt 的提示注入面(H1)。id/preset 此前走 default
|
|
372
|
+
// orchestrator-v2 systemPrompt 的提示注入面(H1)。id/preset 此前走 default
|
|
373
373
|
// 分支原样拷贝,可注入换行破坏 dispatch:profiles 行。
|
|
374
374
|
sanitizeTextField(key, value, clean, warnings);
|
|
375
375
|
break;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// lib/core/session-read.mjs — 会话事件统一读取口(双轨兼容)。
|
|
2
|
+
//
|
|
3
|
+
// 读取会话事件的两个不兼容环境:
|
|
4
|
+
// * 稳定线:session.events 是数组(本插件当前发布环境的形状);
|
|
5
|
+
// * 新线:session.events 属性被移除,改为按需读取 session.snapshotEvents()。
|
|
6
|
+
// 直接改成新 API 会让稳定线崩溃;直接保留旧读法会在新线崩溃。因此本 helper
|
|
7
|
+
// 逐次 feature-detect:events 数组优先,snapshotEvents() 其次,两轨都不可用
|
|
8
|
+
// 时返回空数组(fall-soft,绝不抛出)。
|
|
9
|
+
//
|
|
10
|
+
// 返回语义:稳定线分支返回 session.events 原数组引用(不拷贝),保持既有
|
|
11
|
+
// identity 语义与零开销;新线分支返回 snapshotEvents() 的返回值(默认全量快照)。
|
|
12
|
+
export function readSessionEvents(session) {
|
|
13
|
+
if (session === null || session === undefined) return [];
|
|
14
|
+
if (Array.isArray(session.events)) return session.events;
|
|
15
|
+
if (typeof session.snapshotEvents === 'function') return session.snapshotEvents();
|
|
16
|
+
return [];
|
|
17
|
+
}
|
package/lib/core/shims.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { createRequire } from 'node:module';
|
|
|
23
23
|
// 导出缺失会在 apply 能运行之前就以明确错误中止模块加载——这正是本类存在的隔离。
|
|
24
24
|
import { assertSubagentMaxDepth, resolveChildDepth } from '@deepseek-ai/dsh-subagent';
|
|
25
25
|
import { toStopReason } from './pure.mjs';
|
|
26
|
+
import { readSessionEvents } from './session-read.mjs';
|
|
26
27
|
|
|
27
28
|
// warn:模块顶层没有 ctx / logger,降级用 console.warn。
|
|
28
29
|
// 前缀让来源在共享宿主日志中可辨认。
|
|
@@ -162,7 +163,7 @@ const [foldConsumedWork, finalAssistantOutput, createUserMessage, appendDelegate
|
|
|
162
163
|
// finalAssistantOutput(最后一个非空 assistant/message,否则拼接的 text-delta
|
|
163
164
|
// 分片,再否则 undefined -> [])。
|
|
164
165
|
function readResult(child, boundary, cancelled) {
|
|
165
|
-
const own = child.session.
|
|
166
|
+
const own = readSessionEvents(child.session).slice(boundary);
|
|
166
167
|
const end = foldConsumedWork(own).end;
|
|
167
168
|
const recorded = toStopReason(end?.data.reason);
|
|
168
169
|
const stopReason = cancelled && recorded !== 'completed' ? 'aborted' : recorded;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subagent-profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "DeepSeek Harness 子 Agent 派发插件:按任务为子代理选模型、推理强度与工具范围,常用组合存成命名方案随时复用;内置安全检查、成本估算与节省分析、完整决策台账。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -79,4 +79,4 @@
|
|
|
79
79
|
"profiles"
|
|
80
80
|
],
|
|
81
81
|
"license": "MIT"
|
|
82
|
-
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agent.cordis.yml 改编自 DeepSeek Harness 内置 Minimal 与 Standard preset(MIT)。
|
|
2
|
+
tool-bootstrap.mjs 来自 https://github.com/xiaobright/dsh-anchored-standard(MIT),
|
|
3
|
+
并包含 dsh-liangshen 的两阶段隔离扩展。
|
|
4
|
+
custom-bash.mjs 移植自同一上游(xiaobright/dsh-anchored-standard,MIT)的
|
|
5
|
+
combo-anchored/custom-bash.mjs,用于 win32 上以跨平台子进程通道提供同名
|
|
6
|
+
`bash` 工具(Git Bash)。
|
|
7
|
+
原始 DeepSeek 版权和 MIT 许可声明见开源仓库。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `orchestrator`
|
|
1
|
+
# `orchestrator-v2` 编排者预设 V2:主 Agent 协调模式 + 梁神 V2 两阶段引导。
|
|
2
2
|
#
|
|
3
3
|
# 与官方 `standard` 预设逐行一致,仅 persona 段改写为「主协调 Agent」身份:
|
|
4
4
|
# 拆解任务、经宿主注册的 `dispatch` 工具(按场景选方案/预设/模型/推理强度)
|
|
@@ -6,8 +6,27 @@
|
|
|
6
6
|
# 再集成结果。`dispatch` 工具本身由 dsh-subagent-profile 宿主行注册,对所有
|
|
7
7
|
# 预设可见;本预设只补下面的委派工具。
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# V2 改造(模仿梁神模式 V2,适配当前 DSH >= 0.1.2):
|
|
10
|
+
# - 引入两阶段引导 `./tool-bootstrap.mjs`:第一个持久工具/调用事件之前,
|
|
11
|
+
# 模型只看到 Minimal 双工具(`bash` + `str_replace_editor`)与本预设的
|
|
12
|
+
# persona 段,无运行时上下文、只放行白名单消息来源;锚定(或四步兜底、
|
|
13
|
+
# 或首轮结束)后升入 PTC 模式(单 `run_code` 工具)并恢复全量目录,
|
|
14
|
+
# 工作区指令与技能目录延迟一步注入。压缩后回落到受控阶段
|
|
15
|
+
# (双工具 + compactionTools 核心工作集,含 dispatch/subagent 以维持委派),
|
|
16
|
+
# 等待新的升态信号。
|
|
17
|
+
# - tool-bootstrap.mjs 已适配 0.1.2:经 `session.snapshotEvents()` 读事件日志
|
|
18
|
+
# (旧 `session.events` 属性已移除,直接读会每轮崩 "Cannot read properties
|
|
19
|
+
# of undefined (reading 'length')");persona 已含 `{{cwd}}` 模板时不再追加
|
|
20
|
+
# 字面工作目录行。
|
|
21
|
+
# - 补 `str_replace_editor`(phase-1 编辑器);win32 下补 `custom-bash`
|
|
22
|
+
# (Git Bash `bash` 工具)以保住 phase-1 的 `bash` 锚点——官方 tool-bash
|
|
23
|
+
# 在 win32 禁用、tool-pwsh 不叫 bash,没有它 phase-1 会退化成全量目录。
|
|
24
|
+
# posix 下官方 tool-bash 本身就叫 bash,无需补行。
|
|
25
|
+
# - 补 `command-goal`(/goal 人机命令,对齐当前官方 standard);spawn 行补
|
|
26
|
+
# `modelSelectionSettings: true`。
|
|
27
|
+
#
|
|
28
|
+
# 改编自官方 `standard` 预设与梁神模式 V2(liangshen-v2);原始 DeepSeek
|
|
29
|
+
# 版权与 MIT 许可声明见开源仓库与随附 NOTICE。
|
|
11
30
|
#
|
|
12
31
|
# 本文件是 agent 平面组成:由 roster 挂载一次到 standing scope,引用它的会话
|
|
13
32
|
# 经 scope 父系加入,本文件注册的工具与 prompt 段覆盖每个加入的 agent;会话
|
|
@@ -37,6 +56,35 @@
|
|
|
37
56
|
|
|
38
57
|
边界:子 Agent 能力 ⊆ 你,派发不豁免父会话的审批要求;子 Agent 失败或超出权限时自己接手重做,或用 ask_user_question 向用户澄清。
|
|
39
58
|
|
|
59
|
+
# 两阶段输入引导(同梁神 V2 的 tool-bootstrap):在第一个持久 tool/call 事件
|
|
60
|
+
# 之前,模型只看到 `bash` + `str_replace_editor` 双工具与本预设 persona 段、
|
|
61
|
+
# 无运行时上下文、只放行白名单消息来源(直接用户消息 + goal 自动轮次)。
|
|
62
|
+
# anchorGate 让升态保持到首个 minimal-like 推理块(四步兜底);
|
|
63
|
+
# promoteAfterFirstResponse 避免无工具首答后永久双工具,并在首轮结束时释放
|
|
64
|
+
# 锚门会话。升态后目录以 PTC 模式呈现(线上单一 run_code 工具,步边界切换),
|
|
65
|
+
# 工作区指令与技能目录延迟一步;bootstrapMaxTokens 封顶 phase-1 输出预算
|
|
66
|
+
# (1024),升态后再剥离。压缩后回落到受控阶段(双工具 + compactionTools),
|
|
67
|
+
# 直到新升态信号——本预设把 dispatch/subagent 加进 compactionTools,
|
|
68
|
+
# 让压缩后的编排者仍能继续委派。
|
|
69
|
+
- id: tool-bootstrap
|
|
70
|
+
name: ./tool-bootstrap.mjs
|
|
71
|
+
config:
|
|
72
|
+
shellTools: [bash]
|
|
73
|
+
commonTools: [str_replace_editor]
|
|
74
|
+
messageSources: [user, goal]
|
|
75
|
+
anchorGate: true
|
|
76
|
+
maxBootstrapSteps: 4
|
|
77
|
+
promoteAfterFirstResponse: true
|
|
78
|
+
bootstrapMaxTokens: 1024
|
|
79
|
+
compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question, dispatch, subagent]
|
|
80
|
+
deferredSources: [agent-instructions, skill-catalog]
|
|
81
|
+
deferredGraceSteps: 1
|
|
82
|
+
promotedPresentation: code
|
|
83
|
+
# 升态后以一次性非指令式提示替代 AGENTS.md 全文注入(梁神 V2 保留的
|
|
84
|
+
# issue #388 设定),知识经 read / skill_load 可达。设 false 恢复全文注入。
|
|
85
|
+
instructionHint: true
|
|
86
|
+
|
|
87
|
+
# 工作区指令在 phase 1 被 tool-bootstrap 隔离,升态后延迟一步注入并保持更新。
|
|
40
88
|
- id: agent-instructions
|
|
41
89
|
name: '@deepseek-ai/dsh-agent-instructions'
|
|
42
90
|
config:
|
|
@@ -57,8 +105,25 @@
|
|
|
57
105
|
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
58
106
|
disabled: !!js process.platform !== 'win32'
|
|
59
107
|
|
|
108
|
+
# win32 专用 `bash` 工具(custom-bash.mjs,同梁神 V2):官方 tool-bash 在
|
|
109
|
+
# win32 禁用、tool-pwsh 又不叫 bash,phase-1 的 Minimal 锚点(shellTools:
|
|
110
|
+
# [bash])会找不到 shell 而退化成全量目录。此工具经跨平台 subprocess 缝隙
|
|
111
|
+
# 执行 Git Bash(`bash -c`),保住锚点;posix 下官方 tool-bash 已提供
|
|
112
|
+
# 同名工具,此行走禁用。非零退出上报不抛;调用间状态不持久;Windows 无
|
|
113
|
+
# OS 沙箱限制。`bashPath` 可选(缺省按 Git 安装根推断)。
|
|
114
|
+
- id: custom-bash
|
|
115
|
+
name: ./custom-bash.mjs
|
|
116
|
+
disabled: !!js process.platform !== 'win32'
|
|
117
|
+
|
|
60
118
|
# ── filesystem ──────────────────────────────────────────────────────────────
|
|
61
119
|
|
|
120
|
+
# Phase-1 编辑器(同梁神 V2):顶层直排,复用宿主沙箱 ctx.fs 而非裸本地
|
|
121
|
+
# 文件系统,schema 与官方 Minimal `str_replace_editor` 一致。
|
|
122
|
+
- id: str-replace-editor
|
|
123
|
+
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
|
124
|
+
config:
|
|
125
|
+
maxOutputChars: 16000
|
|
126
|
+
|
|
62
127
|
# 两者都注册进宿主 tools 注册表且不提供服务,无需 realm;fs 服务与其策略留在宿主。
|
|
63
128
|
- id: tool-fs
|
|
64
129
|
name: '@deepseek-ai/dsh-tool-fs'
|
|
@@ -90,10 +155,15 @@
|
|
|
90
155
|
|
|
91
156
|
# ── goals ───────────────────────────────────────────────────────────────────
|
|
92
157
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
158
|
+
# goal 服务与会话驱动留在宿主平面,Gateway 在那里解析它们。人机命令与
|
|
159
|
+
# 模型可见工具注册进本预设的 scope 层。
|
|
160
|
+
- id: command-goal
|
|
161
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
162
|
+
|
|
163
|
+
# 只放模型可见的工具。goal 服务与会话驱动留在宿主平面:Gateway 以 Remote
|
|
164
|
+
# endpoint 服务 goal 域,接收方来自生成描述符,条目本地 realm 会隐藏它。
|
|
165
|
+
# 注册表本就按会话键控,一个宿主实例服务所有会话;预设只决定其 agent
|
|
166
|
+
# 能否调用 goal 工具。
|
|
97
167
|
- id: tool-goal
|
|
98
168
|
name: '@deepseek-ai/dsh-tool-goal'
|
|
99
169
|
|
|
@@ -182,6 +252,7 @@
|
|
|
182
252
|
config:
|
|
183
253
|
provider: spawn
|
|
184
254
|
toolName: subagent
|
|
255
|
+
modelSelectionSettings: true
|
|
185
256
|
backgroundMode: continuable
|
|
186
257
|
|
|
187
258
|
- id: tool-subagent-fork
|