dsh-subagent-profile 0.4.0 → 0.5.1
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 +16 -57
- package/README.zh.md +16 -57
- package/index.mjs +97 -122
- package/lib/client.js +631 -88
- package/lib/core/cleanup.mjs +33 -0
- package/lib/core/data-maintenance.mjs +139 -0
- package/lib/core/decision-trace.mjs +20 -1
- package/lib/core/delegation.mjs +16 -0
- package/lib/core/dispatch-gates.mjs +122 -9
- package/lib/core/dispatch-schema.mjs +45 -7
- package/lib/core/dispatch-tool.mjs +30 -46
- package/lib/core/evolution-advice.mjs +31 -10
- package/lib/core/evolution-assets.mjs +13 -0
- package/lib/core/evolution-canary.mjs +427 -0
- package/lib/core/evolution-draft.mjs +21 -0
- package/lib/core/evolution-engine.mjs +176 -31
- package/lib/core/evolution-generate.mjs +7 -23
- package/lib/core/evolution-lag.mjs +78 -0
- package/lib/core/evolution-ledger.mjs +4 -1
- package/lib/core/evolution-routes.mjs +157 -16
- package/lib/core/http-routes.mjs +22 -7
- package/lib/core/model-policy.mjs +104 -0
- package/lib/core/presets-sync.mjs +24 -2
- package/lib/core/profile-directory.mjs +17 -0
- package/lib/core/profile-provider.mjs +7 -2
- package/lib/core/profiles-query.mjs +178 -0
- package/lib/core/profiles-store.mjs +40 -6
- package/lib/core/pure.mjs +1 -1
- package/lib/core/reminder-store.mjs +37 -3
- package/lib/core/snapshots.mjs +148 -0
- 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,33 @@
|
|
|
1
|
+
// lib/core/cleanup.mjs — 启动与卸载清理辅助(从 index.mjs 移出以守装配文件行门)。
|
|
2
|
+
// 卸载清理:本插件 3 个数据文件 + orchestrator-v2 预设目录改名备份(effect 清理器
|
|
3
|
+
// 在禁用/热重载/退出时都可能触发;硬删除会静默丢用户数据)。启动清理:删上个
|
|
4
|
+
// 生命周期留下的 .removed-* 备份残留(只清本插件前缀,防热重载反复产生新备份
|
|
5
|
+
// 积累);卸载后用户手动改回原名的场景不受影响。全部 fail-soft(改名/删除失败
|
|
6
|
+
// 不抛)。import-free(仅 node 内置),可被裸 CI 单测。
|
|
7
|
+
|
|
8
|
+
import { readdirSync, renameSync, rmSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export function removeOwnedData(home) {
|
|
12
|
+
const stamp = Date.now();
|
|
13
|
+
for (const file of ['subagent-profiles.json', 'subagent-profiles.state.json', 'subagent-profiles.failed-traces.json']) {
|
|
14
|
+
try { renameSync(join(home, file), join(home, `${file}.removed-${stamp}`)); } catch { /* 源不存在(正常首启)等:best effort */ }
|
|
15
|
+
}
|
|
16
|
+
try { renameSync(join(home, '.agent-presets', 'orchestrator-v2'), join(home, '.agent-presets', `orchestrator-v2.removed-${stamp}`)); } catch { /* best effort */ }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function cleanRemovedBackups(home) {
|
|
20
|
+
for (const dir of [home, join(home, '.agent-presets')]) {
|
|
21
|
+
let names;
|
|
22
|
+
try { names = readdirSync(dir); } catch { continue; }
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
try {
|
|
25
|
+
if (name.startsWith('subagent-profiles.') && name.includes('.removed-')) {
|
|
26
|
+
rmSync(join(dir, name), { force: true });
|
|
27
|
+
} else if (name.startsWith('orchestrator-v2.removed-')) {
|
|
28
|
+
rmSync(join(dir, name), { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
} catch { /* best effort */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// lib/core/data-maintenance.mjs — 设置页数据维护(四区清理 + 快照管理路由)。
|
|
2
|
+
// 一个入口四区(避免多按钮):① 候选/建议清理(进化资产域全清)② 快照清理
|
|
3
|
+
// (snapshots/ 一键清理)③ 提醒中心已处理归档(只移出活动列表,存储与
|
|
4
|
+
// governance-audit 保留——提醒=审计记录,绝不物理删除)④ 重置统计(dispatch.jsonl
|
|
5
|
+
// 封存为 dispatch-archive-<ts>.jsonl,历史台账归档保留、统计从零开始;summaries.json
|
|
6
|
+
// 置空重建)。每次清理记一条治理审计(kind: data-cleared / reminder-archived /
|
|
7
|
+
// stats-reset,字段 target/count/ts)。快照列表/开关/单条查看路由同驻本模块
|
|
8
|
+
// (快照域装配与清除口径单一来源)。CSRF 与 loopback 守卫由 createHttpRoutes
|
|
9
|
+
// 入口统一完成,本模块只做解析与执行。
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { basename, dirname, join } from 'node:path';
|
|
13
|
+
import { json, readBody, persistOk } from './http-helpers.mjs';
|
|
14
|
+
import { writeSummaries } from './evolution-summary.mjs';
|
|
15
|
+
import { SNAPSHOT_CAP } from './snapshots.mjs';
|
|
16
|
+
|
|
17
|
+
// ④ 重置统计执行:活动台账封存(rename,内容字节不变)+ 空台账重建 + summaries
|
|
18
|
+
// 置空。归档名有意不匹配轮转封存件模式(dispatch-<seq>-<ts>.jsonl):金丝雀基线
|
|
19
|
+
// 不再读入(统计从零开始),后续自动轮转也不会淘汰(决策留痕保留)。
|
|
20
|
+
function resetStats(deps) {
|
|
21
|
+
const dir = dirname(deps.dispatchFile);
|
|
22
|
+
let count = 0;
|
|
23
|
+
let archivedFile = null;
|
|
24
|
+
if (existsSync(deps.dispatchFile)) {
|
|
25
|
+
try {
|
|
26
|
+
count = readFileSync(deps.dispatchFile, 'utf8').split('\n').filter((line) => line.trim() !== '').length;
|
|
27
|
+
} catch { count = 0; }
|
|
28
|
+
if (count > 0) {
|
|
29
|
+
const stamp = Date.now();
|
|
30
|
+
let archive = join(dir, `dispatch-archive-${stamp}.jsonl`);
|
|
31
|
+
let attempt = 0;
|
|
32
|
+
while (existsSync(archive)) {
|
|
33
|
+
attempt += 1;
|
|
34
|
+
archive = join(dir, `dispatch-archive-${stamp}-${attempt}.jsonl`);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
renameSync(deps.dispatchFile, archive);
|
|
39
|
+
archivedFile = basename(archive);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
deps.logger.warn(`[dsh-subagent-profile] 台账封存失败(${error instanceof Error ? error.message : String(error)}),统计未重置`);
|
|
42
|
+
return { count: 0, error: '台账封存失败,统计未重置' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(deps.dispatchFile, '', 'utf8');
|
|
48
|
+
writeSummaries(deps.summariesFile, { l1: {}, l2: {} });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
deps.logger.warn(`[dsh-subagent-profile] 统计重建失败(${error instanceof Error ? error.message : String(error)})`);
|
|
51
|
+
return { count, archivedFile, error: '统计重建失败' };
|
|
52
|
+
}
|
|
53
|
+
deps.recordAudit({ kind: 'stats-reset', target: 'stats', count, ts: Date.now(), ...(archivedFile !== null ? { archivedFile } : {}) });
|
|
54
|
+
return { count, archivedFile };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createDataMaintenance({ dispatchFile, summariesFile, snapshots, reminderStore, evolution, logger, store, recordAudit = () => {} } = {}) {
|
|
58
|
+
const deps = { dispatchFile, summariesFile, snapshots, reminderStore, evolution, logger, store, recordAudit };
|
|
59
|
+
return {
|
|
60
|
+
// 四区清理:非法 zone 返回 null(调用方 400);成功返回 { count, ... }。
|
|
61
|
+
// 审计一次清理一条(target/count/ts),快照清理沿用 B8 口径 kind=data-cleared。
|
|
62
|
+
clean(zone) {
|
|
63
|
+
if (zone === 'candidates') {
|
|
64
|
+
const count = deps.evolution !== undefined && typeof deps.evolution.purgeCandidates === 'function' ? deps.evolution.purgeCandidates() : 0;
|
|
65
|
+
deps.recordAudit({ kind: 'data-cleared', target: 'candidates', count, ts: Date.now() });
|
|
66
|
+
return { count };
|
|
67
|
+
}
|
|
68
|
+
if (zone === 'snapshots') {
|
|
69
|
+
const count = deps.snapshots !== undefined && typeof deps.snapshots.clear === 'function' ? deps.snapshots.clear() : 0;
|
|
70
|
+
deps.recordAudit({ kind: 'data-cleared', target: 'snapshots', count, ts: Date.now() });
|
|
71
|
+
return { count };
|
|
72
|
+
}
|
|
73
|
+
if (zone === 'reminders') {
|
|
74
|
+
const count = deps.reminderStore !== undefined && typeof deps.reminderStore.archiveActed === 'function' ? deps.reminderStore.archiveActed() : 0;
|
|
75
|
+
deps.recordAudit({ kind: 'reminder-archived', target: 'reminders', count, ts: Date.now() });
|
|
76
|
+
return { count };
|
|
77
|
+
}
|
|
78
|
+
if (zone === 'stats') return resetStats(deps);
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── 路由处理(快照管理 + 数据维护 + 提醒归档视图)─────────────────────────────
|
|
85
|
+
|
|
86
|
+
function handleSnapshotsList(deps, res) {
|
|
87
|
+
if (deps.snapshots === undefined) return json(res, 404, { ok: false, error: '配置快照域未装配' });
|
|
88
|
+
return json(res, 200, {
|
|
89
|
+
ok: true,
|
|
90
|
+
snapshotEnabled: typeof deps.store.loadSnapshotEnabled === 'function' ? deps.store.loadSnapshotEnabled() : false,
|
|
91
|
+
count: deps.snapshots.count(),
|
|
92
|
+
cap: SNAPSHOT_CAP,
|
|
93
|
+
entries: deps.snapshots.list(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handleSnapshotsSetEnabled(deps, req, res) {
|
|
98
|
+
const body = await readBody(req);
|
|
99
|
+
const next = !!(body && body.snapshotEnabled === true);
|
|
100
|
+
const persist = deps.store.persistSnapshotEnabled(next);
|
|
101
|
+
return persistOk(res, { snapshotEnabled: deps.store.loadSnapshotEnabled() }, persist);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleSnapshotView(deps, url, res) {
|
|
105
|
+
const id = url.searchParams.get('id') ?? '';
|
|
106
|
+
const snapshot = deps.snapshots !== undefined ? deps.snapshots.view(id) : null;
|
|
107
|
+
if (snapshot === null) return json(res, 404, { ok: false, error: '快照不存在或已损坏' });
|
|
108
|
+
return json(res, 200, { ok: true, snapshot });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function handleMaintenanceClean(deps, req, res) {
|
|
112
|
+
const body = await readBody(req);
|
|
113
|
+
const zone = typeof body.zone === 'string' ? body.zone : '';
|
|
114
|
+
if (zone === '') return json(res, 400, { ok: false, error: 'zone 不能为空' });
|
|
115
|
+
if (deps.maintenance === undefined) return json(res, 404, { ok: false, error: '数据维护入口未装配' });
|
|
116
|
+
const result = deps.maintenance.clean(zone);
|
|
117
|
+
if (result === null) return json(res, 400, { ok: false, error: 'zone 无效(candidates/snapshots/reminders/stats)' });
|
|
118
|
+
if (result.error !== undefined) return json(res, 500, { ok: false, error: result.error });
|
|
119
|
+
return json(res, 200, { ok: true, zone, count: result.count, ...(result.archivedFile !== undefined ? { archivedFile: result.archivedFile } : {}) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function handleRemindersArchived(deps, url, res) {
|
|
123
|
+
const offset = Number.parseInt(url.searchParams.get('offset') ?? '0', 10);
|
|
124
|
+
const limit = Number.parseInt(url.searchParams.get('limit') ?? '20', 10);
|
|
125
|
+
const page = deps.reminderStore !== undefined && typeof deps.reminderStore.listArchived === 'function'
|
|
126
|
+
? deps.reminderStore.listArchived({ offset, limit })
|
|
127
|
+
: { reminders: [], total: 0, offset: 0, limit: 20 };
|
|
128
|
+
return json(res, 200, { ok: true, ...page });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 数据维护域路由分发:只处理本域路径,未知交给统一 404。
|
|
132
|
+
export async function routeDataMaintenance(deps, req, res, url, sub) {
|
|
133
|
+
if (req.method === 'GET' && sub === '/snapshots') return handleSnapshotsList(deps, res);
|
|
134
|
+
if (req.method === 'POST' && sub === '/snapshots/set-enabled') return handleSnapshotsSetEnabled(deps, req, res);
|
|
135
|
+
if (req.method === 'GET' && sub === '/snapshot') return handleSnapshotView(deps, url, res);
|
|
136
|
+
if (req.method === 'POST' && sub === '/maintenance/clean') return handleMaintenanceClean(deps, req, res);
|
|
137
|
+
if (req.method === 'GET' && sub === '/reminders/archived') return handleRemindersArchived(deps, url, res);
|
|
138
|
+
return json(res, 404, { ok: false, error: `未知路由 ${sub}` });
|
|
139
|
+
}
|
|
@@ -99,12 +99,19 @@ export function requestedOf(args, opts = {}) {
|
|
|
99
99
|
const unknownKeys = args !== null && typeof args === 'object' && !Array.isArray(args)
|
|
100
100
|
? Object.keys(args).filter((key) => args[key] !== undefined && !DISPATCH_PARAMETER_KEYS.includes(key))
|
|
101
101
|
: [];
|
|
102
|
+
// 字段双名:requested 对外显示补官方名 reasoning_effort(与官方 subagent 工具
|
|
103
|
+
// 字段对齐);内部记录键 reasoningEffort 保持(台账/客户端读取方不动,后续 UI
|
|
104
|
+
// 批次迁移读取方后可移除旧键)。内部键记录归一后值(camel 优先),官方键记录
|
|
105
|
+
// 调用方实际使用的名(snake 优先)——同送两义时两值并留、交由 fail-loud 拒绝。
|
|
106
|
+
const internalEffort = args.reasoningEffort !== undefined ? args.reasoningEffort : args.reasoning_effort;
|
|
107
|
+
const officialEffort = args.reasoning_effort !== undefined ? args.reasoning_effort : args.reasoningEffort;
|
|
102
108
|
return {
|
|
103
109
|
profile: args.profile,
|
|
104
110
|
preset: args.preset,
|
|
105
111
|
provider: args.provider,
|
|
106
112
|
model: args.model,
|
|
107
|
-
reasoningEffort:
|
|
113
|
+
reasoningEffort: internalEffort,
|
|
114
|
+
...(officialEffort !== undefined ? { reasoning_effort: officialEffort } : {}),
|
|
108
115
|
tokenTier: args.tokenTier,
|
|
109
116
|
persona_present: typeof args.persona === 'string' && args.persona.length > 0,
|
|
110
117
|
toolFilter_present: args.toolFilter !== undefined,
|
|
@@ -119,6 +126,18 @@ export function requestedOf(args, opts = {}) {
|
|
|
119
126
|
};
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
// 生效来源判定(per-call > profile > parent > default):回答「这次生效的
|
|
130
|
+
// provider/model 路由是谁决定的」。call=per-call 显式指定任一模型字段;
|
|
131
|
+
// profile=方案绑定;parent=继承父会话路由;default=provider 默认或符号默认。
|
|
132
|
+
// 与官方 README 的解析优先级语义对齐;写入 requested.source 与派发台账
|
|
133
|
+
// effectiveModelSource 共用同一判定。
|
|
134
|
+
export function effectiveModelSourceOf(args, merged, parent) {
|
|
135
|
+
if (args?.provider !== undefined || args?.model !== undefined) return 'call';
|
|
136
|
+
if (merged?.provider !== undefined || merged?.model !== undefined) return 'profile';
|
|
137
|
+
if (parent?.options?.provider !== undefined || parent?.options?.model !== undefined) return 'parent';
|
|
138
|
+
return 'default';
|
|
139
|
+
}
|
|
140
|
+
|
|
122
141
|
// 生效值(与 buildMeta 同源字段 + ignored 列表)。ignored 与结果块的 ignored
|
|
123
142
|
// 列表同源:continuable 为 ['preset','reasoningEffort'],前/后台为空数组。
|
|
124
143
|
export function effectiveMeta(meta, ignored = []) {
|
package/lib/core/delegation.mjs
CHANGED
|
@@ -91,6 +91,22 @@ export function collectChildCalls(session, maxCalls = 6) {
|
|
|
91
91
|
return { calls, truncated: totalCalls > maxCalls, totalCalls };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// childTotalTokens:宿主 tokenMeter 启发式估算(surface 口径,非计费 usage);仅
|
|
95
|
+
// completed 结算时测量,服务缺失/非函数/抛错均返回 undefined(fail-soft)。
|
|
96
|
+
export function measureChildTokens(getService, session) {
|
|
97
|
+
if (session === undefined || session === null) return undefined;
|
|
98
|
+
const meter = getService('tokenMeter');
|
|
99
|
+
if (meter === undefined || typeof meter.measure !== 'function') return undefined;
|
|
100
|
+
try {
|
|
101
|
+
const measured = meter.measure(session);
|
|
102
|
+
return measured !== null && typeof measured === 'object' && typeof measured.totalTokens === 'number'
|
|
103
|
+
? measured.totalTokens
|
|
104
|
+
: undefined;
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
// 把一个后台 one-shot run 结算为带前台同等可观测元数据的任务结果。非 completed
|
|
95
111
|
// 停因转 failed(aborted => killed,官方词汇表)并附部分输出;硬失败绝不 reject
|
|
96
112
|
// 任务。
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
// 从 dispatch-tool.mjs 拆出以守文件行门。闸逻辑与 fail-loud 文案逐字不变;
|
|
3
3
|
// 这里只「应用闸并就地记录 decisionTrace 的 pass/fail」,不改变任何派发行为。
|
|
4
4
|
//
|
|
5
|
-
// 依赖边界(无 @deepseek-ai 依赖):只 import lib/core
|
|
6
|
-
// (assertCostGuard)、decision-trace(recordGate / hardLimitChecks
|
|
7
|
-
// (MAX_TOKENS / MAX_DEPTH
|
|
5
|
+
// 依赖边界(无 @deepseek-ai 依赖):只 import lib/core 模块——cost-guard
|
|
6
|
+
// (assertCostGuard)、decision-trace(recordGate / hardLimitChecks /
|
|
7
|
+
// effectiveModelSourceOf)、pure(MAX_TOKENS / MAX_DEPTH)、model-policy
|
|
8
|
+
// (官方模型选择投影读取与路由断言)、escape(放行审计快照口径)。
|
|
8
9
|
|
|
9
10
|
import { assertCostGuard } from './cost-guard.mjs';
|
|
10
|
-
import { recordGate, hardLimitChecks } from './decision-trace.mjs';
|
|
11
|
+
import { recordGate, hardLimitChecks, effectiveModelSourceOf } from './decision-trace.mjs';
|
|
11
12
|
import { MAX_TOKENS, MAX_DEPTH, computeContinuableAllow } from './pure.mjs';
|
|
13
|
+
import { assertOfficialModelRoute, readModelSelectionPolicy } from './model-policy.mjs';
|
|
14
|
+
import { escapeGatesSnapshot } from './escape.mjs';
|
|
12
15
|
|
|
13
16
|
// 预检:显式具体 preset 必须在运行时推导的白名单内;与父 composed 预设相同的
|
|
14
17
|
// preset 改写为 'inherit'(不换装)。纯函数化:whitelist / parentComposed 由调用方
|
|
@@ -34,6 +37,64 @@ export function applyWhitelistGate(trace, merged, whitelist, parentComposed) {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
// 治理审计(official-model-reject):dispatch 渠道带决策轨迹快照(escapeGatesSnapshot),
|
|
41
|
+
// provider 渠道带渠道口径快照(whitelist/cost 已过、intersection deferred、approval never)。
|
|
42
|
+
function recordOfficialModelReject(deps, effective) {
|
|
43
|
+
deps.evoLedger.recordGovernanceAudit({
|
|
44
|
+
kind: 'official-model-reject',
|
|
45
|
+
provider: effective.provider ?? null,
|
|
46
|
+
model: effective.model ?? null,
|
|
47
|
+
session_id: deps.sessionId ?? null,
|
|
48
|
+
gates_snapshot: deps.gatesSnapshot ?? { whitelist: 'pass', cost: 'pass', intersection: 'deferred', approval: 'never' },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 官方模型选择闸:policy 未配置(投影缺失)→ 静默跳过——轨迹不新增闸条目,
|
|
53
|
+
// 行为与 0.4.0 完全一致;配置后对生效路由(解析链完成后的最终 provider/model,
|
|
54
|
+
// 含继承带出的模型)精确成对校验,越界 fail-loud 三层中文错误 + 治理审计。
|
|
55
|
+
// fail 时先记 fail 闸再 rethrow。
|
|
56
|
+
export function applyOfficialModelGate(deps, { policy, effective }) {
|
|
57
|
+
if (policy === undefined || policy.configured !== true) return;
|
|
58
|
+
const input = {
|
|
59
|
+
policy: { enabled: policy.enabled, allowedModels: [...policy.allowedModels] },
|
|
60
|
+
effective: { provider: effective.provider, model: effective.model },
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
assertOfficialModelRoute(policy, effective.provider, effective.model);
|
|
64
|
+
recordGate(deps.trace, { name: 'official-model', input, output: { allowed: true }, verdict: 'pass' });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
recordGate(deps.trace, { name: 'official-model', input, output: { allowed: false }, verdict: 'fail', reason: error.message });
|
|
67
|
+
recordOfficialModelReject({ ...deps, gatesSnapshot: escapeGatesSnapshot(deps.trace) }, effective);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// dispatch 渠道官方模型预检:一次完成「生效来源判定(写入 requested 快照供台账
|
|
73
|
+
// 与派发台账共用)→ 投影读取 → 生效路由判定 → 闸记录/审计」,调用方只需一行接线。
|
|
74
|
+
// 投影缺失时闸静默跳过;source 值 call/profile/parent/default 恒写入 requested。
|
|
75
|
+
export function runOfficialModelPreflight(deps, parent, merged, args) {
|
|
76
|
+
deps.trace.requested.source = effectiveModelSourceOf(args, merged, parent);
|
|
77
|
+
const policy = readModelSelectionPolicy(parent?.ctx, parent?.session);
|
|
78
|
+
const effective = {
|
|
79
|
+
provider: merged.provider ?? parent?.options?.provider,
|
|
80
|
+
model: merged.model ?? parent?.options?.model,
|
|
81
|
+
};
|
|
82
|
+
applyOfficialModelGate(deps, { policy, effective });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// provider 渠道执行(宿主直连 start 的路径无 decisionTrace):只写治理审计 +
|
|
86
|
+
// 抛同一三层中文文案;判定输入为 resolveChildAgentOptions 解析后的最终
|
|
87
|
+
// agentOptions provider/model(含父继承)。
|
|
88
|
+
export function applyOfficialModelProviderGate(deps, policy, effective) {
|
|
89
|
+
if (policy === undefined || policy.configured !== true) return;
|
|
90
|
+
try {
|
|
91
|
+
assertOfficialModelRoute(policy, effective.provider, effective.model);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
recordOfficialModelReject(deps, effective);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
37
98
|
// 成本闸:硬上限(maxTokens/maxDepth)在调 assertCostGuard 前自行预判(不重复
|
|
38
99
|
// throw,真 throw 仍在 assertHardLimits);llm 能力面各检查项经 assertCostGuard
|
|
39
100
|
// 的 onCheck 回调逐项回填。预判读 merged(profile 合并后的实际判定值)而非原始
|
|
@@ -98,15 +159,67 @@ export function applyIntersectionGate(trace, mode, intersectionInput, merged, pa
|
|
|
98
159
|
}
|
|
99
160
|
}
|
|
100
161
|
|
|
162
|
+
// --- 预算余量三态与低位封顶 ---------------------------------------------------
|
|
163
|
+
// 阈值常量(后续校准只改这里):可用比例 ≥50% 充足 / ≥15% 紧张 / 其余告警。
|
|
164
|
+
export const BUDGET_AMPLE_RATIO = 0.5;
|
|
165
|
+
export const BUDGET_TIGHT_RATIO = 0.15;
|
|
166
|
+
|
|
167
|
+
// 剩余 token 与可用比例(剩余按 0 兜底;上限非正数时比例按 0 = 告警)。
|
|
168
|
+
export function budgetRemaining(snapshot) {
|
|
169
|
+
const max = snapshot !== null && typeof snapshot === 'object' && typeof snapshot.maxParentTokens === 'number' ? snapshot.maxParentTokens : 0;
|
|
170
|
+
const tokens = snapshot !== null && typeof snapshot === 'object' && typeof snapshot.tokens === 'number' ? snapshot.tokens : 0;
|
|
171
|
+
const remaining = Math.max(0, max - tokens);
|
|
172
|
+
return { remaining, ratio: max > 0 ? remaining / max : 0 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// dispatch:budget section 三态文案(与 index.mjs 共用)。
|
|
176
|
+
export function budgetSectionText(snapshot) {
|
|
177
|
+
const { remaining, ratio } = budgetRemaining(snapshot);
|
|
178
|
+
const max = snapshot !== null && typeof snapshot === 'object' && typeof snapshot.maxParentTokens === 'number' ? snapshot.maxParentTokens : 0;
|
|
179
|
+
if (ratio >= BUDGET_AMPLE_RATIO) return `派发预算:充足(剩余 ${remaining} token / 上限 ${max})`;
|
|
180
|
+
if (ratio >= BUDGET_TIGHT_RATIO) return `派发预算:紧张(剩余 ${remaining} token / 上限 ${max}),优先 cheap 方案`;
|
|
181
|
+
return `派发预算:告警(剩余 ${remaining} token / 上限 ${max}),仅允许 cheap 方案;当前可用最高档=cheap`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 请求档位是否超出 cheap(tokenTier 或 model/effort 推定):tokenTier 缺省
|
|
185
|
+
// balanced;tokenTier=cheap 但 reasoningEffort 显式高于 off、或显式模型名非 flash
|
|
186
|
+
// 系 → 推定超出。不做 tokenTier→effort 映射(禁止事项),flash 名称启发式只拦
|
|
187
|
+
// 显式重负载。
|
|
188
|
+
export function requestedTierBeyondCheap(merged) {
|
|
189
|
+
const tier = merged !== null && typeof merged === 'object' && typeof merged.tokenTier === 'string' && merged.tokenTier !== '' ? merged.tokenTier : 'balanced';
|
|
190
|
+
if (tier !== 'cheap') return true;
|
|
191
|
+
const effort = merged !== null && typeof merged === 'object' ? merged.reasoningEffort : undefined;
|
|
192
|
+
if (typeof effort === 'string' && effort !== '' && effort !== 'off') return true;
|
|
193
|
+
const model = merged !== null && typeof merged === 'object' ? merged.model : undefined;
|
|
194
|
+
return typeof model === 'string' && model !== '' && !model.toLowerCase().includes('flash');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 低位封顶预检:余量 <15% 且请求档位超出 cheap → 记 budget fail 闸 + fail-loud。
|
|
198
|
+
function assertBudgetTierCap(deps, trace, parentSessionId, budget, merged) {
|
|
199
|
+
if (typeof parentSessionId !== 'string' || parentSessionId === '' || budgetRemaining(budget).ratio >= BUDGET_TIGHT_RATIO || !requestedTierBeyondCheap(merged)) return;
|
|
200
|
+
const message = '预算不足:当前可用最高档=cheap,请改用 cheap 方案或为会话增加预算';
|
|
201
|
+
recordGate(trace, {
|
|
202
|
+
name: 'budget',
|
|
203
|
+
input: { parentSessionId, current: budget, requestedTier: merged?.tokenTier ?? 'balanced(缺省)' },
|
|
204
|
+
output: { allowed: false },
|
|
205
|
+
verdict: 'fail',
|
|
206
|
+
reason: message,
|
|
207
|
+
});
|
|
208
|
+
deps.logger.warn(`[dsh-subagent-profile] ${message}`);
|
|
209
|
+
throw new Error(message);
|
|
210
|
+
}
|
|
211
|
+
|
|
101
212
|
// 总预算闸(并发上限 + 每父累计 token):前/后台在派发入口占并发额度(continuable
|
|
102
|
-
// 不占——持久会话启动、非在途 one-shot
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
213
|
+
// 不占——持久会话启动、非在途 one-shot)。低位封顶在 acquire 之前(fail-loud 不占
|
|
214
|
+
// 并发槽)。拒绝时记一道 budget fail 闸 + warn + 抛可行动中文错误(只拦新增派发,
|
|
215
|
+
// 数值内零影响);无父 sessionId 时 acquire 内部跳过守卫。返回
|
|
216
|
+
// { parentSessionId, finish }:finish(key, childTotalTokens) 是结算共用钩子
|
|
217
|
+
// (untrack + recordTokens + release),前/后台 settle 时恰好调用一次。
|
|
218
|
+
export function applyBudgetGate(deps, parent, args, merged, trace) {
|
|
107
219
|
if (args.continuable === true) return { parentSessionId: undefined, finish: () => {}, release: () => {} };
|
|
108
220
|
const parentSessionId = parent.session?.header?.id;
|
|
109
221
|
const budget = deps.guard.snapshot(parentSessionId);
|
|
222
|
+
assertBudgetTierCap(deps, trace, parentSessionId, budget, merged);
|
|
110
223
|
const acquired = deps.guard.acquire(parentSessionId);
|
|
111
224
|
if (!acquired.ok) {
|
|
112
225
|
const message = acquired.reason === 'concurrency'
|
|
@@ -14,20 +14,23 @@ export const DISPATCH_PARAMETERS = {
|
|
|
14
14
|
// 参数轴归属与约束(能力轴 / 预算轴 / 防护 / 模式):
|
|
15
15
|
// 能力轴 preset→whitelist 闸;model/provider→llm 目录核验;persona→仅 shadow
|
|
16
16
|
// order 0(不进任何闸);toolFilter→工具交集闸(只减不增)。
|
|
17
|
-
// 预算轴 reasoningEffort
|
|
17
|
+
// 预算轴 reasoningEffort/reasoning_effort(双名,官方名经 normalizeReasoningEffort
|
|
18
|
+
// 归一为 camel)→resolveCallConfig;maxTokens→MAX_TOKENS 硬上限;
|
|
18
19
|
// tokenTier→档位元数据(估算口径)。
|
|
19
20
|
// 防护 maxDepth→MAX_DEPTH 硬上限。
|
|
20
21
|
// 模式字段(run_in_background/continuable/envelope)是派发方式选择,不入配置覆盖。
|
|
21
22
|
// 新增字段时须按上列轴归类,并同步键集测试(percall-spec)的 WHITELIST_KEYS。
|
|
22
23
|
// per-call 规范化清单闭合:宿主 dsh-tools 的 DSL 参数解析**默认拒绝
|
|
23
24
|
// 未知键**(2026-08 实测:参数级 additionalProperties 只接受 value schema 对象、
|
|
24
|
-
// 不接受 boolean,故无法显式声明;键集闭合由 percall-spec 测试「键集恰为
|
|
25
|
-
//
|
|
25
|
+
// 不接受 boolean,故无法显式声明;键集闭合由 percall-spec 测试「键集恰为 15 键
|
|
26
|
+
// (含双名键 reasoningEffort/reasoning_effort)」锁定;mergeProfileArgs 只透传
|
|
27
|
+
// 白名单 9 键(merge 前经 normalizeReasoningEffort 归一官方名),未知字段到不了子请求)。
|
|
26
28
|
profile: { type: 'string', description: '容器选择(profile 注册表 id:内建 swap-standard/researcher,或设置页自建);省略则该 subagent 原样继承父预设与工具集,不加配置覆盖。' },
|
|
27
29
|
preset: { type: 'string', description: '能力轴·目标预设强制覆盖;必须是本运行时 system-trust 预设(白名单内的受信任预设),否则 fail-loud 拒绝;与父 composed 预设相同时改写为 inherit(不换装)。' },
|
|
28
|
-
model: { type: 'string', description: '能力轴·子 Agent 显式模型覆盖,per-call 优先于 profile;必须在该 provider 目录中已发布,否则 fail-loud 拒绝(无发现能力的空目录 adapter
|
|
30
|
+
model: { type: 'string', description: '能力轴·子 Agent 显式模型覆盖,per-call 优先于 profile;必须在该 provider 目录中已发布,否则 fail-loud 拒绝(无发现能力的空目录 adapter 无法校验时按兼容模式开关决定)。派发已遵循官方模型选择白名单(本会话启用时;未启用则继承父模型)。' },
|
|
29
31
|
provider: { type: 'string', description: '能力轴·子 Agent 显式 provider 覆盖,per-call 优先于 profile;必须已注册在其 provider 目录中,否则 fail-loud 拒绝。' },
|
|
30
|
-
reasoningEffort: { type: 'string', description: '预算轴·注入每个子请求的推理档位覆盖,per-call 优先于 profile;经 resolveCallConfig 校验该档受显式 provider/model 支持,否则 fail-loud 拒绝;continuable
|
|
32
|
+
reasoningEffort: { type: 'string', description: '预算轴·注入每个子请求的推理档位覆盖,per-call 优先于 profile;经 resolveCallConfig 校验该档受显式 provider/model 支持,否则 fail-loud 拒绝;continuable 模式忽略本字段(子会话不支持)。(弃用别名,请用 reasoning_effort)' },
|
|
33
|
+
reasoning_effort: { type: 'string', description: '预算轴·与 reasoningEffort 同义的官方字段名(与官方 subagent 工具字段同名):注入每个子请求的推理档位覆盖,per-call 优先于 profile;经 resolveCallConfig 校验该档受显式 provider/model 支持,否则 fail-loud 拒绝;continuable 模式忽略本字段(子会话不支持)。与 reasoningEffort 同送(两义即歧义)时 fail-loud 拒绝,请二选一。' },
|
|
31
34
|
tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '预算轴·成本/深度档位(估算口径,非计费):cheap 省 token、balanced 均衡、premium 高成本。覆盖 profile 的 tokenTier;缺省 balanced。' },
|
|
32
35
|
persona: { type: 'string', description: '能力轴·覆盖子 Agent 的部署 persona 段(只 shadow order 0 的 persona 影子段,不进 descriptor);per-call 优先于 profile。' },
|
|
33
36
|
toolFilter: {
|
|
@@ -45,7 +48,7 @@ export const DISPATCH_PARAMETERS = {
|
|
|
45
48
|
maxTokens: { type: 'number', description: '预算轴·子 Agent 显式 token 预算覆盖,per-call 优先于 profile;超过部署硬上限(MAX_TOKENS)fail-loud 拒绝。' },
|
|
46
49
|
maxDepth: { type: 'number', description: '防护·子 Agent 显式绝对递归深度上限覆盖,per-call 优先于 profile;超过部署硬上限(MAX_DEPTH)fail-loud 拒绝。' },
|
|
47
50
|
run_in_background: { type: 'boolean', description: '模式选择·异步 one-shot:走 jobs.start 包 start(),返回 jobId;仍单轮即弃,非 continuable。不入配置覆盖。' },
|
|
48
|
-
continuable: { type: 'boolean', description: '模式选择·启动持久可 continuation 的 subagent 而非 one-shot:立即返回 subagentId 并经 send_message 工具在后续轮次延续对话;默认 false。不入配置覆盖;与 run_in_background 同真时本字段优先,忽略 preset 换用与 reasoningEffort
|
|
51
|
+
continuable: { type: 'boolean', description: '模式选择·启动持久可 continuation 的 subagent 而非 one-shot:立即返回 subagentId 并经 send_message 工具在后续轮次延续对话;默认 false。不入配置覆盖;与 run_in_background 同真时本字段优先,忽略 preset 换用与 reasoningEffort。失败经结算通知回报;超时可 send_message 追问。' },
|
|
49
52
|
// 信封模式 = opt-in(仅「中段即交付物」的任务用):true 时把信封骨架追加进
|
|
50
53
|
// 子 persona(systemPrompt 影子段,模型可见),子 Agent 按结构化信封汇报;
|
|
51
54
|
// 默认 false 走结果剪枝回收。
|
|
@@ -55,9 +58,27 @@ export const DISPATCH_PARAMETERS = {
|
|
|
55
58
|
|
|
56
59
|
// 已知 per-call 参数键集(单一事实来源):decisionTrace 的 requested 快照据此把
|
|
57
60
|
// 未知字段记录为 unknown_keys(fail-visible,不静默吞)。宿主 dsh-tools DSL 对
|
|
58
|
-
//
|
|
61
|
+
// 参数默认拒绝未知键,此处是插件侧决策轨迹的可观测兜底。双名键(reasoningEffort /
|
|
62
|
+
// reasoning_effort)均在其中,任一名的请求都不会误报 unknown_keys。
|
|
59
63
|
export const DISPATCH_PARAMETER_KEYS = Object.freeze(Object.keys(DISPATCH_PARAMETERS));
|
|
60
64
|
|
|
65
|
+
// 字段双名归一(mergeProfileArgs 合并前调用):官方名 reasoning_effort 与内部旧名
|
|
66
|
+
// reasoningEffort 等效(同值同行为)。只传官方名时归一为 reasoningEffort(下游
|
|
67
|
+
// 只读 camel);同送两义即歧义 → fail-loud 拒绝(与「未知键拒绝」同风格,绝不
|
|
68
|
+
// 静默二选一)。旧名保留为弃用别名(description 已标),后续 major 移除。
|
|
69
|
+
export function normalizeReasoningEffort(args) {
|
|
70
|
+
if (args === null || typeof args !== 'object') return args;
|
|
71
|
+
const snake = args.reasoning_effort;
|
|
72
|
+
const camel = args.reasoningEffort;
|
|
73
|
+
if (snake !== undefined && camel !== undefined) {
|
|
74
|
+
throw new Error('dispatch: reasoning_effort 与 reasoningEffort 不得同送(两义即歧义,请二选一)');
|
|
75
|
+
}
|
|
76
|
+
if (snake === undefined) return args;
|
|
77
|
+
const normalized = { ...args, reasoningEffort: snake };
|
|
78
|
+
delete normalized.reasoning_effort;
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
export const DISPATCH_OUTPUT_SCHEMA = {
|
|
62
83
|
// 每个结果上的可观测元数据。oneOf 覆盖 background 变体(kind/jobId)与
|
|
63
84
|
// foreground 变体(output),两者闭合且都携带生效的委派值。`ignored` 必须
|
|
@@ -78,6 +99,7 @@ export const DISPATCH_OUTPUT_SCHEMA = {
|
|
|
78
99
|
model: { type: 'string' },
|
|
79
100
|
reasoningEffort: { type: 'string' },
|
|
80
101
|
tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费)。' },
|
|
102
|
+
imageText: { type: 'string', description: '一句话生效画像(方案/模型路由/档位),经 presentationMeta 供决策卡片展示;不进 render 行。' },
|
|
81
103
|
childTotalTokens: { type: 'number', description: '子 Agent 会话 token 估算总量(宿主启发式估算,非计费 usage token);后台结算经 job 结果携带,dispatch 结果不带。' },
|
|
82
104
|
childUsage: {
|
|
83
105
|
type: 'object',
|
|
@@ -109,6 +131,7 @@ export const DISPATCH_OUTPUT_SCHEMA = {
|
|
|
109
131
|
model: { type: 'string' },
|
|
110
132
|
reasoningEffort: { type: 'string' },
|
|
111
133
|
tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费)。' },
|
|
134
|
+
imageText: { type: 'string', description: '一句话生效画像(方案/模型路由/档位),经 presentationMeta 供决策卡片展示;不进 render 行。' },
|
|
112
135
|
childTotalTokens: { type: 'number', description: '子 Agent 会话 token 估算总量(宿主启发式估算,非计费 usage token);仅前台/后台结算时携带,continuable 实际省略。' },
|
|
113
136
|
childUsage: {
|
|
114
137
|
type: 'object',
|
|
@@ -139,6 +162,7 @@ export const DISPATCH_OUTPUT_SCHEMA = {
|
|
|
139
162
|
model: { type: 'string' },
|
|
140
163
|
reasoningEffort: { type: 'string' },
|
|
141
164
|
tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费)。' },
|
|
165
|
+
imageText: { type: 'string', description: '一句话生效画像(方案/模型路由/档位),经 presentationMeta 供决策卡片展示;不进 render 行。' },
|
|
142
166
|
childTotalTokens: { type: 'number', description: '子 Agent 会话 token 估算总量(宿主启发式估算,非计费 usage token);仅前台 completed 结算时携带。' },
|
|
143
167
|
childUsage: {
|
|
144
168
|
type: 'object',
|
|
@@ -161,6 +185,20 @@ export const DISPATCH_OUTPUT_SCHEMA = {
|
|
|
161
185
|
]
|
|
162
186
|
};
|
|
163
187
|
|
|
188
|
+
// 一句话生效画像(imageText):有 profile → `方案 X;provider/model 或继承父;
|
|
189
|
+
// 档位 Y`;无 profile(继承父)→ `继承父方案;provider/model;档位 Y`。字段随
|
|
190
|
+
// presentationMeta 投影进决策卡片,不进 render 行(UI 展示留后续版本)。
|
|
191
|
+
export function imageTextOf(meta) {
|
|
192
|
+
const tier = meta !== null && typeof meta === 'object' && typeof meta.tokenTier === 'string' && meta.tokenTier !== '' ? meta.tokenTier : 'balanced';
|
|
193
|
+
const profile = meta !== null && typeof meta === 'object' ? meta.profile : undefined;
|
|
194
|
+
const hasProfile = typeof profile === 'string' && profile !== '' && profile !== '(inline)';
|
|
195
|
+
const provider = meta !== null && typeof meta === 'object' && typeof meta.provider === 'string' ? meta.provider : '(parent)';
|
|
196
|
+
const model = meta !== null && typeof meta === 'object' && typeof meta.model === 'string' ? meta.model : '(parent)';
|
|
197
|
+
const routeKnown = provider !== '(parent)' && model !== '(parent)';
|
|
198
|
+
if (hasProfile) return `方案 ${profile};${routeKnown ? `${provider}/${model}` : '继承父'};档位 ${tier}`;
|
|
199
|
+
return `继承父方案;${provider}/${model};档位 ${tier}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
164
202
|
// 结果 render 行(从 dispatch-tool.mjs 逐字移入以守文件行门):continuable 丢弃
|
|
165
203
|
// preset 换用与 reasoningEffort —— 渲染行把 `ignored` 列表回显出来
|
|
166
204
|
// (`reasoningEffort=<值>(ignored)`,再加 ignored 项明细),让模型「看见」被丢弃项;
|