dsh-subagent-profile 0.3.3 → 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 +48 -20
- package/README.zh.md +101 -73
- 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 +142 -73
- package/lib/client.js +2239 -380
- 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/cost-evidence.mjs +145 -0
- package/lib/core/cost-guard.mjs +1 -1
- package/lib/core/decision-trace.mjs +11 -31
- package/lib/core/delegation.mjs +8 -5
- package/lib/core/dispatch-gates.mjs +12 -5
- package/lib/core/dispatch-guard.mjs +6 -0
- package/lib/core/dispatch-schema.mjs +10 -5
- package/lib/core/dispatch-tool.mjs +52 -45
- package/lib/core/draft-gates.mjs +45 -0
- package/lib/core/drafts-store.mjs +45 -0
- package/lib/core/evolution-advice.mjs +224 -0
- package/lib/core/evolution-ledger.mjs +20 -9
- package/lib/core/evolution-summary.mjs +46 -223
- package/lib/core/http-routes.mjs +154 -85
- package/lib/core/presets-sync.mjs +254 -256
- package/lib/core/prices.mjs +46 -0
- package/lib/core/profile-directory.mjs +139 -0
- package/lib/core/profile-provider.mjs +3 -2
- package/lib/core/profiles-store.mjs +1 -1
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/shims.mjs +11 -1
- package/package.json +82 -82
- package/presets/orchestrator/agent.cordis.yml +243 -243
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// lib/core/cost-evidence.mjs — 省 token 证据(反事实对照,纯函数)。
|
|
2
|
+
// 判定读 trace.requested.model(不能读 effective.model:buildMeta 会把继承的父
|
|
3
|
+
// 模型名写进 effective,无法区分「继承父」与「显式选 pro」)。未指定 model 时
|
|
4
|
+
// saving=0 并打标「未指定模型(继承父)」,让根因①可见。
|
|
5
|
+
//
|
|
6
|
+
// 成本口径:B 级五段单价可用时按 usage 逐段计价(元/千 token);否则回退 A 级
|
|
7
|
+
// 单次均价(docs/cost-closed-loop-design.md L14 实测)。无价格/无 usage/无模型
|
|
8
|
+
// 一律 fail-soft 返回 undefined,不显示不猜数。
|
|
9
|
+
|
|
10
|
+
import { MODEL_AVG_COST, MODEL_USAGE_PRICES, priceKeyFor } from './prices.mjs';
|
|
11
|
+
|
|
12
|
+
function costFromUsage(model, usage) {
|
|
13
|
+
const key = priceKeyFor(model);
|
|
14
|
+
if (key === undefined) return undefined;
|
|
15
|
+
const prices = MODEL_USAGE_PRICES[key];
|
|
16
|
+
if (prices === undefined) return undefined;
|
|
17
|
+
let total = 0;
|
|
18
|
+
let any = false;
|
|
19
|
+
for (const seg of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']) {
|
|
20
|
+
const unit = prices[seg];
|
|
21
|
+
const amount = usage?.[seg];
|
|
22
|
+
if (typeof unit === 'number' && Number.isFinite(unit) && typeof amount === 'number' && Number.isFinite(amount) && amount >= 0) {
|
|
23
|
+
total += (unit * amount) / 1000;
|
|
24
|
+
any = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return any ? Number(total.toFixed(6)) : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 单条派发的估算成本(元)。优先五段计价,其次 A 级单次均价。
|
|
31
|
+
export function dispatchCost({ model, usage } = {}) {
|
|
32
|
+
const key = priceKeyFor(model);
|
|
33
|
+
if (key === undefined) return undefined;
|
|
34
|
+
const byUsage = costFromUsage(model, usage);
|
|
35
|
+
if (byUsage !== undefined) return byUsage;
|
|
36
|
+
const avg = MODEL_AVG_COST[key];
|
|
37
|
+
return typeof avg === 'number' ? avg : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 反事实对照结果。record 为 dispatch.jsonl 行;显式指定 model 才有「继承父 vs
|
|
41
|
+
// 指定 model」价差,未指定(继承父)saving 恒 0 并打 inherit 标。
|
|
42
|
+
export function counterfactualFor(record) {
|
|
43
|
+
if (record === null || typeof record !== 'object') return undefined;
|
|
44
|
+
const requestedModel = record.requested?.model;
|
|
45
|
+
const usage = record.outcome?.usage;
|
|
46
|
+
const parentModel = record.parent_model;
|
|
47
|
+
if (typeof requestedModel !== 'string' || requestedModel === '') {
|
|
48
|
+
const parentCost = dispatchCost({ model: parentModel, usage });
|
|
49
|
+
return {
|
|
50
|
+
inherit: true,
|
|
51
|
+
model: parentModel ?? null,
|
|
52
|
+
actual: parentCost,
|
|
53
|
+
counterfactual: parentCost,
|
|
54
|
+
saving: 0,
|
|
55
|
+
ratio: null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const actual = dispatchCost({ model: requestedModel, usage });
|
|
59
|
+
const counterfactual = dispatchCost({ model: parentModel, usage });
|
|
60
|
+
if (actual === undefined || counterfactual === undefined) return undefined;
|
|
61
|
+
const saving = Number((counterfactual - actual).toFixed(6));
|
|
62
|
+
return {
|
|
63
|
+
inherit: false,
|
|
64
|
+
model: requestedModel,
|
|
65
|
+
parentModel: parentModel ?? null,
|
|
66
|
+
actual,
|
|
67
|
+
counterfactual,
|
|
68
|
+
saving,
|
|
69
|
+
ratio: actual > 0 ? Number((counterfactual / actual).toFixed(2)) : null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 聚合一组 record 的估算成本与继承占比。只统计可计价(actual 非 undefined)记录。
|
|
74
|
+
export function summarizeCosts(records) {
|
|
75
|
+
let actualSum = 0;
|
|
76
|
+
let counterfactualSum = 0;
|
|
77
|
+
let savingSum = 0;
|
|
78
|
+
let priced = 0;
|
|
79
|
+
let inheritCount = 0;
|
|
80
|
+
for (const record of records) {
|
|
81
|
+
if (record === null || typeof record !== 'object') continue;
|
|
82
|
+
const c = counterfactualFor(record);
|
|
83
|
+
if (c === undefined) continue;
|
|
84
|
+
if (c.actual === undefined) continue;
|
|
85
|
+
actualSum += c.actual;
|
|
86
|
+
if (c.counterfactual !== undefined) counterfactualSum += c.counterfactual;
|
|
87
|
+
savingSum += c.saving;
|
|
88
|
+
priced += 1;
|
|
89
|
+
if (c.inherit) inheritCount += 1;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
estimated_cost: Number(actualSum.toFixed(4)),
|
|
93
|
+
estimated_inherit_cost: Number(counterfactualSum.toFixed(4)),
|
|
94
|
+
estimated_saving: Number(savingSum.toFixed(4)),
|
|
95
|
+
priced,
|
|
96
|
+
inherit_count: inheritCount,
|
|
97
|
+
inherit_ratio: priced > 0 ? Number((inheritCount / priced).toFixed(3)) : 0,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 从 summaries.json 的 l1/l2 聚合成本汇总:全局(所有 L1 组求和)与按模型轴
|
|
102
|
+
// (l2 的 :model: 键,同一模型多组归并)。同一记录只属一个 L1 组与一个 model 轴键,
|
|
103
|
+
// 故全局与模型轴各自无重复。无数据返回空汇总(调用方省略显示)。
|
|
104
|
+
export function costSummaryFromSummaries(summaries) {
|
|
105
|
+
const root = summaries !== null && typeof summaries === 'object' ? summaries : {};
|
|
106
|
+
const l1 = root.l1 !== null && typeof root.l1 === 'object' ? root.l1 : {};
|
|
107
|
+
const l2 = root.l2 !== null && typeof root.l2 === 'object' ? root.l2 : {};
|
|
108
|
+
const global = { estimated_cost: 0, estimated_inherit_cost: 0, estimated_saving: 0, priced: 0, inherit_count: 0 };
|
|
109
|
+
for (const group of Object.values(l1)) {
|
|
110
|
+
const c = group !== null && typeof group === 'object' ? group.cost : null;
|
|
111
|
+
if (c === null || typeof c !== 'object') continue;
|
|
112
|
+
if (typeof c.estimated_cost === 'number') global.estimated_cost += c.estimated_cost;
|
|
113
|
+
if (typeof c.estimated_inherit_cost === 'number') global.estimated_inherit_cost += c.estimated_inherit_cost;
|
|
114
|
+
if (typeof c.estimated_saving === 'number') global.estimated_saving += c.estimated_saving;
|
|
115
|
+
if (typeof c.priced === 'number') global.priced += c.priced;
|
|
116
|
+
if (typeof c.inherit_count === 'number') global.inherit_count += c.inherit_count;
|
|
117
|
+
}
|
|
118
|
+
global.estimated_cost = Number(global.estimated_cost.toFixed(4));
|
|
119
|
+
global.estimated_inherit_cost = Number(global.estimated_inherit_cost.toFixed(4));
|
|
120
|
+
global.estimated_saving = Number(global.estimated_saving.toFixed(4));
|
|
121
|
+
global.inherit_ratio = global.priced > 0 ? Number((global.inherit_count / global.priced).toFixed(3)) : 0;
|
|
122
|
+
const byModel = new Map();
|
|
123
|
+
for (const [key, group] of Object.entries(l2)) {
|
|
124
|
+
if (group === null || typeof group !== 'object') continue;
|
|
125
|
+
const match = /:model:([^|]+)$/.exec(key);
|
|
126
|
+
if (match === null) continue;
|
|
127
|
+
const model = match[1];
|
|
128
|
+
const cur = byModel.get(model) ?? { model, estimated_cost: 0, priced: 0, deployments: 0 };
|
|
129
|
+
const c = group.cost;
|
|
130
|
+
if (c !== null && typeof c === 'object') {
|
|
131
|
+
if (typeof c.estimated_cost === 'number') cur.estimated_cost += c.estimated_cost;
|
|
132
|
+
if (typeof c.priced === 'number') cur.priced += c.priced;
|
|
133
|
+
}
|
|
134
|
+
if (typeof group.deployments_total === 'number') cur.deployments += group.deployments_total;
|
|
135
|
+
byModel.set(model, cur);
|
|
136
|
+
}
|
|
137
|
+
const modelRows = [...byModel.values()].map((m) => ({
|
|
138
|
+
model: m.model,
|
|
139
|
+
deployments: m.deployments,
|
|
140
|
+
estimated_cost: Number(m.estimated_cost.toFixed(4)),
|
|
141
|
+
priced: m.priced,
|
|
142
|
+
avg_cost: m.priced > 0 ? Number((m.estimated_cost / m.priced).toFixed(4)) : null,
|
|
143
|
+
})).sort((a, b) => (a.model < b.model ? -1 : a.model > b.model ? 1 : 0));
|
|
144
|
+
return { global, byModel: modelRows };
|
|
145
|
+
}
|
package/lib/core/cost-guard.mjs
CHANGED
|
@@ -130,7 +130,7 @@ export async function assertCostGuard(parent, profile, allowFailOpen, logger, ca
|
|
|
130
130
|
|
|
131
131
|
// llm 存在:目录读取走共享 catalog 快照(缓存只提速目录解析,校验逻辑不变)。
|
|
132
132
|
const snapshot = await catalog.getSnapshot(llm);
|
|
133
|
-
//
|
|
133
|
+
// 快照形状守卫——畸形快照(llm 缺失 / providers 非数组)不得被静默放行,
|
|
134
134
|
// 归一为「目录为空」走同一 fail-loud / fail-open 门。
|
|
135
135
|
const dir = (snapshot !== null && typeof snapshot === 'object' && snapshot.llm !== null && typeof snapshot.llm === 'object') ? snapshot.llm : {};
|
|
136
136
|
const providers = Array.isArray(dir.providers) ? dir.providers : [];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { DISPATCH_PARAMETER_KEYS } from './dispatch-schema.mjs';
|
|
3
|
+
export { profileSnapshotOf } from './profile-directory.mjs';
|
|
2
4
|
|
|
3
5
|
// lib/core/decision-trace.mjs — dispatch 决策轨迹(decisionTrace)的纯组装与
|
|
4
6
|
// 失败台账。纯记录层:只「记录」不参与任何派发行为;轨迹经
|
|
5
|
-
// output.presentationMeta 投影进会话块 meta
|
|
7
|
+
// output.presentationMeta 投影进会话块 meta 供客户端台账展示,绝不进模型可见面
|
|
6
8
|
// (render 行 / 结果文本)。
|
|
7
9
|
//
|
|
8
10
|
// 边界(后来开发者须知):
|
|
@@ -25,11 +27,6 @@ const MAX_TEXT_CHARS = 200;
|
|
|
25
27
|
const MAX_CHECKS = 6;
|
|
26
28
|
const MAX_CALLS = 4;
|
|
27
29
|
|
|
28
|
-
// 决策输入快照阈值:profiles_snapshot 条目数与 description 截断长度(体积受
|
|
29
|
-
// assertTraceSize 兜底)。
|
|
30
|
-
const MAX_PROFILE_SNAPSHOT_ENTRIES = 6;
|
|
31
|
-
const PROFILE_DESC_CHARS = 120;
|
|
32
|
-
|
|
33
30
|
// 名单类数组截断:长度超过 MAX_LIST_ITEMS 且元素全为字符串的数组(工具/预设名
|
|
34
31
|
// 单)替换为 { values: 前 8 项, truncated: true }。数值类数组(如 usage)元素非
|
|
35
32
|
// 字符串,不截断,避免误伤。
|
|
@@ -90,13 +87,18 @@ export function parentContextOf({ parentPreset, parentProvider, parentModel, par
|
|
|
90
87
|
|
|
91
88
|
// 请求参数快照。requested.prompt 原文绝不进 trace(体积 + 隐私),只记
|
|
92
89
|
// persona / toolFilter 的存在性布尔与 prompt 摘要(前 200 字符 + 截断标记,
|
|
93
|
-
//
|
|
90
|
+
// 供台账展示「这次派发让子 Agent 干什么」,不泄漏全文)。
|
|
94
91
|
// opts.profiles:模型可见的 profile 目录快照(调用方按 dispatch:profiles section
|
|
95
92
|
// 同源同序传入,见 profileSnapshotOf)——「模型为什么选这个方案」的决策输入记录面。
|
|
96
93
|
export function requestedOf(args, opts = {}) {
|
|
97
94
|
const rawPrompt = typeof args.prompt === 'string' ? args.prompt : '';
|
|
98
95
|
const truncated = rawPrompt.length > 200;
|
|
99
96
|
const profiles = Array.isArray(opts.profiles) ? opts.profiles : [];
|
|
97
|
+
// 未知 per-call 字段不再静默吞——与 DISPATCH_PARAMETER_KEYS(单一事实来源)
|
|
98
|
+
// 对照后记入 requested 快照,台账「请求值」区可见(fail-visible,不改派发行为)。
|
|
99
|
+
const unknownKeys = args !== null && typeof args === 'object' && !Array.isArray(args)
|
|
100
|
+
? Object.keys(args).filter((key) => args[key] !== undefined && !DISPATCH_PARAMETER_KEYS.includes(key))
|
|
101
|
+
: [];
|
|
100
102
|
return {
|
|
101
103
|
profile: args.profile,
|
|
102
104
|
preset: args.preset,
|
|
@@ -109,33 +111,11 @@ export function requestedOf(args, opts = {}) {
|
|
|
109
111
|
maxTokens: args.maxTokens,
|
|
110
112
|
maxDepth: args.maxDepth,
|
|
111
113
|
envelope: args.envelope === true,
|
|
114
|
+
advice_present: opts.advicePresent === true,
|
|
112
115
|
prompt_excerpt: rawPrompt.slice(0, 200),
|
|
113
116
|
...(truncated ? { prompt_truncated: true } : {}),
|
|
114
117
|
...(profiles.length > 0 ? { profiles_snapshot: profiles } : {}),
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 模型可见 profile 目录快照(纯函数):enabled 且按调用方顺序(应传 cheap-first
|
|
119
|
-
// 排序,与 dispatch:profiles section 同源同序)。description 截断、条目数截到
|
|
120
|
-
// maxEntries 并打 truncated 标记。统一返回 { entries, truncated, total }。
|
|
121
|
-
export function profileSnapshotOf(profiles, { maxEntries = MAX_PROFILE_SNAPSHOT_ENTRIES, maxDesc = PROFILE_DESC_CHARS } = {}) {
|
|
122
|
-
const list = Array.isArray(profiles) ? profiles : [];
|
|
123
|
-
const enabled = [];
|
|
124
|
-
for (const p of list) {
|
|
125
|
-
if (p === null || typeof p !== 'object' || typeof p.id !== 'string' || p.id === '') continue;
|
|
126
|
-
if (p.enabled === false) continue;
|
|
127
|
-
const description = typeof p.description === 'string' ? p.description : '';
|
|
128
|
-
enabled.push({
|
|
129
|
-
id: p.id,
|
|
130
|
-
...(description !== '' ? { description: description.length > maxDesc ? `${description.slice(0, maxDesc)}…` : description } : {}),
|
|
131
|
-
...(typeof p.preset === 'string' && p.preset !== '' ? { preset: p.preset } : {}),
|
|
132
|
-
...(typeof p.tokenTier === 'string' && p.tokenTier !== '' ? { tokenTier: p.tokenTier } : {}),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
entries: enabled.slice(0, maxEntries),
|
|
137
|
-
truncated: enabled.length > maxEntries,
|
|
138
|
-
total: enabled.length,
|
|
118
|
+
...(unknownKeys.length > 0 ? { unknown_keys: unknownKeys } : {}),
|
|
139
119
|
};
|
|
140
120
|
}
|
|
141
121
|
|
package/lib/core/delegation.mjs
CHANGED
|
@@ -55,7 +55,7 @@ export function collectChildUsage(session) {
|
|
|
55
55
|
// 产出单步五段 usage(inputTokens/outputTokens 恒在、缺报按 0;可选字段缓存读/写/
|
|
56
56
|
// 推理仅该条报告时携带)。返回前 maxCalls 条 + truncated(总数 > maxCalls)+
|
|
57
57
|
// totalCalls(总数);无任何 usage 事件返回 undefined。与 collectChildUsage 并存:
|
|
58
|
-
//
|
|
58
|
+
// 前者是逐次明细(台账展开区),后者是会话总量(结算 meta),互不替代。非 object /
|
|
59
59
|
// 非 number 一律忽略(fail-soft),使逐条收集绝不阻断派发结算。
|
|
60
60
|
//
|
|
61
61
|
// maxCalls 默认 6(非 12):单条 calls 的五段 JSON 键名主导体积(约 100 字节/条),
|
|
@@ -112,7 +112,8 @@ async function settleRun(run, meta, prune, measureChild, t0, logger) {
|
|
|
112
112
|
detail: withPartialText(failure, result.output),
|
|
113
113
|
...meta,
|
|
114
114
|
elapsedMs: Date.now() - t0,
|
|
115
|
-
stopReason: result.stopReason
|
|
115
|
+
stopReason: result.stopReason,
|
|
116
|
+
childSessionId: run.id
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
119
|
// 仅 completed 结算时测量(非 completed 走上方失败分支,不测);在 dispose
|
|
@@ -137,7 +138,7 @@ async function settleRun(run, meta, prune, measureChild, t0, logger) {
|
|
|
137
138
|
if (logger !== undefined && typeof logger.info === 'function') {
|
|
138
139
|
logger.info('[dsh-subagent-profile] trusted-output: ' + trustAudit(metaOut));
|
|
139
140
|
}
|
|
140
|
-
return { status: 'completed', output, ...metaOut };
|
|
141
|
+
return { status: 'completed', output, childSessionId: run.id, ...metaOut };
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
export async function settleStart(start, signal, meta, prune = (blocks) => blocks, measureChild = () => undefined, t0 = Date.now(), logger = undefined, onSettled = undefined) {
|
|
@@ -148,9 +149,11 @@ export async function settleStart(start, signal, meta, prune = (blocks) => block
|
|
|
148
149
|
settled = await settleRun(run, meta, prune, measureChild, t0, logger);
|
|
149
150
|
return settled;
|
|
150
151
|
} catch (error) {
|
|
152
|
+
// run 可能已创建(start resolve 后 result reject),此时仍可回填子会话 id;
|
|
153
|
+
// start 自身 reject 时 run 为 undefined,childSessionId 缺省(调用方降级处理)。
|
|
151
154
|
settled = signal.aborted
|
|
152
|
-
? { status: 'killed', ...meta, elapsedMs: Date.now() - t0, stopReason: 'aborted' }
|
|
153
|
-
: { status: 'failed', detail: String(error), ...meta, elapsedMs: Date.now() - t0, stopReason: 'error' };
|
|
155
|
+
? { status: 'killed', ...meta, elapsedMs: Date.now() - t0, stopReason: 'aborted', ...(run !== undefined && typeof run.id === 'string' ? { childSessionId: run.id } : {}) }
|
|
156
|
+
: { status: 'failed', detail: String(error), ...meta, elapsedMs: Date.now() - t0, stopReason: 'error', ...(run !== undefined && typeof run.id === 'string' ? { childSessionId: run.id } : {}) };
|
|
154
157
|
return settled;
|
|
155
158
|
} finally {
|
|
156
159
|
// 无论结果如何结算都要释放子句柄——run.result reject 不得泄漏子 agent
|
|
@@ -43,7 +43,7 @@ export async function applyCostGuardGate(trace, merged, parent, deps) {
|
|
|
43
43
|
const allowFailOpen = deps.store.getAllowFailOpen();
|
|
44
44
|
const input = { provider: merged.provider, model: merged.model, reasoningEffort: merged.reasoningEffort, maxTokens: merged.maxTokens, maxDepth: merged.maxDepth, allowFailOpen };
|
|
45
45
|
// 决策输入快照补充:请求 model 的 reasoning-effort 支持档位(catalog 快照,
|
|
46
|
-
// 有缓存;拿不到 fail-soft
|
|
46
|
+
// 有缓存;拿不到 fail-soft 省略)——台账可见「该模型当时可选哪些档位」。
|
|
47
47
|
if (typeof merged.model === 'string' && merged.model !== '') {
|
|
48
48
|
try {
|
|
49
49
|
const snapshot = await deps.catalog.getSnapshot(parent.ctx.get('llm'));
|
|
@@ -84,7 +84,13 @@ export function applyIntersectionGate(trace, mode, intersectionInput, merged, pa
|
|
|
84
84
|
}
|
|
85
85
|
try {
|
|
86
86
|
const effectiveAllow = computeContinuableAllow(parentToolNames, merged.toolFilter);
|
|
87
|
-
|
|
87
|
+
// 工具集子节数据(continuable 可计算闭集与移除清单)。名单类数组超 8 项由
|
|
88
|
+
// recordGate 的 truncateLists 自动截断为 { values, truncated }(台账展示时认标记)。
|
|
89
|
+
const removedRaw = parentToolNames
|
|
90
|
+
.filter((name) => !effectiveAllow.includes(name))
|
|
91
|
+
.map((name) => ({ name, reason: name === 'run_code' ? '安全策略:子代理不执行代码' : '不在白名单' }));
|
|
92
|
+
const removedTools = removedRaw.length > 8 ? { values: removedRaw.slice(0, 8), truncated: true } : removedRaw;
|
|
93
|
+
recordGate(trace, { name: 'intersection', input: intersectionInput, output: { effectiveAllowCount: effectiveAllow.length, effectiveAllowNames: effectiveAllow, removedTools }, verdict: 'pass' });
|
|
88
94
|
return effectiveAllow;
|
|
89
95
|
} catch (error) {
|
|
90
96
|
recordGate(trace, { name: 'intersection', input: intersectionInput, output: {}, verdict: 'fail', reason: error.message });
|
|
@@ -100,6 +106,7 @@ export function applyIntersectionGate(trace, mode, intersectionInput, merged, pa
|
|
|
100
106
|
export function applyBudgetGate(deps, parent, args, trace) {
|
|
101
107
|
if (args.continuable === true) return { parentSessionId: undefined, finish: () => {}, release: () => {} };
|
|
102
108
|
const parentSessionId = parent.session?.header?.id;
|
|
109
|
+
const budget = deps.guard.snapshot(parentSessionId);
|
|
103
110
|
const acquired = deps.guard.acquire(parentSessionId);
|
|
104
111
|
if (!acquired.ok) {
|
|
105
112
|
const message = acquired.reason === 'concurrency'
|
|
@@ -107,7 +114,7 @@ export function applyBudgetGate(deps, parent, args, trace) {
|
|
|
107
114
|
: `dispatch: 本会话累计派发 token 已达上限 ${acquired.limit},请等待在途任务完成或开启新会话`;
|
|
108
115
|
recordGate(trace, {
|
|
109
116
|
name: 'budget',
|
|
110
|
-
input: { parentSessionId, reason: acquired.reason, limit: acquired.limit },
|
|
117
|
+
input: { parentSessionId, current: budget, reason: acquired.reason, limit: acquired.limit },
|
|
111
118
|
output: { allowed: false },
|
|
112
119
|
verdict: 'fail',
|
|
113
120
|
reason: message,
|
|
@@ -115,11 +122,11 @@ export function applyBudgetGate(deps, parent, args, trace) {
|
|
|
115
122
|
deps.logger.warn(`[dsh-subagent-profile] ${message}`);
|
|
116
123
|
throw new Error(message);
|
|
117
124
|
}
|
|
118
|
-
// 通过也记闸(与 whitelist/cost/intersection 同口径:pass/fail
|
|
125
|
+
// 通过也记闸(与 whitelist/cost/intersection 同口径:pass/fail 均记,台账时间线可
|
|
119
126
|
// 见预算检查项;无父 sessionId 时 acquire 跳过守卫,同样记 pass 并注明 skipped)。
|
|
120
127
|
recordGate(trace, {
|
|
121
128
|
name: 'budget',
|
|
122
|
-
input: { parentSessionId },
|
|
129
|
+
input: { parentSessionId, current: budget },
|
|
123
130
|
output: { allowed: true, skipped: acquired.skipped === true },
|
|
124
131
|
verdict: 'pass',
|
|
125
132
|
});
|
|
@@ -117,6 +117,12 @@ export function createDispatchGuard({ maxConcurrent = 8, maxParentTokens = 20000
|
|
|
117
117
|
concurrency.delete(parentSessionId);
|
|
118
118
|
tokenTotals.delete(parentSessionId);
|
|
119
119
|
},
|
|
120
|
+
snapshot: (parentSessionId) => ({
|
|
121
|
+
concurrency: typeof parentSessionId === 'string' && parentSessionId !== '' ? (concurrency.get(parentSessionId) ?? 0) : 0,
|
|
122
|
+
tokens: typeof parentSessionId === 'string' && parentSessionId !== '' ? (tokenTotals.get(parentSessionId) ?? 0) : 0,
|
|
123
|
+
maxConcurrent,
|
|
124
|
+
maxParentTokens,
|
|
125
|
+
}),
|
|
120
126
|
reset: () => {
|
|
121
127
|
concurrency.clear();
|
|
122
128
|
tokenTotals.clear();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// assertResultSchemaConsistency 在 apply 时锁定——任一分支漏补共享字段即 throw。
|
|
8
8
|
// decisionTrace 是共享键:三分支都必须携带,但只锁「存在性」(深层结构由
|
|
9
9
|
// lib/core/decision-trace.mjs 的纯函数与单测保证),它经 output.presentationMeta
|
|
10
|
-
// 投影进会话块 meta
|
|
10
|
+
// 投影进会话块 meta 供客户端台账展示,不进模型可见面。
|
|
11
11
|
|
|
12
12
|
// 输入参数 schema(从 dispatch-tool.mjs 逐字移入;defineTool 只读不改)。
|
|
13
13
|
export const DISPATCH_PARAMETERS = {
|
|
@@ -19,7 +19,7 @@ export const DISPATCH_PARAMETERS = {
|
|
|
19
19
|
// 防护 maxDepth→MAX_DEPTH 硬上限。
|
|
20
20
|
// 模式字段(run_in_background/continuable/envelope)是派发方式选择,不入配置覆盖。
|
|
21
21
|
// 新增字段时须按上列轴归类,并同步键集测试(percall-spec)的 WHITELIST_KEYS。
|
|
22
|
-
// per-call
|
|
22
|
+
// per-call 规范化清单闭合:宿主 dsh-tools 的 DSL 参数解析**默认拒绝
|
|
23
23
|
// 未知键**(2026-08 实测:参数级 additionalProperties 只接受 value schema 对象、
|
|
24
24
|
// 不接受 boolean,故无法显式声明;键集闭合由 percall-spec 测试「键集恰为 14 键」
|
|
25
25
|
// 锁定,mergeProfileArgs 只透传白名单 9 键,未知字段到不了子请求)。
|
|
@@ -53,6 +53,11 @@ export const DISPATCH_PARAMETERS = {
|
|
|
53
53
|
prompt: { type: 'string', required: true, description: '任务文本·该 subagent 完成的自包含任务(它看不到当前对话)。' }
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
// 已知 per-call 参数键集(单一事实来源):decisionTrace 的 requested 快照据此把
|
|
57
|
+
// 未知字段记录为 unknown_keys(fail-visible,不静默吞)。宿主 dsh-tools DSL 对
|
|
58
|
+
// 参数默认拒绝未知键,此处是插件侧决策轨迹的可观测兜底。
|
|
59
|
+
export const DISPATCH_PARAMETER_KEYS = Object.freeze(Object.keys(DISPATCH_PARAMETERS));
|
|
60
|
+
|
|
56
61
|
export const DISPATCH_OUTPUT_SCHEMA = {
|
|
57
62
|
// 每个结果上的可观测元数据。oneOf 覆盖 background 变体(kind/jobId)与
|
|
58
63
|
// foreground 变体(output),两者闭合且都携带生效的委派值。`ignored` 必须
|
|
@@ -89,7 +94,7 @@ export const DISPATCH_OUTPUT_SCHEMA = {
|
|
|
89
94
|
elapsedMs: { type: 'number', description: '派发耗时(毫秒,execute 入口到结算时刻);后台结算经 job 结果携带,dispatch 结果不带。' },
|
|
90
95
|
stopReason: { type: 'string', enum: ['completed', 'max-tokens', 'aborted', 'refusal', 'error'], description: '子 Agent 结束原因;后台结算经 job 结果携带,dispatch 结果不带。' },
|
|
91
96
|
ignored: { type: 'array', items: { type: 'string' } },
|
|
92
|
-
decisionTrace: { type: 'object', additionalProperties: true, description: '
|
|
97
|
+
decisionTrace: { type: 'object', additionalProperties: true, description: '派发决策轨迹(供客户端台账展示,不进模型可见面)' }
|
|
93
98
|
}
|
|
94
99
|
},
|
|
95
100
|
{
|
|
@@ -120,7 +125,7 @@ export const DISPATCH_OUTPUT_SCHEMA = {
|
|
|
120
125
|
elapsedMs: { type: 'number', description: '派发耗时(毫秒,execute 入口到结算时刻);continuable 不结算,实际省略。' },
|
|
121
126
|
stopReason: { type: 'string', enum: ['completed', 'max-tokens', 'aborted', 'refusal', 'error'], description: '子 Agent 结束原因;continuable 不结算,实际省略。' },
|
|
122
127
|
ignored: { type: 'array', items: { type: 'string' } },
|
|
123
|
-
decisionTrace: { type: 'object', additionalProperties: true, description: '
|
|
128
|
+
decisionTrace: { type: 'object', additionalProperties: true, description: '派发决策轨迹(供客户端台账展示,不进模型可见面)' }
|
|
124
129
|
}
|
|
125
130
|
},
|
|
126
131
|
{
|
|
@@ -150,7 +155,7 @@ export const DISPATCH_OUTPUT_SCHEMA = {
|
|
|
150
155
|
elapsedMs: { type: 'number', description: '派发耗时(毫秒,execute 入口到结算时刻);前台 completed 结算时携带。' },
|
|
151
156
|
stopReason: { type: 'string', enum: ['completed', 'max-tokens', 'aborted', 'refusal', 'error'], description: '子 Agent 结束原因;前台 completed 结算时携带。' },
|
|
152
157
|
ignored: { type: 'array', items: { type: 'string' } },
|
|
153
|
-
decisionTrace: { type: 'object', additionalProperties: true, description: '
|
|
158
|
+
decisionTrace: { type: 'object', additionalProperties: true, description: '派发决策轨迹(供客户端台账展示,不进模型可见面)' }
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
]
|