dsh-subagent-profile 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -29
- package/README.zh.md +38 -29
- package/index.mjs +188 -62
- package/lib/client.js +1240 -47
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +433 -0
- package/lib/core/delegation.mjs +106 -48
- package/lib/core/dispatch-gates.mjs +146 -0
- package/lib/core/dispatch-guard.mjs +150 -0
- package/lib/core/dispatch-schema.mjs +98 -14
- package/lib/core/dispatch-tool.mjs +208 -199
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-ledger.mjs +289 -0
- package/lib/core/evolution-summary.mjs +432 -0
- package/lib/core/http-routes.mjs +155 -40
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +256 -136
- package/lib/core/profile-provider.mjs +41 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/shims.mjs +56 -75
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +2 -3
- package/presets/orchestrator/agent.cordis.yml +243 -271
- package/presets/orchestrator/NOTICE +0 -3
|
@@ -2,106 +2,41 @@
|
|
|
2
2
|
// 结果 schema 一致性锁与 syncTool 注册/注销逻辑,从 index.mjs 逐字拆出。
|
|
3
3
|
// 仅引用 lib + shims;无 @deepseek-ai 依赖(shims 是唯一入口)。
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
// register ctx.tools.register
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
5
|
+
// 注入:所有 apply 闭包 / ctx 依赖都是显式参数——
|
|
6
|
+
// register ctx.tools.register(工具由 syncTool 注册/注销,设置开关可运行时移除),
|
|
7
|
+
// store profile store(resolveProfile 取基方案、getAllowFailOpen 供 cost guard),
|
|
8
|
+
// getEnabled 读取 apply 闭包 `enabled` 标志(continuable fail-loud 门 +
|
|
9
|
+
// syncTool 注册条件),
|
|
10
|
+
// getService 请求时服务 getter(ctx.get('toolResultPruner') / ctx.get('jobs')
|
|
11
|
+
// 按次读取,绝不在 apply 时读),
|
|
12
|
+
// logger ctx.logger(决策级派发日志 + continuable 告警),
|
|
13
|
+
// subagents ctx.subagents(start / startContinuable 驱动子 Agent),
|
|
14
|
+
// guard dispatch guard(execute 入口 acquire / 结算 release+记账 /
|
|
15
|
+
// syncTool 禁用时 cancelAll+reset)。
|
|
16
|
+
// 工厂返回 { syncTool, dispose }:syncTool 交给 HTTP 路由(/set-enabled),
|
|
17
|
+
// dispose 在插件卸载时运行。
|
|
18
18
|
//
|
|
19
|
-
// execute 按预检段 +
|
|
20
|
-
//
|
|
21
|
-
// output 结果 schema 已拆至 lib/core/dispatch-schema.mjs(守文件行门)——
|
|
22
|
-
// 工厂保持装配态;除各任务明示的新增行为外,执行路径零漂移。
|
|
19
|
+
// execute 按预检段 + 三分支拆为模块级私有函数;parameters / output schema 拆至
|
|
20
|
+
// dispatch-schema.mjs,预检闸应用拆至 dispatch-gates.mjs(守文件行门),零漂移。
|
|
23
21
|
|
|
24
22
|
import { defineTool } from './shims.mjs';
|
|
25
|
-
import {
|
|
23
|
+
import { textFrom, stopReasonError, withPartialText, pruneBlocks, assertResultSchemaConsistency, trustLabel, trustAudit, dispatchLabel, tierSortKey } from './pure.mjs';
|
|
26
24
|
import { resolveWhitelist } from './whitelist.mjs';
|
|
27
|
-
import {
|
|
28
|
-
import { settleStart, collectChildUsage } from './delegation.mjs';
|
|
29
|
-
import { DISPATCH_OUTPUT_SCHEMA } from './dispatch-schema.mjs';
|
|
25
|
+
import { recordEscapeAllow } from './escape.mjs';
|
|
26
|
+
import { settleStart, collectChildUsage, collectChildCalls } from './delegation.mjs';
|
|
27
|
+
import { DISPATCH_OUTPUT_SCHEMA, DISPATCH_PARAMETERS, DISPATCH_RENDER } from './dispatch-schema.mjs';
|
|
28
|
+
import { createDecisionTrace, finalizeTrace, assertTraceSize, parentContextOf, requestedOf, effectiveMeta, recordApprovalGate, recordFailure, profileSnapshotOf } from './decision-trace.mjs';
|
|
29
|
+
import { applyWhitelistGate, applyCostGuardGate, applyIntersectionGate, assertContinuableEnabled, readParentToolNames, applyBudgetGate } from './dispatch-gates.mjs';
|
|
30
|
+
import { trackForegroundInflight, trackBackgroundInflight, trackContinuableInflight } from './dispatch-guard.mjs';
|
|
31
|
+
import { finishForegroundLedger, finishBackgroundLedger, finishContinuableLedger } from './evolution-ledger.mjs';
|
|
30
32
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const DISPATCH_PARAMETERS = {
|
|
34
|
-
profile: { type: 'string', description: 'Optional profile id from the profile registry (built-ins: swap-standard, researcher, plus any you define in the settings page); omit to inherit the parent preset and tools as-is.' },
|
|
35
|
-
preset: { type: 'string', description: 'Explicit target preset override; must be a system-trust preset of this runtime.' },
|
|
36
|
-
model: { type: 'string', description: 'Explicit model override for the child.' },
|
|
37
|
-
provider: { type: 'string', description: 'Explicit provider override for the child.' },
|
|
38
|
-
reasoningEffort: { type: 'string', description: 'Explicit reasoning-effort override injected into every child request.' },
|
|
39
|
-
tokenTier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '成本/深度分层(估算口径,非计费):cheap 省 token、balanced 均衡、premium 高成本。覆盖 profile 的 tokenTier;缺省 balanced。' },
|
|
40
|
-
persona: { type: 'string', description: 'Persona text shadowing the child deployment:persona section.' },
|
|
41
|
-
toolFilter: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
// DSL 对象参数默认拒绝未知键,toolFilter 必须闭合其 schema,
|
|
44
|
-
// 否则 defineTool 在 apply 时 throw、插件加载失败。
|
|
45
|
-
additionalProperties: false,
|
|
46
|
-
description: 'Extra tool whitelist intersection for the child (intersected with the parent tool set).',
|
|
47
|
-
properties: {
|
|
48
|
-
allow: { type: 'array', items: { type: 'string' }, description: 'When present, only these tool names are kept.' },
|
|
49
|
-
deny: { type: 'array', items: { type: 'string' }, description: 'These tool names are always removed.' }
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
maxTokens: { type: 'number', description: 'Explicit max-tokens budget for the child.' },
|
|
53
|
-
maxDepth: { type: 'number', description: 'Absolute delegation-depth cap for this child.' },
|
|
54
|
-
run_in_background: { type: 'boolean', description: '异步 one-shot:走 jobs.start 包 start(),返回 jobId;仍单轮即弃,非 continuable' },
|
|
55
|
-
continuable: { type: 'boolean', description: 'Start a durable continuable subagent instead of a one-shot: returns a subagentId immediately and keeps the child conversation available for later turns via the send_message tool. Defaults to false.' },
|
|
56
|
-
// 信封模式 = opt-in(仅「中段即交付物」的任务用):true 时把信封骨架追加进
|
|
57
|
-
// 子 persona(systemPrompt 影子段,模型可见),子 Agent 按结构化信封汇报;
|
|
58
|
-
// 默认 false 走结果剪枝回收。
|
|
59
|
-
envelope: { type: 'boolean', description: 'true 时子 Agent 按结构化信封汇报(简短结论 + 结构分项 + 关键发现落点),适合中段即交付物的长任务;默认 false 走剪枝回收' },
|
|
60
|
-
prompt: { type: 'string', required: true, description: 'The complete, self-contained task for the child (it does not see this conversation).' }
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// 信封骨架:envelope:true 时追加进子 persona 的结构化汇报契约。三分支共享此
|
|
64
|
-
// 常量;骨架只进 persona(systemPrompt 影子段、模型可见),不进 descriptor
|
|
65
|
-
// (model-hidden、仅 session 记录),故 execute 在三分支创建子 Agent 前统一
|
|
66
|
-
// 追加到 merged.persona。
|
|
33
|
+
// 信封骨架:envelope:true 时追加进子 persona 的结构化汇报契约(三分支共享);骨架
|
|
34
|
+
// 只进 persona(systemPrompt 影子段、模型可见),不进 descriptor(model-hidden,仅 session 记录)。
|
|
67
35
|
const ENVELOPE_SKELETON = '完成前按以下骨架输出:## 结论(1-2 句)\n## 结构分项(逐项)\n## 关键发现落点(文件路径/数据位置)';
|
|
68
36
|
|
|
69
|
-
// continuable 丢弃 preset 换用与 reasoningEffort —— 渲染行把 `ignored`
|
|
70
|
-
// 列表回显出来(`reasoningEffort=<值>(ignored)`,再加 ignored 项明细),让模型
|
|
71
|
-
// 「看见」被丢弃项;background/foreground 无忽略项时该后缀为空。
|
|
72
|
-
const DISPATCH_RENDER = (_args, value) => {
|
|
73
|
-
const ignored = value.ignored !== undefined && value.ignored.length > 0
|
|
74
|
-
? `(ignored: ${value.ignored.join(', ')})`
|
|
75
|
-
: '';
|
|
76
|
-
const tier = typeof value.tokenTier === 'string' && value.tokenTier !== ''
|
|
77
|
-
? ` · tokenTier=${value.tokenTier}`
|
|
78
|
-
: '';
|
|
79
|
-
const tokens = typeof value.childTotalTokens === 'number'
|
|
80
|
-
? ` · childTotalTokens=${value.childTotalTokens}`
|
|
81
|
-
: '';
|
|
82
|
-
const elapsed = typeof value.elapsedMs === 'number'
|
|
83
|
-
? ` · elapsedMs=${value.elapsedMs}`
|
|
84
|
-
: '';
|
|
85
|
-
const stopReason = typeof value.stopReason === 'string' && value.stopReason !== ''
|
|
86
|
-
? ` · stopReason=${value.stopReason}`
|
|
87
|
-
: '';
|
|
88
|
-
// childUsage 是嵌套对象,render 行是扁平的 key=value,故 JSON 序列化为单段
|
|
89
|
-
// 供 client 侧 parseDispatchText 解析回对象(五段分解)。仅前台 completed 结算
|
|
90
|
-
// 携带;后台结算经 job 结果携带,continuable 不携带,故该段只在 foreground 行。
|
|
91
|
-
const usage = value.childUsage !== undefined && value.childUsage !== null && typeof value.childUsage === 'object'
|
|
92
|
-
? ` · childUsage=${JSON.stringify(value.childUsage)}`
|
|
93
|
-
: '';
|
|
94
|
-
const text = value.kind === 'background'
|
|
95
|
-
? `[dispatch] background job ${value.jobId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}`
|
|
96
|
-
: value.kind === 'continuable'
|
|
97
|
-
? `[dispatch] started subagent ${value.subagentId} · profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}`
|
|
98
|
-
: `[dispatch] profile=${value.profile} · preset=${value.preset} · provider=${value.provider} · model=${value.model} · reasoningEffort=${value.reasoningEffort}${ignored}${tier}${tokens}${usage}${elapsed}${stopReason}\n\n${value.output}`;
|
|
99
|
-
return [{ type: 'text', text }];
|
|
100
|
-
};
|
|
101
|
-
|
|
102
37
|
// --- execute 预检段(从 execute 拆出;行为逐字不变)-----------------------------
|
|
103
38
|
|
|
104
|
-
//
|
|
39
|
+
// 解析基方案(旁路),再叠加显式参数。
|
|
105
40
|
function mergeProfileArgs(args, store) {
|
|
106
41
|
const base = args.profile !== undefined ? store.resolveProfile(args.profile) : {};
|
|
107
42
|
const merged = { ...base };
|
|
@@ -111,21 +46,7 @@ function mergeProfileArgs(args, store) {
|
|
|
111
46
|
return merged;
|
|
112
47
|
}
|
|
113
48
|
|
|
114
|
-
//
|
|
115
|
-
// a preset equal to the parent's composed preset is rewritten to
|
|
116
|
-
// 'inherit' (no swap).
|
|
117
|
-
async function assertPresetWhitelist(parent, merged) {
|
|
118
|
-
if (typeof merged.preset !== 'string' || merged.preset === 'inherit') return;
|
|
119
|
-
const whitelist = new Set(await resolveWhitelist(parent.ctx.get('agentPresets')));
|
|
120
|
-
if (!whitelist.has(merged.preset)) {
|
|
121
|
-
throw new Error(`dispatch: preset "${merged.preset}" is not in the target-preset whitelist`);
|
|
122
|
-
}
|
|
123
|
-
const parentPresets = parent.ctx.get('agentPresets');
|
|
124
|
-
const parentComposed = parentPresets !== undefined ? parentPresets.composedPreset(parent.ctx) : undefined;
|
|
125
|
-
if (merged.preset === parentComposed) merged.preset = 'inherit';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Effective delegation values for observability.
|
|
49
|
+
// 生效的委派值(供可观测元数据)。
|
|
129
50
|
function buildMeta(args, merged, parent) {
|
|
130
51
|
return {
|
|
131
52
|
profile: args.profile ?? '(inline)',
|
|
@@ -137,10 +58,10 @@ function buildMeta(args, merged, parent) {
|
|
|
137
58
|
};
|
|
138
59
|
}
|
|
139
60
|
|
|
140
|
-
//
|
|
61
|
+
// 组装前台/后台请求(continuable 在下方自建)。
|
|
141
62
|
function buildRequest(args, merged, parent, signal) {
|
|
142
63
|
return {
|
|
143
|
-
label:
|
|
64
|
+
label: dispatchLabel(args, merged),
|
|
144
65
|
prompt: [{ type: 'text', text: args.prompt }],
|
|
145
66
|
parent,
|
|
146
67
|
signal,
|
|
@@ -151,8 +72,7 @@ function buildRequest(args, merged, parent, signal) {
|
|
|
151
72
|
};
|
|
152
73
|
}
|
|
153
74
|
|
|
154
|
-
//
|
|
155
|
-
// guard and after request assembly, before dispatch.
|
|
75
|
+
// 决策级日志:经 cost guard 与请求组装之后、派发之前的生效委派输入。
|
|
156
76
|
function logDispatchDecision(logger, args, merged, parent) {
|
|
157
77
|
logger.info('[dsh-subagent-profile] dispatch:', JSON.stringify({
|
|
158
78
|
profile: args.profile ?? '(inline)',
|
|
@@ -174,10 +94,9 @@ function logDispatchDecision(logger, args, merged, parent) {
|
|
|
174
94
|
// 假设:continuable 继承父预设(preset swap 被忽略)⇒ 子工具集 ≈ 父工具集;
|
|
175
95
|
// 失效条件:任何导致子工具集与父工具集不一致的宿主行为变化,父集都可能含
|
|
176
96
|
// 子集上不存在之工具 → tools.restrict 抛「未知工具」→ 本缓解自动降级为
|
|
177
|
-
// fail-loud(保守安全)。
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const effectiveAllow = computeContinuableAllow(parentNames, merged.toolFilter);
|
|
97
|
+
// fail-loud(保守安全)。effectiveAllow 由调用方(runDispatch 交集闸处)算好
|
|
98
|
+
// 传入——同一结果同时用于 request 组装与决策轨迹记录,不重复计算。
|
|
99
|
+
function buildContinuableRequest(args, merged, parent, effectiveAllow) {
|
|
181
100
|
const hasAgentOptions = merged.provider !== undefined || merged.model !== undefined || merged.maxTokens !== undefined;
|
|
182
101
|
return {
|
|
183
102
|
prompt: [{ type: 'text', text: args.prompt }],
|
|
@@ -195,15 +114,12 @@ function buildContinuableRequest(args, merged, parent) {
|
|
|
195
114
|
};
|
|
196
115
|
}
|
|
197
116
|
|
|
198
|
-
// Continuable
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
// `start`,不拦 `startContinuable` —— 这里显式补上。当前 syncTool 会在
|
|
205
|
-
// 禁用时注销 dispatch 工具(间接门),此处是防御性兜底:禁用后
|
|
206
|
-
// dispatch(continuable:true) 必须 fail-loud,不得静默派生子树。
|
|
117
|
+
// Continuable(持久)路径:startContinuable 发布持久子会话并返回其持久 id,
|
|
118
|
+
// 后续轮次由官方 send_message 工具驱动。优先处理——同时要 background 与
|
|
119
|
+
// continuable 的调用方得到 continuable 子会话。
|
|
120
|
+
async function runContinuable(args, merged, meta, parent, exec, deps, trace, effectiveAllow, base) {
|
|
121
|
+
// provider `start` 的 !enabled 检查只拦 `start` 不拦 `startContinuable`——此处
|
|
122
|
+
// 显式兜底(syncTool 注销工具是间接门):禁用后不得静默派生子树,必须 fail-loud。
|
|
207
123
|
if (!deps.getEnabled()) {
|
|
208
124
|
throw new Error('dispatch: 插件已禁用(设置 → 子 Agent 方案 重新启用)');
|
|
209
125
|
}
|
|
@@ -217,19 +133,21 @@ async function runContinuable(args, merged, meta, parent, exec, deps) {
|
|
|
217
133
|
if (merged.reasoningEffort !== undefined) {
|
|
218
134
|
deps.logger.warn(`[dsh-subagent-profile] continuable mode cannot set reasoningEffort; ignoring "${merged.reasoningEffort}"`);
|
|
219
135
|
}
|
|
220
|
-
const continuableRequest = buildContinuableRequest(args, merged, parent);
|
|
136
|
+
const continuableRequest = buildContinuableRequest(args, merged, parent, effectiveAllow);
|
|
221
137
|
const { childId } = await deps.subagents.startContinuable({
|
|
222
138
|
provider: 'profile',
|
|
223
|
-
label:
|
|
139
|
+
label: dispatchLabel(args, merged),
|
|
224
140
|
request: continuableRequest,
|
|
225
141
|
signal: exec.signal
|
|
226
142
|
});
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
//
|
|
143
|
+
trackContinuableInflight(deps.guard, deps.subagents, childId, parent.session?.header?.id);
|
|
144
|
+
// Continuable 丢弃方案的 preset swap 与 reasoningEffort(子继承父预设),
|
|
145
|
+
// 故可观测 meta 必须报告实际生效值,而非被忽略的请求值。
|
|
230
146
|
// 可见性修复:`reasoningEffort` 回显**请求值**(经 meta.reasoningEffort),
|
|
231
147
|
// `preset:'inherit'` 是真实生效值;`ignored` 明确列出被丢弃项。
|
|
232
|
-
|
|
148
|
+
// 信任标注不适用:continuable 只回 subagentId、无文本 output(后续经宿主的
|
|
149
|
+
// send_message 流转),没有可加前缀的回收文本,故此处不加 trustLabel。
|
|
150
|
+
const out = {
|
|
233
151
|
kind: 'continuable',
|
|
234
152
|
subagentId: childId,
|
|
235
153
|
profile: meta.profile,
|
|
@@ -240,21 +158,33 @@ async function runContinuable(args, merged, meta, parent, exec, deps) {
|
|
|
240
158
|
tokenTier: meta.tokenTier,
|
|
241
159
|
ignored: ['preset', 'reasoningEffort']
|
|
242
160
|
};
|
|
161
|
+
finalizeTrace(trace, {
|
|
162
|
+
effective: effectiveMeta({ ...meta, preset: 'inherit' }, ['preset', 'reasoningEffort']),
|
|
163
|
+
execution: { kind: 'continuable', parentSessionId: parent.session?.header?.id, childSessionId: childId, mode: 'continuable' },
|
|
164
|
+
});
|
|
165
|
+
out.decisionTrace = assertTraceSize(trace);
|
|
166
|
+
finishContinuableLedger(deps.evoLedger, base, childId); // 无结算:continuable 只写静态元数据(无 outcome)
|
|
167
|
+
return out;
|
|
243
168
|
}
|
|
244
169
|
|
|
245
170
|
// --- 后台 / 前台分支(从 execute 拆出)--------------------------------------------
|
|
246
171
|
|
|
247
|
-
//
|
|
248
|
-
// AbortController
|
|
249
|
-
|
|
250
|
-
async function runBackground(args, meta, request, parent, deps, pruneResultOutput, t0) {
|
|
172
|
+
// 后台 one-shot(job)路径:jobs.start 包 start() 并提供原生
|
|
173
|
+
// AbortController;仍是一轮即弃,非 continuable。
|
|
174
|
+
async function runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base) {
|
|
251
175
|
const jobs = deps.getService('jobs');
|
|
252
176
|
if (jobs === undefined) {
|
|
253
|
-
throw new Error('dispatch:
|
|
177
|
+
throw new Error('dispatch: 后台派发不可用:缺少 jobs 服务(请安装 @deepseek-ai/dsh-jobs 与 @deepseek-ai/dsh-tool-jobs,或改用前台派发)');
|
|
254
178
|
}
|
|
255
|
-
|
|
179
|
+
// jobId 由 jobs.start 同步返回,故用提升的 let 在结算(异步)时读取。
|
|
180
|
+
let jobId;
|
|
181
|
+
const onSettled = (settled) => {
|
|
182
|
+
guardHandle.finish(jobId, settled.childTotalTokens);
|
|
183
|
+
finishBackgroundLedger(deps.evoLedger, base, jobId, settled);
|
|
184
|
+
};
|
|
185
|
+
jobId = jobs.start({
|
|
256
186
|
kind: 'subagent',
|
|
257
|
-
label:
|
|
187
|
+
label: dispatchLabel(args, merged),
|
|
258
188
|
owner: parent,
|
|
259
189
|
run: () => {
|
|
260
190
|
const controller = new AbortController();
|
|
@@ -266,18 +196,23 @@ async function runBackground(args, meta, request, parent, deps, pruneResultOutpu
|
|
|
266
196
|
meta,
|
|
267
197
|
pruneResultOutput,
|
|
268
198
|
(session) => measureChildTokens(deps.getService, session),
|
|
269
|
-
t0
|
|
199
|
+
t0,
|
|
200
|
+
deps.logger,
|
|
201
|
+
onSettled
|
|
270
202
|
)
|
|
271
203
|
};
|
|
272
204
|
}
|
|
273
205
|
});
|
|
274
|
-
|
|
206
|
+
trackBackgroundInflight(deps.guard, jobs, jobId, parent);
|
|
207
|
+
// 后台:execute 返回时 start 尚未 resolve,无 settled(结算经 job 结果携带,不入会话块)。
|
|
208
|
+
const out = { kind: 'background', jobId, ...meta };
|
|
209
|
+
finalizeTrace(trace, { effective: effectiveMeta(meta, []), execution: { kind: 'background', parentSessionId: parent.session?.header?.id, jobId, mode: 'one-shot' } });
|
|
210
|
+
out.decisionTrace = assertTraceSize(trace);
|
|
211
|
+
return out;
|
|
275
212
|
}
|
|
276
213
|
|
|
277
|
-
// childTotalTokens
|
|
278
|
-
//
|
|
279
|
-
// measure 非函数、返回缺 totalTokens 或抛错时均返回 undefined(fail-soft),
|
|
280
|
-
// 使测量绝不阻断派发结算。
|
|
214
|
+
// childTotalTokens:宿主 tokenMeter 启发式估算(surface 口径,非计费 usage)。
|
|
215
|
+
// 仅 completed 结算时测量;服务缺失/非函数/抛错均返回 undefined(fail-soft)。
|
|
281
216
|
function measureChildTokens(getService, session) {
|
|
282
217
|
if (session === undefined || session === null) return undefined;
|
|
283
218
|
const meter = getService('tokenMeter');
|
|
@@ -292,92 +227,163 @@ function measureChildTokens(getService, session) {
|
|
|
292
227
|
}
|
|
293
228
|
}
|
|
294
229
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
230
|
+
// 仅 completed 结算时测量一次(tokenMeter 估算 + usage 分解 + 调用明细),dispose
|
|
231
|
+
// 前读子 session 保证事件流完整;全 fail-soft(缺失/抛错 → undefined)绝不阻断结算。
|
|
232
|
+
function measureForegroundRun(deps, run) {
|
|
233
|
+
const childSession = run.localAgent?.session;
|
|
234
|
+
return {
|
|
235
|
+
childTotalTokens: measureChildTokens(deps.getService, childSession),
|
|
236
|
+
childUsage: collectChildUsage(childSession),
|
|
237
|
+
childCalls: collectChildCalls(childSession),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 前台:收集结果,无论 reject 与否都释放句柄,非 completed 停因 fail-loud;
|
|
242
|
+
// run.result reject(undefined)也写失败 outcome(M9)。
|
|
243
|
+
async function runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base) {
|
|
298
244
|
const run = await deps.subagents.start('profile', request);
|
|
245
|
+
finalizeTrace(trace, { execution: { kind: 'foreground', parentSessionId: parent.session?.header?.id, childSessionId: run.id, mode: 'one-shot' } });
|
|
246
|
+
trackForegroundInflight(deps.guard, run);
|
|
299
247
|
let result;
|
|
300
|
-
let
|
|
301
|
-
let childUsage;
|
|
248
|
+
let measurement;
|
|
302
249
|
try {
|
|
303
250
|
result = await run.result;
|
|
304
|
-
|
|
305
|
-
if (result.stopReason === 'completed') {
|
|
306
|
-
const childSession = run.localAgent?.session;
|
|
307
|
-
childTotalTokens = measureChildTokens(deps.getService, childSession);
|
|
308
|
-
childUsage = collectChildUsage(childSession);
|
|
309
|
-
}
|
|
251
|
+
if (result.stopReason === 'completed') measurement = measureForegroundRun(deps, run);
|
|
310
252
|
} finally {
|
|
253
|
+
if (result === undefined) finishForegroundLedger(deps.evoLedger, base, run.id, { stopReason: 'error', output: [] }, [], Date.now() - t0, undefined);
|
|
311
254
|
await run.dispose().catch(() => {});
|
|
255
|
+
guardHandle.finish(run.id, measurement?.childTotalTokens); // untrack + recordTokens + release
|
|
312
256
|
}
|
|
313
|
-
|
|
257
|
+
const childTotalTokens = measurement?.childTotalTokens;
|
|
258
|
+
const childUsage = measurement?.childUsage;
|
|
259
|
+
const childCalls = measurement?.childCalls;
|
|
260
|
+
// 台账:completed 与非 completed 都写 outcome(对照 settleStart 语义),随后非 completed 仍 throw。
|
|
261
|
+
const pruned = pruneResultOutput(result.output);
|
|
262
|
+
const elapsedMs = Date.now() - t0;
|
|
263
|
+
finishForegroundLedger(deps.evoLedger, base, run.id, result, pruned, elapsedMs, childCalls);
|
|
314
264
|
const failure = stopReasonError(result);
|
|
315
265
|
if (failure !== undefined) throw new Error(withPartialText(failure, result.output));
|
|
316
|
-
// 结算 meta:elapsedMs 自 t0;stopReason 取底层值(此分支必 completed);
|
|
317
|
-
// childTotalTokens / childUsage 仅可测量时携带。
|
|
318
266
|
const metaOut = {
|
|
319
267
|
...meta,
|
|
320
268
|
...(childTotalTokens !== undefined ? { childTotalTokens } : {}),
|
|
321
269
|
...(childUsage !== undefined ? { childUsage } : {}),
|
|
322
|
-
elapsedMs
|
|
270
|
+
elapsedMs,
|
|
323
271
|
stopReason: result.stopReason
|
|
324
272
|
};
|
|
325
|
-
|
|
273
|
+
finalizeTrace(trace, {
|
|
274
|
+
effective: effectiveMeta(metaOut, []),
|
|
275
|
+
settled: {
|
|
276
|
+
stopReason: result.stopReason,
|
|
277
|
+
elapsedMs: metaOut.elapsedMs,
|
|
278
|
+
...(childTotalTokens !== undefined ? { childTotalTokens } : {}),
|
|
279
|
+
...(childUsage !== undefined ? { childUsage } : {}),
|
|
280
|
+
...(childCalls !== undefined ? { calls: childCalls } : {}),
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
// 信任标注:completed 子结果回灌父上下文前加结构化前缀(profile/preset 元数据、
|
|
284
|
+
// 无 prompt 原文),审计同字段 JSON 只进 logger、不进结果字符串。render 行首部
|
|
285
|
+
// 格式不变——前缀只进 output 文本(模型看到的正文带头)。
|
|
286
|
+
const output = `${trustLabel(metaOut)}${textFrom(pruned)}`;
|
|
287
|
+
deps.logger.info('[dsh-subagent-profile] trusted-output: ' + trustAudit(metaOut));
|
|
288
|
+
return { output, ...metaOut, decisionTrace: assertTraceSize(trace) };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 预检段读取:whitelist / parentComposed / parentToolNames 只读取一次,同时喂闸
|
|
292
|
+
// 与轨迹(避免重复 list() / schemas()),并组装 trace 骨架与交集闸 input。
|
|
293
|
+
async function prepareTrace(args, parent, deps) {
|
|
294
|
+
const agentPresets = parent.ctx.get('agentPresets');
|
|
295
|
+
const whitelist = new Set(await resolveWhitelist(agentPresets, deps.getEscapeSet()));
|
|
296
|
+
const parentComposed = agentPresets !== undefined ? agentPresets.composedPreset(parent.ctx) : undefined;
|
|
297
|
+
const parentToolNames = readParentToolNames(parent);
|
|
298
|
+
const parentToolCount = parentToolNames.length;
|
|
299
|
+
// 决策输入快照:与 dispatch:profiles section 同源同序(enabled、cheap-first)——
|
|
300
|
+
// 「模型当时看到哪些候选」是回答「为什么这么选」的记录面(P5 补偿)。
|
|
301
|
+
const snapshot = profileSnapshotOf([...deps.store.profiles.values()].filter((p) => p.enabled !== false).sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier)));
|
|
302
|
+
const trace = createDecisionTrace(
|
|
303
|
+
parentContextOf({
|
|
304
|
+
parentPreset: parentComposed,
|
|
305
|
+
parentProvider: parent.options.provider,
|
|
306
|
+
parentModel: parent.options.model,
|
|
307
|
+
parentToolCount,
|
|
308
|
+
allowFailOpen: deps.store.getAllowFailOpen(),
|
|
309
|
+
}),
|
|
310
|
+
requestedOf(args, { profiles: snapshot.entries })
|
|
311
|
+
);
|
|
312
|
+
const mode = args.continuable === true ? 'continuable' : (args.run_in_background === true ? 'background' : 'foreground');
|
|
313
|
+
const intersectionInput = { parentToolCount, requestedToolFilter: args.toolFilter, mode };
|
|
314
|
+
return { whitelist, parentComposed, parentToolNames, trace, mode, intersectionInput };
|
|
326
315
|
}
|
|
327
316
|
|
|
328
|
-
// execute 主体:预检段 +
|
|
317
|
+
// execute 主体:预检段 + 三分支分派。任一闸 fail 先记 fail 闸再 rethrow 原错误
|
|
318
|
+
// 对象,失败 trace 进进程内台账(deps.ledger)。
|
|
329
319
|
async function runDispatch(args, exec, deps) {
|
|
330
320
|
const parent = exec.agent;
|
|
331
|
-
if (!parent) throw new Error('dispatch
|
|
321
|
+
if (!parent) throw new Error('dispatch: 缺少调用 Agent(值需来自模型/编排者会话,请勿直接调用本工具)');
|
|
332
322
|
// 派发耗时基准:execute 入口记 t0,前台/后台各自在结算处算 elapsedMs。
|
|
333
|
-
// continuable 不结算、不携带,故 t0 仅穿线到前台/后台。
|
|
334
323
|
const t0 = Date.now();
|
|
335
|
-
const
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
324
|
+
const { whitelist, parentComposed, parentToolNames, trace, mode, intersectionInput } = await prepareTrace(args, parent, deps);
|
|
325
|
+
// 预算闸句柄提至 try 外:任何分支 throw(前台 start reject / 后台 jobs 缺失等)
|
|
326
|
+
// 都在 catch 里幂等 release 并发槽——防错误路径把父会话的并发额度永久耗尽
|
|
327
|
+
// (正常路径 finish 已 release,catch 再调为 no-op)。
|
|
328
|
+
let guardHandle = { release: () => {} };
|
|
329
|
+
try {
|
|
330
|
+
const merged = mergeProfileArgs(args, deps.store);
|
|
331
|
+
// 信封模式 opt-in:三分支创建子 Agent 前,把信封骨架追加进子 persona(systemPrompt
|
|
332
|
+
// 影子段、模型可见),不触碰 descriptor;既有 persona 非空时以空行衔接追加。
|
|
333
|
+
if (args.envelope === true) {
|
|
334
|
+
const hasPersona = merged.persona !== undefined && merged.persona.trim() !== '';
|
|
335
|
+
merged.persona = hasPersona ? `${merged.persona}\n\n${ENVELOPE_SKELETON}` : ENVELOPE_SKELETON;
|
|
336
|
+
}
|
|
337
|
+
assertContinuableEnabled(deps, args);
|
|
338
|
+
applyWhitelistGate(trace, merged, whitelist, parentComposed);
|
|
339
|
+
// Cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控)。
|
|
340
|
+
await applyCostGuardGate(trace, merged, parent, deps);
|
|
341
|
+
// 交集闸(三分支统一闸序,见 dispatch-gates.mjs);continuable 的 effectiveAllow
|
|
342
|
+
// 同时用于 request 组装。
|
|
343
|
+
const effectiveAllow = applyIntersectionGate(trace, mode, intersectionInput, merged, parentToolNames);
|
|
344
|
+
recordApprovalGate(trace);
|
|
345
|
+
const meta = buildMeta(args, merged, parent);
|
|
346
|
+
const request = buildRequest(args, merged, parent, exec.signal);
|
|
347
|
+
// 结果回收默认剪枝:在 textFrom 前复用宿主 toolResultPruner.pruneContent 预剪;
|
|
348
|
+
// pruner 缺失时 pruneBlocks 回退为不剪(剪枝是增强、非硬依赖)。
|
|
349
|
+
const pruneResultOutput = (blocks) => pruneBlocks(blocks, deps.getService('toolResultPruner'));
|
|
350
|
+
logDispatchDecision(deps.logger, args, merged, parent);
|
|
351
|
+
// 总预算守卫:前/后台在 execute 入口占并发额度(continuable 不占),结算处 release
|
|
352
|
+
// + recordTokens。guardHandle 传入分支用于释放与父 sessionId 记账。
|
|
353
|
+
guardHandle = applyBudgetGate(deps, parent, args, trace);
|
|
354
|
+
// 派发台账静态元数据(双栏 cfg/task 指纹),分支结算处补 child_id+outcome 写盘。
|
|
355
|
+
const base = deps.evoLedger.baseEntry({ parent, args, merged, mode });
|
|
356
|
+
recordEscapeAllow(deps, parent, merged, trace);
|
|
357
|
+
if (args.continuable === true) return await runContinuable(args, merged, meta, parent, exec, deps, trace, effectiveAllow, base);
|
|
358
|
+
if (args.run_in_background === true) return await runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base);
|
|
359
|
+
return await runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
guardHandle.release();
|
|
362
|
+
recordFailure(deps.ledger, parent, trace);
|
|
363
|
+
throw error;
|
|
345
364
|
}
|
|
346
|
-
await assertPresetWhitelist(parent, merged);
|
|
347
|
-
// Cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控;
|
|
348
|
-
// llm 目录读取走共享 catalog 快照)。
|
|
349
|
-
await assertCostGuard(parent, merged, deps.store.getAllowFailOpen(), deps.logger, deps.catalog);
|
|
350
|
-
const meta = buildMeta(args, merged, parent);
|
|
351
|
-
const request = buildRequest(args, merged, parent, exec.signal);
|
|
352
|
-
// 结果回收默认剪枝:在 textFrom(result.output) 之前复用宿主
|
|
353
|
-
// toolResultPruner.pruneContent 预剪。`pruneResultOutput` 每次现取
|
|
354
|
-
// ctx.get('toolResultPruner') 以反映服务就绪状态;pruner 缺失时
|
|
355
|
-
// pruneBlocks 回退为不剪(剪枝是增强、非硬依赖)。envelope:true 时信封
|
|
356
|
-
// 骨架已在上方追加进 merged.persona(不改变此处的剪枝回收路径)。
|
|
357
|
-
const pruneResultOutput = (blocks) => pruneBlocks(blocks, deps.getService('toolResultPruner'));
|
|
358
|
-
logDispatchDecision(deps.logger, args, merged, parent);
|
|
359
|
-
if (args.continuable === true) return runContinuable(args, merged, meta, parent, exec, deps);
|
|
360
|
-
if (args.run_in_background === true) return runBackground(args, meta, request, parent, deps, pruneResultOutput, t0);
|
|
361
|
-
return runForeground(meta, request, parent, deps, pruneResultOutput, t0);
|
|
362
365
|
}
|
|
363
366
|
|
|
364
|
-
export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents, catalog }) {
|
|
365
|
-
const deps = { store, getEnabled, getService, logger, subagents, catalog };
|
|
367
|
+
export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, getEscapeSet }) {
|
|
368
|
+
const deps = { store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, getEscapeSet };
|
|
366
369
|
const dispatchTool = defineTool({
|
|
367
370
|
name: 'dispatch',
|
|
368
|
-
description: '
|
|
371
|
+
description: '派发子任务给派生子 Agent,可逐个覆盖其 preset、model、provider、推理档位、persona、工具白名单、token 预算或递归深度。前台等待结果;run_in_background: true 启动后台任务(单轮即弃);continuable: true 启动持久子 Agent,后续轮次经 send_message 工具延续对话。前瞻:continuable 模式忽略 preset 换用与 reasoningEffort(结果以 ignored 提示)。',
|
|
369
372
|
parameters: DISPATCH_PARAMETERS,
|
|
370
|
-
output: {
|
|
373
|
+
output: {
|
|
374
|
+
schema: DISPATCH_OUTPUT_SCHEMA,
|
|
375
|
+
render: DISPATCH_RENDER,
|
|
376
|
+
// 决策轨迹经 presentationMeta 投影进会话块 meta(客户端账本数据源),不进 render 行。
|
|
377
|
+
presentationMeta: (_args, value) => value.decisionTrace,
|
|
378
|
+
},
|
|
371
379
|
isConcurrencySafe: () => true,
|
|
372
380
|
execute: (args, exec) => runDispatch(args, exec, deps),
|
|
373
381
|
});
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// model-side schema never silently rejects a分支.
|
|
382
|
+
// 共享一致性规则锁:闭合 oneOf 结果 schema 的三个分支必须携带同一套共享
|
|
383
|
+
// 元数据键集。只在 apply 时触发;未来加元数据字段时若漏掉某个分支,在这里
|
|
384
|
+
// throw 一次(而不是让模型侧 schema 静默拒绝某个分支)。
|
|
378
385
|
assertResultSchemaConsistency(dispatchTool.output.schema);
|
|
379
|
-
//
|
|
380
|
-
// turns off so it disappears from the model's tool list without a restart.
|
|
386
|
+
// 只在启用时注册工具;开关一关立即注销,使工具无需重启就从模型的工具列表消失。
|
|
381
387
|
let disposeTool;
|
|
382
388
|
function syncTool() {
|
|
383
389
|
if (getEnabled() && disposeTool === undefined) {
|
|
@@ -385,7 +391,10 @@ export function createDispatchTool({ register, store, getEnabled, getService, lo
|
|
|
385
391
|
} else if (!getEnabled() && disposeTool !== undefined) {
|
|
386
392
|
const dispose = disposeTool;
|
|
387
393
|
disposeTool = undefined;
|
|
388
|
-
dispose();
|
|
394
|
+
if (typeof dispose === 'function') dispose();
|
|
395
|
+
// 禁用:级联取消在途派发 + 清空并发/token 记账(重开后记账从零起步)。
|
|
396
|
+
deps.guard.cancelAll();
|
|
397
|
+
deps.guard.reset();
|
|
389
398
|
}
|
|
390
399
|
}
|
|
391
400
|
syncTool();
|
|
@@ -395,7 +404,7 @@ export function createDispatchTool({ register, store, getEnabled, getService, lo
|
|
|
395
404
|
if (disposeTool !== undefined) {
|
|
396
405
|
const dispose = disposeTool;
|
|
397
406
|
disposeTool = undefined;
|
|
398
|
-
dispose();
|
|
407
|
+
if (typeof dispose === 'function') dispose();
|
|
399
408
|
}
|
|
400
409
|
}
|
|
401
410
|
};
|