dsh-subagent-profile 0.3.2 → 0.3.4
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 +77 -40
- package/README.zh.md +111 -74
- package/docs/screenshots/dispatch-card.png +0 -0
- package/docs/screenshots/settings-page1.png +0 -0
- package/docs/screenshots/settings-page2.png +0 -0
- package/index.mjs +276 -81
- package/lib/client.js +3218 -166
- package/lib/core/adoption-reminder.mjs +48 -0
- package/lib/core/adoption-tracker.mjs +430 -0
- package/lib/core/background-ledger.mjs +71 -0
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-evidence.mjs +145 -0
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +413 -0
- package/lib/core/delegation.mjs +111 -50
- package/lib/core/dispatch-gates.mjs +153 -0
- package/lib/core/dispatch-guard.mjs +156 -0
- package/lib/core/dispatch-schema.mjs +103 -14
- package/lib/core/dispatch-tool.mjs +220 -204
- package/lib/core/draft-gates.mjs +45 -0
- package/lib/core/drafts-store.mjs +45 -0
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-advice.mjs +224 -0
- package/lib/core/evolution-ledger.mjs +300 -0
- package/lib/core/evolution-summary.mjs +255 -0
- package/lib/core/http-routes.mjs +256 -72
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +161 -43
- package/lib/core/prices.mjs +46 -0
- package/lib/core/profile-directory.mjs +139 -0
- package/lib/core/profile-provider.mjs +42 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/shims.mjs +67 -76
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +82 -83
- package/presets/orchestrator/agent.cordis.yml +59 -87
- package/presets/orchestrator/NOTICE +0 -3
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
// lib/core/profiles-store.mjs — profile
|
|
2
|
-
//
|
|
3
|
-
// node
|
|
1
|
+
// lib/core/profiles-store.mjs — profile 注册表 store 与启用/禁用开关
|
|
2
|
+
// (从 index.mjs 逐字移出并重构为工厂;import-free——
|
|
3
|
+
// 仅 node 内置 + lib/core/pure.mjs,无 @deepseek-ai 依赖)。
|
|
4
4
|
//
|
|
5
|
-
// `dshHome()`
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// original closure state.
|
|
5
|
+
// `dshHome()` 与 `BUILTIN_SEEDS` 是模块级导出,工厂与 lib/core/http-routes.mjs
|
|
6
|
+
// (设置 HTTP 处理器查 seeds)共享。per-apply store 由
|
|
7
|
+
// `createProfileStore({ dshHome, logger })` 构造,取代 apply 闭包单例:
|
|
8
|
+
// 每次 apply() 调用都有自己的 profiles Map / deletedBuiltins Set /
|
|
9
|
+
// allowFailOpen 标志,与原始闭包状态完全一致。
|
|
11
10
|
//
|
|
12
|
-
// 工厂内嵌的 loadProfiles/persistProfiles/
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// 工厂内嵌的 loadProfiles/persistProfiles/resolveProfile 按行门抽为模块级函数,
|
|
12
|
+
// per-apply 状态收集进 `state` 对象注入;开关态(enabled / evolutionAdvice)
|
|
13
|
+
// 经 loadState/persistState 读写共享 state.json。
|
|
15
14
|
|
|
16
15
|
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
16
|
import { homedir } from 'node:os';
|
|
18
17
|
import { join } from 'node:path';
|
|
19
18
|
import { sanitizeProfile } from './pure.mjs';
|
|
20
19
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
20
|
+
// 解析 DSH 主目录(env 覆盖优先,平台回退)——与 dsh-persona-ref bundle 相同
|
|
21
|
+
// 策略:用户 profile 持久化到 ~/.dsh/subagent-profiles.json,跨 harness 工作目录
|
|
22
|
+
// 稳定。
|
|
24
23
|
export function dshHome() {
|
|
25
24
|
const raw = process.env.DSH_HOME;
|
|
26
25
|
if (typeof raw === 'string' && raw.trim() !== '') {
|
|
@@ -32,15 +31,13 @@ export function dshHome() {
|
|
|
32
31
|
return join(homedir(), '.dsh');
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
// 1. Profile
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// connection.defaults.thinking — this deployment leaves thinking unset,
|
|
43
|
-
// so the full set is advertised for deepseek-v4-flash).
|
|
34
|
+
// 1. Profile 注册表(per-instance 状态;bundle 行是进程级,故本 Map 是单例
|
|
35
|
+
// store,与动态插件完全一致)。内置种子带 `builtin: true`,reset/remove 可
|
|
36
|
+
// 识别它们,改过的内置方案与纯用户 profile 可区分。
|
|
37
|
+
// description 是语义化的:一行定位 + 何时用,帮助模型选择。reasoningEffort
|
|
38
|
+
// 档位经 llm-deepseek adapter 核验(off/high/max;见 resolveModel 对
|
|
39
|
+
// connection.defaults.thinking 的门控——本部署未设 thinking,
|
|
40
|
+
// 故对 deepseek-v4-flash 全档位宣传)。
|
|
44
41
|
export const BUILTIN_SEEDS = [
|
|
45
42
|
{ id: 'swap-standard', name: '标准编码', description: '切换到 standard 预设的完整编码工具集。当父会话不是 standard、但子任务需要完整编码能力时用。', preset: 'standard', tokenTier: 'balanced', builtin: true },
|
|
46
43
|
{ 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 }
|
|
@@ -50,14 +47,14 @@ export const BUILTIN_SEEDS = [
|
|
|
50
47
|
|
|
51
48
|
function resolveProfile(profiles, id) {
|
|
52
49
|
const found = profiles.get(id);
|
|
53
|
-
if (found === undefined) throw new Error(`dispatch:
|
|
54
|
-
if (found.enabled === false) throw new Error(`dispatch: profile "${id}"
|
|
50
|
+
if (found === undefined) throw new Error(`dispatch: 未知 profile "${id}"(可在设置页的 profile 列表确认可用的 id)`);
|
|
51
|
+
if (found.enabled === false) throw new Error(`dispatch: profile "${id}" 已禁用(可在设置页重新启用该 profile)`);
|
|
55
52
|
return found;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// { entries: undefined }
|
|
55
|
+
// 两种文件形状:v1 = 裸数组;v2 = { version:2, profiles:[...],
|
|
56
|
+
// allowFailOpen:<bool> }。返回 { entries, version };无法识别的形状返回
|
|
57
|
+
// { entries: undefined }。
|
|
61
58
|
function classifyProfileFile(parsed) {
|
|
62
59
|
if (Array.isArray(parsed)) return { entries: parsed, version: 1 };
|
|
63
60
|
if (parsed !== null && typeof parsed === 'object' && Array.isArray(parsed.profiles)) {
|
|
@@ -66,10 +63,9 @@ function classifyProfileFile(parsed) {
|
|
|
66
63
|
return { entries: undefined, version: undefined };
|
|
67
64
|
}
|
|
68
65
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// 'loaded' | 'skipped' | 'deleted' so loadProfiles can keep its counters.
|
|
66
|
+
// 逐条净化(strict=false):超限字段被丢弃(字段移除)+ 告警;超长 persona
|
|
67
|
+
// 保留 + 告警(绝不静默截断)。坏条目跳过(fail-soft)。返回
|
|
68
|
+
// 'loaded' | 'skipped' | 'deleted',loadProfiles 据此维护计数。
|
|
73
69
|
function applyProfileEntry(raw, profiles, deletedBuiltins, logger) {
|
|
74
70
|
if (!raw || typeof raw !== 'object' || typeof raw.id !== 'string' || raw.id.length === 0) {
|
|
75
71
|
logger.warn('[dsh-subagent-profile] skipping malformed profile entry:', raw === null ? String(raw) : typeof raw);
|
|
@@ -95,13 +91,11 @@ function applyProfileEntry(raw, profiles, deletedBuiltins, logger) {
|
|
|
95
91
|
return 'loaded';
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
// 1b.
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// add/remove HTTP routes rewrite the file. Persistence is an enhancement, not
|
|
104
|
-
// a hard dependency: any failure only warns and the builtin seeds work.
|
|
94
|
+
// 1b. 用户 profile 持久化。设置服务服务不了本插件(其写路径硬性要求
|
|
95
|
+
// register(ns, schema) + schemastery schema),故用户 profile 经 node:fs
|
|
96
|
+
// 持久化到 ~/.dsh/subagent-profiles.json——bundle 有 node 全局,不像需要可选
|
|
97
|
+
// `fs` 服务的动态插件沙箱。启动时加载一次;add/remove HTTP 路由重写文件。
|
|
98
|
+
// 持久化是增强而非硬依赖:任何失败只告警,内置种子照常工作。
|
|
105
99
|
function loadProfiles(state, logger) {
|
|
106
100
|
if (!existsSync(state.profilesFile)) return;
|
|
107
101
|
try {
|
|
@@ -112,7 +106,7 @@ function loadProfiles(state, logger) {
|
|
|
112
106
|
return;
|
|
113
107
|
}
|
|
114
108
|
if (version !== 2) {
|
|
115
|
-
// v1
|
|
109
|
+
// v1(或无版本数组):内存迁移,保持 fail-open 兼容。
|
|
116
110
|
state.allowFailOpen = true;
|
|
117
111
|
logger.warn('[dsh-subagent-profile] v1 数据:fail-open 兼容模式');
|
|
118
112
|
} else {
|
|
@@ -127,8 +121,8 @@ function loadProfiles(state, logger) {
|
|
|
127
121
|
if (status === 'loaded') loaded++;
|
|
128
122
|
else if (status === 'skipped') skipped++;
|
|
129
123
|
}
|
|
130
|
-
//
|
|
131
|
-
//
|
|
124
|
+
// 静默成功:报告载入了多少持久化 profile(没有时跳过——缺失/空文件是
|
|
125
|
+
// 正常首启)。
|
|
132
126
|
if (loaded > 0) logger.info(`[dsh-subagent-profile] loaded ${loaded} persisted profile(s)`);
|
|
133
127
|
if (skipped > 0) logger.warn(`[dsh-subagent-profile] skipped ${skipped} malformed profile entry(ies)`);
|
|
134
128
|
} catch (error) {
|
|
@@ -137,13 +131,11 @@ function loadProfiles(state, logger) {
|
|
|
137
131
|
}
|
|
138
132
|
|
|
139
133
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* the caller can signal "已保存但未持久化" while the in-memory state keeps
|
|
146
|
-
* driving this process. Always writes the v2 envelope shape.
|
|
134
|
+
* 持久化每个 `persisted: true` 的 profile 加内置删除墓碑。
|
|
135
|
+
* 原子写:同目录写 `<profilesFile>.tmp`,再 `renameSync` 覆盖目标
|
|
136
|
+
* (崩溃只留下旧文件完好,绝不留下截断文件)。失败可见:失败不抛、不
|
|
137
|
+
* 回滚内存 `profiles` Map——返回 `{ persisted: false }`,调用方可以提示
|
|
138
|
+
* 「已保存但未持久化」,内存态继续驱动本进程。恒写 v2 信封形状。
|
|
147
139
|
*/
|
|
148
140
|
function persistProfiles(state, logger) {
|
|
149
141
|
const entries = [];
|
|
@@ -166,42 +158,67 @@ function persistProfiles(state, logger) {
|
|
|
166
158
|
renameSync(tmp, state.profilesFile);
|
|
167
159
|
return { persisted: true };
|
|
168
160
|
} catch (error) {
|
|
169
|
-
//
|
|
161
|
+
// 尽力清理残留的 tmp 文件(rename 未执行)。
|
|
170
162
|
try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
171
163
|
logger.warn('[dsh-subagent-profile] persisted profile write failed:', error instanceof Error ? error.message : String(error));
|
|
172
164
|
return { persisted: false, error: error instanceof Error ? error.message : String(error) };
|
|
173
165
|
}
|
|
174
166
|
}
|
|
175
167
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
168
|
+
// 插件开关态(state.json):enabled(默认开,禁用即关派发工具)+ evolutionAdvice
|
|
169
|
+
// (派发优化建议注入,默认关,需显式开)+ escapeEnabled(逃生舱放行,默认关——信任
|
|
170
|
+
// 底板,仅设置页显式 opt-in 可放开)。三开关共用同一 state.json,读写整份对象
|
|
171
|
+
// 以互不覆盖;缺失文件回退默认值(全新部署),损坏文件 fail-closed(三开关全关,
|
|
172
|
+
// 绝不静默回退 enabled:true —— 安全禁用会被撤销),内存态仍驱动本进程。
|
|
173
|
+
const STATE_DEFAULTS = { enabled: true, evolutionAdvice: false, escapeEnabled: false };
|
|
174
|
+
const STATE_FAIL_CLOSED = { enabled: false, evolutionAdvice: false, escapeEnabled: false };
|
|
175
|
+
|
|
176
|
+
function loadState(state) {
|
|
177
|
+
if (!existsSync(state.stateFile)) return { ...STATE_DEFAULTS };
|
|
180
178
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
const parsed = JSON.parse(readFileSync(state.stateFile, 'utf8'));
|
|
180
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('state 文件不是对象');
|
|
181
|
+
return {
|
|
182
|
+
enabled: parsed.enabled !== false,
|
|
183
|
+
evolutionAdvice: parsed.evolutionAdvice === true,
|
|
184
|
+
escapeEnabled: parsed.escapeEnabled === true,
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// fail-closed(S2):损坏的 state.json 若回退 enabled:true,会把「安全禁用」静默撤销。
|
|
188
|
+
// 三开关一律关(派发/建议/逃生舱全停),显式 warn;下次持久化写入即自愈。
|
|
189
|
+
if (state.logger !== undefined && typeof state.logger.warn === 'function') {
|
|
190
|
+
state.logger.warn('[dsh-subagent-profile] state.json 损坏,按 fail-closed 处理(三开关全关):', error instanceof Error ? error.message : String(error));
|
|
184
191
|
}
|
|
185
|
-
|
|
186
|
-
// fall through to enabled
|
|
192
|
+
return { ...STATE_FAIL_CLOSED };
|
|
187
193
|
}
|
|
188
|
-
return true;
|
|
189
194
|
}
|
|
190
195
|
|
|
191
|
-
|
|
196
|
+
// 原子写 state.json(tmp+rename,与 persistProfiles 同口径,修复「全库唯一非原子写」)。
|
|
197
|
+
// 返回 { persisted: true } 或 { persisted: false, error },供开关路由回传 persisted 信号;
|
|
198
|
+
// 写失败仍触发治理审计钩子(不抛),内存态照常驱动本进程。
|
|
199
|
+
function persistState(state, patch, onGovernanceFailure) {
|
|
200
|
+
const next = { ...loadState(state), ...patch };
|
|
201
|
+
const tmp = `${state.stateFile}.tmp`;
|
|
192
202
|
try {
|
|
193
|
-
writeFileSync(
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8');
|
|
204
|
+
renameSync(tmp, state.stateFile);
|
|
205
|
+
return { persisted: true };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
208
|
+
// best effort——内存态仍驱动本进程;
|
|
209
|
+
// 开关态(含 persistEnabled)写失败也是治理审计丢失,通知审计钩子(不抛)。
|
|
210
|
+
if (typeof onGovernanceFailure === 'function') onGovernanceFailure();
|
|
211
|
+
return { persisted: false, error: error instanceof Error ? error.message : String(error) };
|
|
196
212
|
}
|
|
197
213
|
}
|
|
198
214
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
215
|
+
// 构造一个 store 实例。`dshHome` 是已解析的主目录字符串(来自导出的
|
|
216
|
+
// dshHome());`logger` 是 apply 时的 ctx.logger。`onGovernanceFailure`(可选)
|
|
217
|
+
// 是任何 profile/state 持久化写失败时触发的审计钩子——一次治理审计丢失事件。
|
|
218
|
+
// per-apply 状态(profiles / deletedBuiltins / paths / allowFailOpen)收进
|
|
219
|
+
// `state` 供移出的模块级函数使用。profile 注册表表面与拆分前工厂一致;两个
|
|
220
|
+
// 开关访问器(enabled / evolutionAdvice)读写共享 state.json。
|
|
221
|
+
export function createProfileStore({ dshHome, logger, onGovernanceFailure = () => {} }) {
|
|
205
222
|
const profiles = new Map(BUILTIN_SEEDS.map((p) => [p.id, { ...p }]));
|
|
206
223
|
const deletedBuiltins = new Set();
|
|
207
224
|
const state = {
|
|
@@ -210,17 +227,27 @@ export function createProfileStore({ dshHome, logger }) {
|
|
|
210
227
|
profilesFile: join(dshHome, 'subagent-profiles.json'),
|
|
211
228
|
stateFile: join(dshHome, 'subagent-profiles.state.json'),
|
|
212
229
|
allowFailOpen: false,
|
|
230
|
+
logger,
|
|
213
231
|
};
|
|
214
232
|
return {
|
|
215
233
|
profiles,
|
|
216
234
|
deletedBuiltins,
|
|
217
235
|
resolveProfile: (id) => resolveProfile(profiles, id),
|
|
218
236
|
loadProfiles: () => loadProfiles(state, logger),
|
|
219
|
-
persistProfiles: () =>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
237
|
+
persistProfiles: () => {
|
|
238
|
+
const result = persistProfiles(state, logger);
|
|
239
|
+
// 治理审计:profile 变更持久化失败计一次治理丢失(写入不抛,仅为高可见标记)。
|
|
240
|
+
if (result.persisted === false) onGovernanceFailure();
|
|
241
|
+
return result;
|
|
242
|
+
},
|
|
243
|
+
loadEnabled: () => loadState(state).enabled,
|
|
244
|
+
persistEnabled: (enabled) => persistState(state, { enabled }, onGovernanceFailure),
|
|
245
|
+
loadEvolutionAdvice: () => loadState(state).evolutionAdvice,
|
|
246
|
+
persistEvolutionAdvice: (value) => persistState(state, { evolutionAdvice: value === true }, onGovernanceFailure),
|
|
247
|
+
loadEscapeEnabled: () => loadState(state).escapeEnabled,
|
|
248
|
+
persistEscapeEnabled: (value) => persistState(state, { escapeEnabled: value === true }, onGovernanceFailure),
|
|
249
|
+
// getAllowFailOpen:cost guard 必须在派发时读取迁移标志;
|
|
250
|
+
// loadProfiles/persistProfiles 持有该写入。
|
|
224
251
|
getAllowFailOpen: () => state.allowFailOpen,
|
|
225
252
|
};
|
|
226
253
|
}
|
package/lib/core/pure.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// lib/core/pure.mjs —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// lib/core/pure.mjs — 纯函数助手(从 index.mjs 拆出,经后续重构扩展)。
|
|
2
|
+
// 无 @deepseek-ai import、无外部依赖:唯一 import 是 node:crypto(dispatchLabel
|
|
3
|
+
// 用的 node 内置),@deepseek-ai 符号全部收敛在 lib/core/shims.mjs(唯一入口),
|
|
4
|
+
// 本模块可在 bare-CI 测试中无 junction 包安全导入。
|
|
5
|
+
// 这里每个函数都是确定性的,无服务/fs 访问。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
// 官方 toStopReason:把一轮结束原因映射到接缝的终态词汇表。
|
|
8
10
|
export function toStopReason(reason) {
|
|
9
11
|
switch (reason?.kind) {
|
|
10
12
|
case 'completed': return 'completed';
|
|
@@ -15,7 +17,7 @@ export function toStopReason(reason) {
|
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
//
|
|
20
|
+
// 官方 stopReasonError + withPartialText 措辞(dsh-tool-subagent L55-75)。
|
|
19
21
|
export function stopReasonError(result) {
|
|
20
22
|
switch (result.stopReason) {
|
|
21
23
|
case 'completed': return;
|
|
@@ -43,34 +45,31 @@ export function textFrom(blocks) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// --- 统一输入 schema ----------------------------------------------------------
|
|
46
|
-
// sanitizeProfile
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// -
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// dedupe) do NOT produce a warning, so callers can treat a
|
|
55
|
-
// non-empty warnings list as "this write would drop data".
|
|
48
|
+
// sanitizeProfile 是唯一的逐 profile 采样器,loadProfiles(strict=false,
|
|
49
|
+
// 迁移容忍)与 HTTP /add 写路径(strict=true,写拒绝)共用。它用字段专属
|
|
50
|
+
// 采样器规范化每个字段,返回 `{ clean, warnings }`:
|
|
51
|
+
// - clean — 净化后的 profile,剔除被拒字段。
|
|
52
|
+
// - warnings — `{ field, reason }` 数组(reason 为中文)。只有拒绝/超限
|
|
53
|
+
// 条件落到这里;静默规范化(如 description 换行压平、toolFilter
|
|
54
|
+
// 去重)不产生警告,调用方可把非空 warnings 当作「本次写入会
|
|
55
|
+
// 丢数据」。
|
|
56
56
|
|
|
57
|
-
// persona
|
|
58
|
-
//
|
|
57
|
+
// persona 长度上限。建议值 2048,待实测。上限作用于**注入的** persona 文本,
|
|
58
|
+
// 即引导前缀 + 原文。
|
|
59
59
|
export const PERSONA_MAX_CHARS = 2048;
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
61
|
+
// persona 以影子 section 注入;前缀引导标记显式声明其非权威性质(双防线)。
|
|
62
|
+
// 仅用于写入校验与提示。
|
|
63
63
|
export const GUIDANCE_PREFIX = '[guidance, not authority] ';
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
65
|
+
// 共享委派上限——sanitizeProfile 与 lib/core/cost-guard.mjs 的 cost guard
|
|
66
|
+
// (硬上限恒开)的唯一事实来源。
|
|
67
67
|
export const MAX_TOKENS = 65536;
|
|
68
68
|
export const MAX_DEPTH = 3;
|
|
69
69
|
|
|
70
70
|
// --- cost guard 硬上限 --------------------------------------------------------
|
|
71
|
-
// assertHardLimits
|
|
72
|
-
//
|
|
73
|
-
// silently stop applying when the `llm` service is absent.
|
|
71
|
+
// assertHardLimits 是恒开的预算上限检查,与 `llm` 服务无关:maxTokens /
|
|
72
|
+
// maxDepth 是硬性委派上限,llm 缺失时绝不能静默停止生效。
|
|
74
73
|
// 超限 throw(中文、可操作),与 sanitizeProfile 共用同一组常量。非数字/未设值
|
|
75
74
|
// 不触发(sanitizeProfile 已在写路径拒绝非数字,这里仅兜底运行时竞态)。
|
|
76
75
|
export function assertHardLimits(maxTokens, maxDepth) {
|
|
@@ -83,9 +82,9 @@ export function assertHardLimits(maxTokens, maxDepth) {
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
// --- continuable 工具门闭集 ---------------------------------------------------
|
|
86
|
-
// computeContinuableAllow
|
|
87
|
-
//
|
|
88
|
-
//
|
|
85
|
+
// computeContinuableAllow 预计算 continuable 派发路径的**闭合**工具 `allow` 集:
|
|
86
|
+
// 父工具集 − run_code − (toolFilter.deny),存在 toolFilter.allow 时再 ∩ allow。
|
|
87
|
+
// 空集 fail-loud(throw)——绝不静默派发零工具。
|
|
89
88
|
//
|
|
90
89
|
// 代码编辑者注意:这里写代码注释的「假设 / 失效条件」必须与 dispatch-tool.mjs
|
|
91
90
|
// continuable 分支的注释保持一致(要求写入代码注释与 README)。
|
|
@@ -109,11 +108,10 @@ export function computeContinuableAllow(parentNames, toolFilter = {}) {
|
|
|
109
108
|
return result;
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
// description/name
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// 是单行、无换行。
|
|
111
|
+
// description/name 采样器:`\n→空格` 压平。这些是显示行(dispatch:profiles
|
|
112
|
+
// section 把它们插进单行),换行会破坏该行。压平仅作存储层;引号包裹由显示层
|
|
113
|
+
// 负责——显示层在该行给 description 加引号(空时显示 '(无描述)' 不套引号),
|
|
114
|
+
// 本纯函数只保证存储值是单行、无换行。
|
|
117
115
|
function sanitizeShortText(value) {
|
|
118
116
|
return value.replace(/\r\n|\r|\n/g, ' ');
|
|
119
117
|
}
|
|
@@ -192,8 +190,8 @@ function sanitizeToolFilterField(value, clean, warnings) {
|
|
|
192
190
|
const deduped = [...new Set(sub.filter((s) => s.length > 0))];
|
|
193
191
|
if (deduped.length > 0) tf[op] = deduped;
|
|
194
192
|
}
|
|
195
|
-
//
|
|
196
|
-
//
|
|
193
|
+
// 未知 toolFilter 键忽略;空(或全非法)toolFilter 直接从 clean 省略
|
|
194
|
+
// (与 /add 写路径一致)。
|
|
197
195
|
if (Object.keys(tf).length > 0) clean.toolFilter = tf;
|
|
198
196
|
}
|
|
199
197
|
|
|
@@ -215,24 +213,22 @@ function sanitizeTokenTierField(value, clean, warnings) {
|
|
|
215
213
|
// 在 `textFrom` 之前执行,把回灌进父上下文的体积压到阈值内。
|
|
216
214
|
//
|
|
217
215
|
// 阈值常量:head / tail / minKeep 是**子级**预期裁剪口径(比 agent.cordis.yml
|
|
218
|
-
// 现行 compaction-basic 的 4096/1024
|
|
216
|
+
// 现行 compaction-basic 的 4096/1024 更保守)。宿主
|
|
219
217
|
// toolResultPruner.pruneContent(blocks) 只接收 blocks,自身读取其配置
|
|
220
218
|
// (thresholdChars/headChars/tailChars),因此这三个常量当前**不会**作为实参
|
|
221
219
|
// 传给宿主 pruner;它们记录本条目的子级口径,并保留给后续「信封 / 精修
|
|
222
|
-
//
|
|
220
|
+
// 剪枝」路径使用。改动前先实测真实分布再校准(当前子输出样本不足,待积累)。
|
|
223
221
|
export const PRUNE_HEAD_CHARS = 2048;
|
|
224
222
|
export const PRUNE_TAIL_CHARS = 1024;
|
|
225
223
|
export const PRUNE_MIN_KEEP = 128;
|
|
226
224
|
|
|
227
|
-
//
|
|
228
|
-
// - `blocks`
|
|
229
|
-
// - `pruner`
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// falls back to the full output, because automatic pruning must NEVER swallow a
|
|
235
|
-
// legitimate child result.
|
|
225
|
+
// 对单个子 Agent 结果的内容块应用宿主剪枝。
|
|
226
|
+
// - `blocks` — result.output 内容块(或 undefined;非数组)。
|
|
227
|
+
// - `pruner` — `toolResultPruner` 服务,或 undefined。
|
|
228
|
+
// 返回(可能被剪的)块,或空数组占位。pruner 缺失(无压缩插件的无头部署)
|
|
229
|
+
// 或内容非数组时回退为不剪——剪枝是增强,绝非硬依赖。
|
|
230
|
+
// 宿主 pruner 在异常内容形状上抛错也回退为完整输出,因为自动剪枝绝不允许
|
|
231
|
+
// 吞掉合法的子结果。
|
|
236
232
|
export function pruneBlocks(blocks, pruner) {
|
|
237
233
|
if (Array.isArray(blocks) && pruner !== undefined && typeof pruner.pruneContent === 'function') {
|
|
238
234
|
try {
|
|
@@ -245,12 +241,58 @@ export function pruneBlocks(blocks, pruner) {
|
|
|
245
241
|
return Array.isArray(blocks) ? blocks : [];
|
|
246
242
|
}
|
|
247
243
|
|
|
244
|
+
// --- 输出回灌信任标注 ----------------------------------------------------------
|
|
245
|
+
// trustLabel 给 completed 子结果的回收文本加结构化信任标注前缀,让父上下文
|
|
246
|
+
// 不把子 Agent 输出误当高信任指令(正文先见标注、再见子输出)。profile/preset
|
|
247
|
+
// 与 buildMeta 同源字段;preset 为 'inherit'(子继承父预设、无换用)或字段缺失
|
|
248
|
+
// 时省略该段。前缀只含这两个元数据标识符,绝不携带 prompt/persona 原文。
|
|
249
|
+
function trustFields(meta) {
|
|
250
|
+
const m = meta !== null && meta !== undefined && typeof meta === 'object' ? meta : {};
|
|
251
|
+
const fields = [];
|
|
252
|
+
if (typeof m.profile === 'string' && m.profile !== '') fields.push(['profile', m.profile]);
|
|
253
|
+
if (typeof m.preset === 'string' && m.preset !== '' && m.preset !== 'inherit') fields.push(['preset', m.preset]);
|
|
254
|
+
return fields;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 模型可见的标注前缀:进回收 output 文本(render 行首部格式不变,前缀在 output 内部)。
|
|
258
|
+
export function trustLabel(meta) {
|
|
259
|
+
const fields = trustFields(meta);
|
|
260
|
+
return fields.length === 0
|
|
261
|
+
? '[dispatch:trusted-output]'
|
|
262
|
+
: `[dispatch:trusted-output] ${fields.map(([key, value]) => `${key}=${value}`).join(' ')}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 审计记录:与 trustLabel 同字段集的 JSON,只进 logger、不进结果字符串,无子输出
|
|
266
|
+
// 原文。调用方拼 `[dsh-subagent-profile] trusted-output: ` 前缀后以 info 记录。
|
|
267
|
+
export function trustAudit(meta) {
|
|
268
|
+
return JSON.stringify(Object.fromEntries(trustFields(meta)));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- 派发 label 去敏 ----------------------------------------------------------
|
|
272
|
+
// dispatchLabel 生成子会话标题/任务卡 label:只含结构化段(profile/preset/model)
|
|
273
|
+
// 加 prompt 的 sha1 前 8 位十六进制哈希,绝不落 prompt 原文前缀。哈希让排障时
|
|
274
|
+
// 不同任务可辨识(不同 prompt → 不同哈希),却不泄漏任何前缀内容。profile 缺失
|
|
275
|
+
// 时回退 '(inline)';preset 缺失或为 'inherit'(子继承父预设、无换用)时省略该
|
|
276
|
+
// 段;model 缺失时省略该段——与 buildMeta/trustFields 的字段口径一致。
|
|
277
|
+
export function dispatchLabel(args, merged) {
|
|
278
|
+
const a = args !== null && typeof args === 'object' ? args : {};
|
|
279
|
+
const m = merged !== null && typeof merged === 'object' ? merged : {};
|
|
280
|
+
const profile = typeof a.profile === 'string' && a.profile !== '' ? a.profile : '(inline)';
|
|
281
|
+
const promptText = String(a.prompt ?? '');
|
|
282
|
+
const hash = createHash('sha1').update(promptText, 'utf8').digest('hex').slice(0, 8);
|
|
283
|
+
const segments = [`dispatch:${profile}`];
|
|
284
|
+
if (typeof m.preset === 'string' && m.preset !== '' && m.preset !== 'inherit') segments.push(m.preset);
|
|
285
|
+
if (typeof m.model === 'string' && m.model !== '') segments.push(m.model);
|
|
286
|
+
segments.push(`#${hash}`);
|
|
287
|
+
return segments.join(' ');
|
|
288
|
+
}
|
|
289
|
+
|
|
248
290
|
// --- continuable 可见性修复 ----------------------------------------------------
|
|
249
291
|
// 共享一致性规则:三个 closed `output.schema.oneOf` 分支
|
|
250
|
-
// (background / continuable / foreground)
|
|
251
|
-
//
|
|
252
|
-
// `profile/preset/provider/model`
|
|
253
|
-
//
|
|
292
|
+
// (background / continuable / foreground) 在新增结果元数据字段时必须携带
|
|
293
|
+
// **相同**的共享元数据键集——`ignored`、`reasoningEffort`、
|
|
294
|
+
// `profile/preset/provider/model` 必须出现在全部三个分支,使模型侧 schema
|
|
295
|
+
// 不会拒绝「忘了」该字段的分支。
|
|
254
296
|
//
|
|
255
297
|
// 判据:元数据集合 = 每个分支 properties 的键集,**剔除**各分支自有的判别键
|
|
256
298
|
// (`kind`/`jobId`/`subagentId`/`output`——background/continuable/foreground 各自
|
|
@@ -287,10 +329,9 @@ export function assertResultSchemaConsistency(schema) {
|
|
|
287
329
|
return true;
|
|
288
330
|
}
|
|
289
331
|
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// the object prototype chain (prototype-pollution guard).
|
|
332
|
+
// profile 上可能存在的完整字段集。任何其它键——包括 __proto__ / constructor /
|
|
333
|
+
// prototype(JSON.parse 可以产出为 OWN 属性)——一律丢弃而不复制,赋值永远
|
|
334
|
+
// 不会走进对象原型链(原型污染防护)。
|
|
294
335
|
const KNOWN_PROFILE_FIELDS = new Set([
|
|
295
336
|
'id', 'name', 'description', 'persona', 'preset', 'provider', 'model',
|
|
296
337
|
'reasoningEffort', 'enabled', 'maxTokens', 'maxDepth', 'toolFilter',
|
|
@@ -298,18 +339,16 @@ const KNOWN_PROFILE_FIELDS = new Set([
|
|
|
298
339
|
]);
|
|
299
340
|
|
|
300
341
|
/**
|
|
301
|
-
*
|
|
302
|
-
* - strict=false
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* Non-rejection normalizations (description/name flatten, toolFilter dedupe) are
|
|
308
|
-
* silent and never produce a warning.
|
|
342
|
+
* 净化一条 profile。`options.strict`:
|
|
343
|
+
* - strict=false(迁移读取):超长 persona 保留(不截断)并告警;超限
|
|
344
|
+
* maxTokens/maxDepth 与非法 toolFilter 子字段丢弃(字段移除)+ 告警。
|
|
345
|
+
* - strict=true(写路径):超长 persona 也从 clean 移除并告警,调用方可
|
|
346
|
+
* 用 400 拒绝整次写入。
|
|
347
|
+
* 非拒绝型规范化(description/name 压平、toolFilter 去重)静默进行,绝不告警。
|
|
309
348
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
349
|
+
* 安全:`clean` 是 `Object.create(null)`(无继承的 `__proto__` setter),且只
|
|
350
|
+
* 复制白名单字段,敌意的 `__proto__` / `constructor` / `prototype` 键被忽略而
|
|
351
|
+
* 不会污染结果。
|
|
313
352
|
*/
|
|
314
353
|
export function sanitizeProfile(profile, options = {}) {
|
|
315
354
|
const { strict = false } = options;
|
|
@@ -325,8 +364,13 @@ export function sanitizeProfile(profile, options = {}) {
|
|
|
325
364
|
continue;
|
|
326
365
|
}
|
|
327
366
|
switch (key) {
|
|
367
|
+
case 'id':
|
|
368
|
+
case 'preset':
|
|
328
369
|
case 'name':
|
|
329
370
|
case 'description':
|
|
371
|
+
// id/preset 与 name/description 同走文本采样:压平换行,阻断经 \n 注入
|
|
372
|
+
// orchestrator systemPrompt 的提示注入面(H1)。id/preset 此前走 default
|
|
373
|
+
// 分支原样拷贝,可注入换行破坏 dispatch:profiles 行。
|
|
330
374
|
sanitizeTextField(key, value, clean, warnings);
|
|
331
375
|
break;
|
|
332
376
|
case 'persona':
|