dsh-plugin-tool-management 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,16 @@ export function listOf(value) {
6
6
  const list = text.split(/[,,]/).map((s) => s.trim()).filter(Boolean);
7
7
  return list.length ? list : undefined;
8
8
  }
9
+ /**
10
+ * frontmatter 里真值标志的写法(`continuable: true` / `yes` / `on` / `1`)。
11
+ *
12
+ * 只认这几种、且**只认真值**:写错的代价是不对称的 —— 少认一个词只是静默保持一次性
13
+ * (与今天的行为一致),而反过来(把 `false` 之类误判成真)会让这个角色突然改成异步回执,
14
+ * 模型拿到一个 id 却以为拿到了结果。所以判据取窄。
15
+ */
16
+ export function truthyFlag(value) {
17
+ return value !== undefined && /^(true|yes|on|1)$/i.test(String(value).trim());
18
+ }
9
19
  /**
10
20
  * 解析 `toolsByPreset:` 块 —— 每个 Agent 预设一行:
11
21
  *
@@ -43,7 +53,7 @@ export function parsePresetToolRules(frontmatter) {
43
53
  }
44
54
  return Object.keys(rules).length ? rules : undefined;
45
55
  }
46
- /** 行式 frontmatter 解析:只认 description / provider / model / tools / toolsDeny / toolsByPreset / catalogDepth / output。 */
56
+ /** 行式 frontmatter 解析:只认 description / provider / model / reasoningEffort / tools / toolsDeny / toolsByPreset / catalogDepth / output / continuable。 */
47
57
  /**
48
58
  * 思考强度档位 id 的归一:单行、去空白、限长。**不校验它是不是合法档位** —— 清单要问 adapter
49
59
  * 才拿得到(可能拿不到),而官方本来就会在 provider I/O 之前拒掉不支持的显式值。
@@ -95,6 +105,8 @@ export function parsePersona(raw, fallbackName) {
95
105
  toolsDeny,
96
106
  ...(toolsByPreset === undefined ? {} : { toolsByPreset }),
97
107
  ...(catalogDepth === undefined ? {} : { catalogDepth }),
108
+ // 允许追问:只有真值才落这个字段(缺省 = 一次性,与今天的行为一致)。
109
+ ...(truthyFlag(data.continuable) ? { continuable: true } : {}),
98
110
  ...(outputLines.length ? { output: outputLines.join('\n') } : {}),
99
111
  body: body.trim(),
100
112
  path: '',
@@ -179,10 +191,19 @@ export function renderPersonaPrompt(persona) {
179
191
  return name;
180
192
  const rawDescription = String(persona.description ?? '').trim();
181
193
  const output = neutralizePromptVariables(String(persona.output ?? '').trim());
194
+ // 身份定位句(0.19.0):此前只有"你正在以「X」的身份执行本次委派任务"一句,它说的是
195
+ // **角色**,没说**身份**。而子会话是整份 join 父会话预设的(官方 `composeFrom`,无分项
196
+ // 开关),于是 `~/.dsh/AGENTS.md` 那份"写给主代理的操作手册"也进了它的上下文 —— 通篇是
197
+ // "最新用户消息""向用户确认""开分支""等 review"。子代理据此认定自己是主代理。
198
+ // 官方的 `SUBAGENT_DELEGATION_CONTEXT` 只讲**权限范围**("your permission scope was
199
+ // fixed…"),不讲身份,所以补这句不是重复。
200
+ // 位置放最前:先定"你是谁",后面那段角色定义才是"你怎么做"。
182
201
  const lines = isChinesePersona(name, rawDescription, raw)
183
202
  ? [
184
203
  `# 角色:${name}`,
185
204
  '',
205
+ `你是被委派的子代理,不是主代理:只做调用方交给你的这一件事,产出交回调用方,不要向用户提问。`,
206
+ '',
186
207
  `你正在以「${name}」的身份执行本次委派任务。以下角色定义由调用方指定,是你本次运行的固定行为准则:与你的默认倾向冲突时,以它为准;任务说要什么,怎么做以它为准。它决定你如何工作,不改变你的权限范围。`,
187
208
  '',
188
209
  '## 角色定义',
@@ -193,6 +214,8 @@ export function renderPersonaPrompt(persona) {
193
214
  : [
194
215
  `# Persona: ${name}`,
195
216
  '',
217
+ `You are a delegated subagent, not the primary agent: do only the one thing the caller gave you, return the output to the caller, and do not ask the user anything.`,
218
+ '',
196
219
  `You are running this delegated task as "${name}". The persona below was specified by the caller and is your fixed operating guideline for this run: where it conflicts with your default inclinations, it wins; the task states what to achieve, and this persona governs how. It governs how you work, not what you are permitted to do.`,
197
220
  '',
198
221
  '## Persona',
@@ -204,6 +227,18 @@ export function renderPersonaPrompt(persona) {
204
227
  }
205
228
  /** 一个人设没写 `catalogDepth` 时的目录注入深度:1 —— 只在顶层注入目录。 */
206
229
  export const DEFAULT_PERSONA_CATALOG_DEPTH = 1;
230
+ /**
231
+ * 允许追问人设的标记(0.19.0),由人设目录与 `subagent_manager_list` 两处共用。
232
+ *
233
+ * 为什么标在目录行上:`continuable` 改变的是**委派的返回值形态**(回执一个 id 而不是产出),
234
+ * 模型必须在这一刻就知道 —— 否则它会按"等下会拿到结果"来安排后续动作,然后拿到一个 id
235
+ * 不知如何处理。标在这里的成本是**按需**的(没有人设开它就不出现),而写进工具描述是每轮
236
+ * 都付一遍。
237
+ *
238
+ * 放在本模块(叶子)而不是 `catalog.ts`:`tools.ts` 也要用它,而 `catalog.ts` 反向依赖
239
+ * `tools.ts` 的 `filterBySceneBinding` —— 放那边会形成 tools ↔ catalog 的循环。
240
+ */
241
+ export const CONTINUABLE_MARK = '(允许追问)';
207
242
  /**
208
243
  * 「不限制嵌套」的目录注入深度(界面下拉的第四档,前端同值见 client.js 的
209
244
  * `CATALOG_DEPTH_UNLIMITED`)。
@@ -3,7 +3,7 @@
3
3
  // 从 subagents/service.ts 整段搬来,一行未改。只写用户填过的键这条纪律留在函数自己的
4
4
  // 注释里;名字谓词仍收敛到 ../paths.ts,这里不另写一份。
5
5
  import { isValidSegment } from '../paths.js';
6
- import { DEFAULT_PERSONA_CATALOG_DEPTH, normalizeReasoningEffort } from './persona-parse.js';
6
+ import { DEFAULT_PERSONA_CATALOG_DEPTH, normalizeReasoningEffort, truthyFlag } from './persona-parse.js';
7
7
  /** 人设名长度上限(字符)。 */
8
8
  export const PERSONA_NAME_MAX = 64;
9
9
  /**
@@ -40,6 +40,10 @@ export function serializePersona(args) {
40
40
  : Number(String(catalogDepthRaw).trim());
41
41
  const hasCatalogDepth = catalogDepth !== undefined && Number.isSafeInteger(catalogDepth) && catalogDepth >= 0 && catalogDepth !== DEFAULT_PERSONA_CATALOG_DEPTH;
42
42
  const body = String((args && args.body) ?? '').trim();
43
+ // 允许追问:**只在真值时才写这一行**(与 `catalogDepth` 默认值同规矩)—— 免得每个人设都多一行
44
+ // 说明"它和默认一样",也保证老的人设文件回写后逐字节不变。入参兼容布尔与字符串两种形态
45
+ // (界面传布尔,手写/导入的 frontmatter 走解析侧已归一成布尔)。
46
+ const continuable = args?.continuable === true || truthyFlag(String(args?.continuable ?? ''));
43
47
  // `output` 可传字符串(按行拆)或数组(界面直接给数组):一条要求写成一行重复键。
44
48
  const outputLines = (Array.isArray(args?.output) ? args.output.map((v) => String(v)) : String(args?.output ?? '').split(/\r?\n/))
45
49
  .map((line) => line.trim())
@@ -55,6 +59,8 @@ export function serializePersona(args) {
55
59
  lines.push('reasoningEffort: ' + reasoningEffort);
56
60
  if (hasCatalogDepth)
57
61
  lines.push('catalogDepth: ' + String(catalogDepth));
62
+ if (continuable)
63
+ lines.push('continuable: true');
58
64
  for (const line of outputLines)
59
65
  lines.push('output: ' + line);
60
66
  if (tools.length)
@@ -88,6 +88,55 @@ export function createRunProvider(deps) {
88
88
  ? { ...(p.tools?.length ? { allow: p.tools } : {}), ...(p.toolsDeny?.length ? { deny: p.toolsDeny } : {}) }
89
89
  : null;
90
90
  const resolved = toolFilter === undefined ? legacyFilter : toolFilter;
91
+ // provider 与 model 是模型路由的两半:DSH 的 resolveModel(provider, model) 不做
92
+ // `provider/model` 字符串拆分,只改 model 会落在**主会话的 provider** 上——跨来源
93
+ // 指定模型(如 sensenova 的 sensenova-6.8-flash-lite)必须两个键一起给。
94
+ // 2026-09-23 加 `reasoningEffort`:条件是"三个键任一有值"—— 只设强度(不改模型)也得
95
+ // 把这个对象发出去,否则那一格永远不生效。官方合并语义(`dsh-subagent/lib/index.js:471-483`)
96
+ // 是"先继承主会话的三项、再用这里的覆盖",所以只给 reasoningEffort 时 provider/model
97
+ // 照旧继承;反过来,改了 provider/model 而没给 reasoningEffort 时,**继承来的那一档会被
98
+ // 删掉**(回落到该模型自己的默认)—— 那不是 bug,是官方的显式语义。
99
+ // 两条通道(一次性 / 可继续)共用这一段,所以提出来只写一次。
100
+ const routeOptions = (p.provider || p.model || p.reasoningEffort)
101
+ ? {
102
+ agentOptions: {
103
+ ...(p.provider ? { provider: p.provider } : {}),
104
+ ...(p.model ? { model: p.model } : {}),
105
+ // 类型是官方的品牌串 `ReasoningEffortId`;我们的值本来就是 adapter 自己给的
106
+ // opaque id(界面从 `resolveModelInfo` 的清单里选出来的),所以这里只做归一、
107
+ // 不重新校验 —— 不支持的档位由官方在 provider I/O 之前拒。
108
+ ...(p.reasoningEffort ? { reasoningEffort: p.reasoningEffort } : {}),
109
+ },
110
+ }
111
+ : {};
112
+ // ── 允许追问通道(人设 `continuable: true`,0.19.0)────────────────────────────
113
+ // 与一次性走的是**两条官方通道**:`startContinuable` 建持久子会话,在**收件箱受理时**
114
+ // 即兑现(不是跑完),返回稳定 `childId`,子代理在后台继续跑。追问不需要本插件再出工具
115
+ // —— 宿主自带全局 `send_message` / `interrupt_agent`(`dsh-tool-subagent-control`),
116
+ // 它们对任何可继续子级都生效,与创建它的工具无关。这里只负责"把它建起来"。
117
+ //
118
+ // 两条官方语义(`dsh-subagent/lib/index.js:599-654`)决定了回执只能写成"已启动 + id":
119
+ // · 子代理启动时被追加一句返回指引,要求它**自己**用 send_message 把结果发给父代理
120
+ // —— 父代理不会自动收到它的转录、工具输出或思考;
121
+ // · 结算时父代理还会收到一条 `subagent-settled` 通知(含收尾正文)。
122
+ // 所以这条通道的产出是**异步**到达的,不是本函数的返回值。
123
+ if (p.continuable === true) {
124
+ const started = await runtime.startContinuable({
125
+ provider: kind,
126
+ label: p.name,
127
+ // `ContinuableStartSpec.request` 是 `Omit<SubagentStartRequest,'label'|'signal'|'outputSchema'>`:
128
+ // label 提到 spec 顶层、signal 由 spec 自己带(官方接管准备期的取消),两者都不进 request。
129
+ request: {
130
+ parent: parentAgent,
131
+ prompt: [{ type: 'text', text: task }],
132
+ persona: renderPersonaPrompt(p),
133
+ ...(resolved === null ? {} : { toolFilter: resolved }),
134
+ ...routeOptions,
135
+ },
136
+ signal: signal ?? new AbortController().signal,
137
+ });
138
+ return { text: '', runId: String(started?.childId ?? ''), stopReason: 'started', continuable: true };
139
+ }
91
140
  // 官方签名:start(name, request) —— name = ctx.subagents 上的 provider 注册名。
92
141
  const run = await runtime.start(kind, {
93
142
  label: p.name,
@@ -107,26 +156,8 @@ export function createRunProvider(deps) {
107
156
  // "目录出现在哪"。传了它会让我们的工具比官方严(1 vs 3),且会要求 provider 具备
108
157
  // `depthLimit` capability(不传就没这个依赖)。让 provider 用它自己的默认,与官方
109
158
  // 工具的能力保持一致。
110
- // provider 与 model 是模型路由的两半:DSH 的 resolveModel(provider, model) 不做
111
- // `provider/model` 字符串拆分,只改 model 会落在**主会话的 provider** 上——跨来源
112
- // 指定模型(如 sensenova 的 sensenova-6.8-flash-lite)必须两个键一起给。
113
- // 2026-09-23 加 `reasoningEffort`:条件是"三个键任一有值"—— 只设强度(不改模型)也得
114
- // 把这个对象发出去,否则那一格永远不生效。官方合并语义(`dsh-subagent/lib/index.js:471-483`)
115
- // 是"先继承主会话的三项、再用这里的覆盖",所以只给 reasoningEffort 时 provider/model
116
- // 照旧继承;反过来,改了 provider/model 而没给 reasoningEffort 时,**继承来的那一档会被
117
- // 删掉**(回落到该模型自己的默认)—— 那不是 bug,是官方的显式语义。
118
- ...(p.provider || p.model || p.reasoningEffort
119
- ? {
120
- agentOptions: {
121
- ...(p.provider ? { provider: p.provider } : {}),
122
- ...(p.model ? { model: p.model } : {}),
123
- // 类型是官方的品牌串 `ReasoningEffortId`;我们的值本来就是 adapter 自己给的
124
- // opaque id(界面从 `resolveModelInfo` 的清单里选出来的),所以这里只做归一、
125
- // 不重新校验 —— 不支持的档位由官方在 provider I/O 之前拒。
126
- ...(p.reasoningEffort ? { reasoningEffort: p.reasoningEffort } : {}),
127
- },
128
- }
129
- : {}),
159
+ // provider / model / reasoningEffort 的合并语义见上方 `routeOptions` 的注释。
160
+ ...routeOptions,
130
161
  });
131
162
  try {
132
163
  const result = await run.result; // 官方契约:child 级失败不 reject(stopReason 体现)
@@ -147,7 +178,10 @@ export function createRunProvider(deps) {
147
178
  await run.dispose().catch(() => { }); // 官方前台语义:collection 后即弃
148
179
  }
149
180
  }
150
- // v1 串行:同一时刻至多一个子代理运行(设计 §3.2)。
181
+ // v1 串行:同一时刻至多一次**启动**在途(设计 §3.2)。
182
+ // ⚠️ 0.19.0 起这条不再等于"至多一个子代理在跑":允许追问(`continuable`)的子代理在
183
+ // `startContinuable` 返回后仍在后台运行,而这里只串行化"启动"这一步。这是官方通道的
184
+ // 固有语义(后台子级本来就允许并存),不是本插件放宽了限制。
151
185
  let chain = Promise.resolve();
152
186
  const runSerial = (parentAgent, p, task, signal, toolFilter, inherit) => {
153
187
  const start = () => runOnce(parentAgent, p, task, signal, toolFilter, inherit);
@@ -16,7 +16,7 @@ import { catalogDepthOf, parsePersona, } from './persona-parse.js';
16
16
  import { serializePersona, validPersonaName } from './persona-write.js';
17
17
  // 拆出四层后 subagents/service.js 的公开面照旧(index.ts、ops/sessions.ts、scenes/candidates.ts、
18
18
  // subagents/tools.ts 与契约测试都按这个路径引)。
19
- export { catalogInjectedAt, catalogDepthOf, DEFAULT_PERSONA_CATALOG_DEPTH, emptyResultNote, neutralizePromptVariables, normalizeReasoningEffort, parsePersona, parsePresetToolRules, renderPersonaPrompt, REASONING_EFFORT_MAX_LENGTH, textOfBlocks, UNLIMITED_PERSONA_CATALOG_DEPTH, } from './persona-parse.js';
19
+ export { catalogInjectedAt, catalogDepthOf, CONTINUABLE_MARK, DEFAULT_PERSONA_CATALOG_DEPTH, emptyResultNote, neutralizePromptVariables, normalizeReasoningEffort, parsePersona, parsePresetToolRules, renderPersonaPrompt, REASONING_EFFORT_MAX_LENGTH, textOfBlocks, UNLIMITED_PERSONA_CATALOG_DEPTH, } from './persona-parse.js';
20
20
  export { presetRulesOf, serializePersona, validPersonaName } from './persona-write.js';
21
21
  export { decideToolFilter } from './tool-filter.js';
22
22
  const message = (e) => String((e && e.message) || e);
@@ -254,7 +254,7 @@ export function createSubagentService(ctx, opts) {
254
254
  ok: true,
255
255
  // catalogDepth 报**生效值**(没写就是默认 1),界面与模型都不必各自知道默认是多少。
256
256
  // 手写 map:新增字段必须**两处都加**(list 与 get),少一处就是界面读不到。
257
- subagents: docs.map((p) => ({ name: p.name, enabled: p.enabled !== false, description: p.description, provider: p.provider ?? null, model: p.model ?? null, reasoningEffort: p.reasoningEffort ?? null, tools: p.tools ?? null, toolsDeny: p.toolsDeny ?? null, toolsByPreset: p.toolsByPreset ?? null, catalogDepth: catalogDepthOf(p), output: p.output ?? null })),
257
+ subagents: docs.map((p) => ({ name: p.name, enabled: p.enabled !== false, description: p.description, provider: p.provider ?? null, model: p.model ?? null, reasoningEffort: p.reasoningEffort ?? null, continuable: p.continuable === true, tools: p.tools ?? null, toolsDeny: p.toolsDeny ?? null, toolsByPreset: p.toolsByPreset ?? null, catalogDepth: catalogDepthOf(p), output: p.output ?? null })),
258
258
  };
259
259
  },
260
260
  'subagent-get': async (args) => {
@@ -263,7 +263,7 @@ export function createSubagentService(ctx, opts) {
263
263
  const p = docs.find((d) => d.name === name);
264
264
  if (!p)
265
265
  return { ok: false, error: `人设不存在: ${name}` };
266
- return { ok: true, persona: { name: p.name, description: p.description, provider: p.provider ?? '', model: p.model ?? '', reasoningEffort: p.reasoningEffort ?? '', tools: p.tools ?? [], toolsDeny: p.toolsDeny ?? [], toolsByPreset: p.toolsByPreset ?? {}, catalogDepth: catalogDepthOf(p), output: p.output ?? '', body: p.body } };
266
+ return { ok: true, persona: { name: p.name, description: p.description, provider: p.provider ?? '', model: p.model ?? '', reasoningEffort: p.reasoningEffort ?? '', continuable: p.continuable === true, tools: p.tools ?? [], toolsDeny: p.toolsDeny ?? [], toolsByPreset: p.toolsByPreset ?? {}, catalogDepth: catalogDepthOf(p), output: p.output ?? '', body: p.body } };
267
267
  },
268
268
  'subagent-create': async (args) => {
269
269
  const name = String((args && args.name) || '').trim();
@@ -1,3 +1,8 @@
1
+ // src/subagents/tools.ts —— 模型工具:subagent_manager_list / subagent_manager_run(人设清单 + 官方 ctx.subagents 委派)。
2
+ // 本文件返回的是 **author 侧 tool 定义**(parameters 是 property-spec 形态),装配处必须经
3
+ // defineTool() 编译后再 register(C4:裸 register 会把未编译的参数声明直接发给模型 API)。
4
+ // 参数与 output schema 用 as const 保留字面量类型,defineTool 才能推断出参数表。
5
+ import { CONTINUABLE_MARK } from './service.js';
1
6
  // 深度探针(`subagentDepthOf`)与目录判据(`catalogInjectedAt`)都不再需要:
2
7
  // 2026-09-17 方案 A 之后,这两个工具都不再看会话深度 —— 委派由官方决定(默认能嵌套到 3 层),
3
8
  // `catalogDepth` 只管常驻目录注入到哪些会话。
@@ -34,7 +39,7 @@ export function defineSubagentManagerListTool(subagents) {
34
39
  // 注入到哪些会话,与"能不能委派"无关 —— 委派由官方决定(`dsh-tool-subagent` 默认能嵌套
35
40
  // 到 3 层)。此前按深度过滤等于把可用人设藏起来:目录不注入时模型只能靠这个工具查,
36
41
  // 而工具又不列全,结果是"明明能委派却查不到人设"。
37
- const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(无描述)'));
42
+ const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(无描述)') + (p.continuable === true ? CONTINUABLE_MARK : ''));
38
43
  let notice = '';
39
44
  try {
40
45
  notice = typeof subagents.noticeFor === 'function' ? await subagents.noticeFor(exec) : '';
@@ -73,7 +78,7 @@ export function defineSubagentManagerRunTool(subagents) {
73
78
  // 最贵的单个参数。收完两处重复后目标 ≈325 tok。判据是"删掉的那句在描述里已经有了吗":
74
79
  // Modes 段留着(它是模型的入口),参数说明只留"这一段独有的信息"。
75
80
  // `inherit` 那句"轮中委派拿不到当前轮"必须留 —— 它是这条参数唯一会让人写错 task 的地方。
76
- description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output.\n\nModes: by default a fresh child that cannot see this conversation, so `task` must be self-contained. With `inherit: true` it also gets this conversation\'s **finished** turns (like the host\'s `subagent_fork`) — the current turn is never included, so a mid-turn hand-off still needs a self-contained `task`.\n\n`task` = the goal plus the context it needs; leave method and output format to the persona.\n\nUse it when the work matches a persona in the「可委派的子智能体」reminder (a review, an investigation, a piece of writing) and the detail should not sit in your own context. Not for reading a file (Read), finding a definition (Grep/Glob), or touching two or three files.',
81
+ description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output. A persona marked「允许追问」instead returns an agent id and runs in the background — continue it with `send_message`.\n\nModes: by default a fresh child that cannot see this conversation, so `task` must be self-contained. With `inherit: true` it also gets this conversation\'s **finished** turns (like the host\'s `subagent_fork`) — the current turn is never included, so a mid-turn hand-off still needs a self-contained `task`.\n\n`task` = the goal plus the context it needs; leave method and output format to the persona.\n\nUse it when the work matches a persona in the「可委派的子智能体」reminder (a review, an investigation, a piece of writing) and the detail should not sit in your own context. Not for reading a file (Read), finding a definition (Grep/Glob), or touching two or three files.',
77
82
  parameters: {
78
83
  agent: { type: 'string', required: true, description: 'Persona name.' },
79
84
  task: { type: 'string', required: true, description: 'The task for the subagent. Self-contained by default; with inherit: true only state what is new.' },
@@ -113,6 +118,16 @@ export function defineSubagentManagerRunTool(subagents) {
113
118
  const r = await subagents.runSerial(exec.agent, persona, task, exec.signal, decision === undefined ? undefined : decision.filter, inherit);
114
119
  const prefix = r.stopReason && r.stopReason !== 'completed' ? `[stopReason: ${r.stopReason}]\n` : '';
115
120
  const note = decision && decision.note ? `⚠ ${decision.note}\n\n` : '';
121
+ // 允许追问通道(人设 `continuable: true`):产出是**异步**到达的,这里只拿到子会话 id。
122
+ // 回执必须把"id + 怎么追问 + 怎么中止"三件事写全 —— 少写一句,模型就会把"没拿到结果"
123
+ // 读成"委派失败"而重试一次(那会再起一个后台子代理,白白翻倍)。
124
+ if (r.continuable === true) {
125
+ return note + '已启动允许追问的子代理「' + name + '」(后台运行)\n'
126
+ + 'agent_id: ' + r.runId + '\n\n'
127
+ + '它的产出稍后自己发到这里,届时你会收到一条通知。要追问就 '
128
+ + 'send_message({ agent_id: "' + r.runId + '", message: "..." }),'
129
+ + '要中止就 interrupt_agent({ agent_id: "' + r.runId + '" })。';
130
+ }
116
131
  return note + prefix + r.text;
117
132
  },
118
133
  };
@@ -33,7 +33,15 @@ export function buildSubagentTools(deps) {
33
33
  try {
34
34
  // 工具限制按**当前会话的 Agent 预设**下发:父会话跑在哪个预设,就用那个预设那一行的
35
35
  // 白/黑名单(`decideToolFilter`),名单里已消失的工具名会被丢掉并在结果里如实说明。
36
- register(defineTool(defineSubagentManagerRunTool({ ...deps.subagentService, sceneLists: deps.sceneLists, toolFilterFor: deps.toolFilterFor })));
36
+ // ⚠️ 必须 `Object.create`,**不能**用对象展开 —— 这是 0.15.0 起就存在的实际故障:
37
+ // `deps.subagentService` 是 index.ts 用 `Object.assign(Object.create(真实服务), { ops })`
38
+ // 造的**包装层**,只有 `ops` 落在自有属性上,`list` / `runSerial` 都挂在原型链上。对象展开
39
+ // 只复制**自有可枚举**属性,展开后只剩 `{ ops }` —— 于是 `subagent_manager_run` 每次一进
40
+ // execute 就在 `subagents.list()` 上抛 `subagents.list is not a function`(人设查不到就
41
+ // 谈不上委派;0.19.0 测「允许追问」时才被暴露出来,因为那条通道同样要过这一步)。
42
+ // `Object.create` 保持同一条原型链:`ops` 仍是审计过的那份,`list` / `runSerial` 回真实服务。
43
+ // 与 index.ts 造这个包装层时用的是同一个写法(那边注释写的是「原型链回真实服务,其余读法一字不动」)。
44
+ register(defineTool(defineSubagentManagerRunTool(Object.assign(Object.create(deps.subagentService), { sceneLists: deps.sceneLists, toolFilterFor: deps.toolFilterFor }))));
37
45
  }
38
46
  catch (e) {
39
47
  recordFailure('subagent_manager_run', e);
@@ -147,6 +155,10 @@ export function buildSubagentTools(deps) {
147
155
  // 就是静默清掉它。`reasoningEffort` 是 0.14.0 新增的,最容易在这里被漏掉 ——
148
156
  // 模型只改一句 body,用户配好的思考强度就没了,而且没有任何提示。
149
157
  reasoningEffort: keep.reasoningEffort ?? '',
158
+ // 0.19.0 同理:`continuable` 不在本工具的参数表里(与工具限制、provider/model 同一档 ——
159
+ // 它在面板上配),所以这里**只负责别把它冲掉**。漏了它的后果是:模型改一次描述,
160
+ // 用户设好的"允许追问"就静默变回一次性,而回执照样说成功。
161
+ continuable: keep.continuable === true,
150
162
  tools: keep.tools ?? [],
151
163
  toolsDeny: keep.toolsDeny ?? [],
152
164
  toolsByPreset: keep.toolsByPreset ?? {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-tool-management",
3
- "version": "0.18.5",
3
+ "version": "0.19.0",
4
4
  "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagent personas and AGENTS.md presets — a scene switches the whole environment, and the model can drive it from chat too.",
5
5
  "repository": {
6
6
  "type": "git",