dsh-agentic-tool-catalog 0.0.0-stage → 0.3.2
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/CHANGELOG.md +63 -0
- package/LICENSE +21 -0
- package/README.md +173 -2
- package/README.zh.md +135 -0
- package/cordis.patch.yml +29 -0
- package/docs/assets/plugin-page.png +0 -0
- package/docs/assets/ui-dock.png +0 -0
- package/docs/assets/ui-panel.png +0 -0
- package/docs/benchmark.md +54 -0
- package/docs/data-card.md +96 -0
- package/docs/detail-benchmark.svg +30 -0
- package/docs/detail-hero.svg +57 -0
- package/docs/detail.md +33 -0
- package/docs/detail.zh.md +30 -0
- package/icon.svg +15 -0
- package/lib/catalog.js +208 -0
- package/lib/client.js +368 -0
- package/lib/generation.js +342 -0
- package/lib/index.js +795 -0
- package/lib/store.js +108 -0
- package/lib/subagent-compat.js +87 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +90 -4
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-groups.js —— 真 agent 参与归类(生产化版):用 DSH 原生子代理生成工具分组。
|
|
3
|
+
*
|
|
4
|
+
* 与"一次性 LLM 调用"的区别(用户 2026-11-10 明确):
|
|
5
|
+
* 这里启动的是**真实 DSH 子代理**(ctx.get('subagents').startActivation,provider=spawn),
|
|
6
|
+
* 它继承本机模型/preset,可多步推理,最后通过结构化输出提交 `{groups:[...]}`;
|
|
7
|
+
* 我们只接收最终结构化结果、校验、缓存、失败回退 rules。
|
|
8
|
+
*
|
|
9
|
+
* 安全/稳定性:
|
|
10
|
+
* - 递归守卫在 index.js:子代理自身(session.header.origin === 'subagent')不触发本层;
|
|
11
|
+
* - 超时 + dispose;任何失败都不抛进 agent-loop,返回 error 供状态记录;
|
|
12
|
+
* - 结果严格校验(100% 覆盖、无重复、组名/描述/组数/组大小合法),不合法即回退规则目录。
|
|
13
|
+
*
|
|
14
|
+
* @module @local/dsh-agentic-tool-catalog/generation
|
|
15
|
+
*/
|
|
16
|
+
import { normalizeGroupId, summarizeGroups } from './catalog.js'
|
|
17
|
+
import { ACTIVATION_SHAPES, isActivationShapeError, startSubagentActivation } from './subagent-compat.js'
|
|
18
|
+
|
|
19
|
+
/** 连续多少次"宿主不认 activation spec"后暂停自动生成(避免 9/9 静默刷失败)。 */
|
|
20
|
+
export const SHAPE_FAILURE_PAUSE_THRESHOLD = 3
|
|
21
|
+
/** 形状不兼容暂停时长:1 小时;期间继续用 last-known-good 目录。 */
|
|
22
|
+
export const SHAPE_FAILURE_PAUSE_MS = 60 * 60 * 1000
|
|
23
|
+
|
|
24
|
+
export const AGENT_GROUP_DEFAULTS = {
|
|
25
|
+
provider: 'spawn',
|
|
26
|
+
timeoutMs: 120000,
|
|
27
|
+
maxGroups: 24,
|
|
28
|
+
minGroups: 6,
|
|
29
|
+
maxGroupSize: 40,
|
|
30
|
+
descriptionLimit: 120,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 结构化输出 schema:子代理必须提交 {groups:[{id,description,tools:[...]}]}。 */
|
|
34
|
+
export const AGENT_GROUP_SCHEMA = {
|
|
35
|
+
type: 'object',
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
properties: {
|
|
38
|
+
groups: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
properties: {
|
|
44
|
+
id: { type: 'string' },
|
|
45
|
+
description: { type: 'string' },
|
|
46
|
+
tools: { type: 'array', items: { type: 'string' } },
|
|
47
|
+
},
|
|
48
|
+
required: ['id', 'description', 'tools'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['groups'],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const truncate = (text, limit) => {
|
|
56
|
+
const value = String(text ?? '').replace(/\s+/g, ' ').trim()
|
|
57
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 生成子代理的任务 prompt(工具清单直接内联,子代理无需调工具)。 */
|
|
61
|
+
export function buildAgentGroupPrompt(tools, options = {}) {
|
|
62
|
+
const feedback = options.feedback
|
|
63
|
+
const limit = options.descriptionLimit ?? AGENT_GROUP_DEFAULTS.descriptionLimit
|
|
64
|
+
const maxGroups = options.maxGroups ?? AGENT_GROUP_DEFAULTS.maxGroups
|
|
65
|
+
const previous = Array.isArray(options.previousGroups) ? options.previousGroups : undefined
|
|
66
|
+
// 组数下限受工具数约束:工具不足 6 个时不可能凑出 6 个非空组(空目录只能是 0 组)。
|
|
67
|
+
const minGroups = Math.min(AGENT_GROUP_DEFAULTS.minGroups, tools.length)
|
|
68
|
+
const allowedGroups = Math.min(maxGroups, tools.length)
|
|
69
|
+
if (previous !== undefined && previous.length > 0) {
|
|
70
|
+
// 增量模式:只给“上次成功分组 + 新工具描述”,要求保留稳定 id、最小改动。
|
|
71
|
+
const known = new Set(tools.map((tool) => tool.name))
|
|
72
|
+
const previousCompact = previous.map((group) => ({
|
|
73
|
+
id: group.id,
|
|
74
|
+
description: truncate(group.description, 160),
|
|
75
|
+
tools: (group.members ?? group.tools ?? []).filter((name) => known.has(name)),
|
|
76
|
+
})).filter((group) => group.tools.length > 0)
|
|
77
|
+
const previousNames = new Set(previousCompact.flatMap((group) => group.tools))
|
|
78
|
+
const newTools = tools.filter((tool) => !previousNames.has(tool.name))
|
|
79
|
+
const inventory = JSON.stringify(newTools.map((tool) => ({ name: tool.name, description: truncate(tool.description, limit) })))
|
|
80
|
+
return [
|
|
81
|
+
'You are updating an existing agent tool-catalog grouping after a catalog change.',
|
|
82
|
+
'Keep every existing group id/description and its tool membership unchanged unless the new tools clearly require',
|
|
83
|
+
'adding to a group or a split/merge; prefer the smallest change. Then return the COMPLETE updated grouping.',
|
|
84
|
+
'',
|
|
85
|
+
`Constraints: between ${minGroups} and ${allowedGroups} groups; every current tool exactly once;`,
|
|
86
|
+
'copy tool names exactly as given (including mcp__server__ prefixes); do not invent/omit tools;',
|
|
87
|
+
'return the result via the structured output tool.',
|
|
88
|
+
...feedback === undefined ? [] : ['', `A previous attempt was rejected: ${feedback}. Fix exactly that and retry.`],
|
|
89
|
+
'',
|
|
90
|
+
'Existing groups (JSON, tool names only):',
|
|
91
|
+
JSON.stringify(previousCompact),
|
|
92
|
+
'',
|
|
93
|
+
'New tools since last successful grouping (JSON):',
|
|
94
|
+
inventory,
|
|
95
|
+
'',
|
|
96
|
+
'Current tool count: ' + tools.length + '; previous grouped tools: ' + previousNames.size,
|
|
97
|
+
].join('\n')
|
|
98
|
+
}
|
|
99
|
+
const inventory = JSON.stringify(tools.map((tool) => ({ name: tool.name, description: truncate(tool.description, limit) })))
|
|
100
|
+
return [
|
|
101
|
+
'You are organizing an agent\'s on-demand tool catalog into capability groups.',
|
|
102
|
+
'The catalog below is the ONLY source of tools. Group every tool exactly once, by user intent/capability',
|
|
103
|
+
'(not merely by name prefix or MCP server namespace). Prefer groups a model can pick by reading the group name.',
|
|
104
|
+
'',
|
|
105
|
+
`Constraints: between ${minGroups} and ${allowedGroups} groups; group id lowercase kebab-case and unique;`,
|
|
106
|
+
'group description one short English sentence (<=160 chars) naming the capability; every tool exactly once;',
|
|
107
|
+
'IMPORTANT: copy every tool name EXACTLY as given, character for character (including the full mcp__server__ prefix);',
|
|
108
|
+
'never shorten, translate, or rename a tool; do not invent tool names; do not omit tools; each tool appears once.',
|
|
109
|
+
'Return the result via the structured output tool.',
|
|
110
|
+
...feedback === undefined ? [] : ['', `A previous attempt was rejected: ${feedback}. Fix exactly that and retry.`],
|
|
111
|
+
'',
|
|
112
|
+
'Tool catalog (JSON):',
|
|
113
|
+
inventory,
|
|
114
|
+
].join('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 校验子代理返回的分组。
|
|
119
|
+
* @returns { groups: [{id, description, members}] } 或 { error: {code,message} }
|
|
120
|
+
*/
|
|
121
|
+
export function validateAgentGroups(tools, raw, options = {}) {
|
|
122
|
+
const maxGroups = options.maxGroups ?? AGENT_GROUP_DEFAULTS.maxGroups
|
|
123
|
+
const minGroups = options.minGroups ?? AGENT_GROUP_DEFAULTS.minGroups
|
|
124
|
+
const maxGroupSize = options.maxGroupSize ?? AGENT_GROUP_DEFAULTS.maxGroupSize
|
|
125
|
+
if (raw === null || typeof raw !== 'object' || !Array.isArray(raw.groups)) {
|
|
126
|
+
return { error: { code: 'INVALID_SHAPE', message: 'agent output has no groups array' } }
|
|
127
|
+
}
|
|
128
|
+
const known = new Map(tools.map((tool) => [tool.name, tool]))
|
|
129
|
+
// 空目录:没有任何可分组对象,唯一合法的输出就是 0 组(否则生成永远卡在 TOO_FEW_GROUPS)。
|
|
130
|
+
if (tools.length === 0) {
|
|
131
|
+
if (raw.groups.length === 0) return { groups: [] }
|
|
132
|
+
return { error: { code: 'UNEXPECTED_GROUPS', message: `catalog is empty but agent returned ${raw.groups.length} group(s)` } }
|
|
133
|
+
}
|
|
134
|
+
// 组数下限受工具数约束:工具不足 minGroups 个时,凑满 minGroups 个非空组是不可能的。
|
|
135
|
+
const requiredMinGroups = Math.min(minGroups, tools.length)
|
|
136
|
+
|
|
137
|
+
/** 模型常把 mcp__workspace__calendar_list 简写/补全错;唯一后缀匹配可安全修正。 */
|
|
138
|
+
const resolveName = (raw) => {
|
|
139
|
+
if (typeof raw !== 'string' || raw === '') return undefined
|
|
140
|
+
if (known.has(raw)) return raw
|
|
141
|
+
const suffix = `__${raw}`
|
|
142
|
+
const matches = [...known.keys()].filter((name) => name.endsWith(suffix) || name === raw)
|
|
143
|
+
return matches.length === 1 ? matches[0] : undefined
|
|
144
|
+
}
|
|
145
|
+
const seen = new Map() // toolName -> groupId
|
|
146
|
+
const groups = []
|
|
147
|
+
const ids = new Set()
|
|
148
|
+
for (const entry of raw.groups) {
|
|
149
|
+
if (entry === null || typeof entry !== 'object') return { error: { code: 'INVALID_SHAPE', message: 'group entry is not an object' } }
|
|
150
|
+
if (typeof entry.id !== 'string' || entry.id.trim() === '') return { error: { code: 'INVALID_GROUP', message: 'group id missing' } }
|
|
151
|
+
const id = normalizeGroupId(entry.id)
|
|
152
|
+
if (ids.has(id)) return { error: { code: 'DUPLICATE_GROUP', message: `duplicate group id ${id}` } }
|
|
153
|
+
ids.add(id)
|
|
154
|
+
const description = truncate(entry.description, 200)
|
|
155
|
+
if (description === '') return { error: { code: 'INVALID_GROUP', message: `group ${id} has no description` } }
|
|
156
|
+
const members = []
|
|
157
|
+
for (const rawName of Array.isArray(entry.tools) ? entry.tools : []) {
|
|
158
|
+
const name = resolveName(rawName)
|
|
159
|
+
if (name === undefined) return { error: { code: 'UNKNOWN_TOOL', message: `group ${id} references unknown tool ${String(rawName).slice(0, 80)}` } }
|
|
160
|
+
if (seen.has(name)) return { error: { code: 'DUPLICATE_TOOL', message: `tool ${name} appears in multiple groups` } }
|
|
161
|
+
seen.set(name, id)
|
|
162
|
+
members.push(name)
|
|
163
|
+
}
|
|
164
|
+
if (members.length === 0) return { error: { code: 'EMPTY_GROUP', message: `group ${id} has no tools` } }
|
|
165
|
+
if (members.length > maxGroupSize) return { error: { code: 'GROUP_TOO_LARGE', message: `group ${id} has ${members.length} tools (>${maxGroupSize})` } }
|
|
166
|
+
groups.push({ id, description, members: members.sort() })
|
|
167
|
+
}
|
|
168
|
+
if (groups.length < requiredMinGroups) return { error: { code: 'TOO_FEW_GROUPS', message: `agent output ${groups.length} groups (<${requiredMinGroups})` } }
|
|
169
|
+
if (groups.length > maxGroups) return { error: { code: 'TOO_MANY_GROUPS', message: `agent output ${groups.length} groups (>${maxGroups})` } }
|
|
170
|
+
const missing = tools.map((tool) => tool.name).filter((name) => !seen.has(name))
|
|
171
|
+
if (missing.length > 0) {
|
|
172
|
+
return { error: { code: 'MISSING_TOOLS', message: `agent output misses ${missing.length} tool(s): ${missing.slice(0, 5).join(', ')}` } }
|
|
173
|
+
}
|
|
174
|
+
return { groups }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 把一组“已知良好”的分组适配到当前工具目录(用于生成失败时的降级,避免退回规则目录)。 */
|
|
178
|
+
export function adaptAgentGroups(tools, groups) {
|
|
179
|
+
const known = new Set(tools.map((tool) => tool.name))
|
|
180
|
+
const memberOf = new Map()
|
|
181
|
+
const descriptions = new Map()
|
|
182
|
+
const kept = []
|
|
183
|
+
for (const group of groups) {
|
|
184
|
+
const members = (group.members ?? group.tools ?? []).filter((name) => known.has(name))
|
|
185
|
+
if (members.length === 0) continue
|
|
186
|
+
const id = normalizeGroupId(group.id)
|
|
187
|
+
descriptions.set(id, truncate(group.description, 200))
|
|
188
|
+
for (const name of members) if (!memberOf.has(name)) memberOf.set(name, id)
|
|
189
|
+
kept.push({ id, description: truncate(group.description, 200), members })
|
|
190
|
+
}
|
|
191
|
+
const entries = tools.map((tool) => ({ ...tool, group: memberOf.get(tool.name) ?? 'others' }))
|
|
192
|
+
return {
|
|
193
|
+
strategy: 'agentic-tool-catalog',
|
|
194
|
+
source: 'agent-stale',
|
|
195
|
+
entries,
|
|
196
|
+
groups: summarizeGroups(entries.filter((entry) => entry.group !== 'others'), descriptions),
|
|
197
|
+
others: entries.filter((entry) => entry.group === 'others').map((entry) => entry.name).sort(),
|
|
198
|
+
warnings: [{ kind: 'catalog-stale-groups', message: `沿用上次成功分组(${kept.length} 组),${entries.filter((e) => e.group === 'others').length} 个新工具暂放 others` }],
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 由校验后的分组构造目录对象(与 policy.buildCatalog 同形)。 */
|
|
203
|
+
export function catalogFromAgentGroups(tools, groups) {
|
|
204
|
+
const memberOf = new Map()
|
|
205
|
+
const descriptions = new Map()
|
|
206
|
+
for (const group of groups) {
|
|
207
|
+
descriptions.set(group.id, group.description)
|
|
208
|
+
for (const name of group.members) memberOf.set(name, group.id)
|
|
209
|
+
}
|
|
210
|
+
const entries = tools.map((tool) => ({ ...tool, group: memberOf.get(tool.name) ?? 'others' }))
|
|
211
|
+
const catalog = {
|
|
212
|
+
strategy: 'agentic-tool-catalog',
|
|
213
|
+
source: 'agent',
|
|
214
|
+
entries,
|
|
215
|
+
groups: summarizeGroups(entries.filter((entry) => entry.group !== 'others'), descriptions),
|
|
216
|
+
others: entries.filter((entry) => entry.group === 'others').map((entry) => entry.name).sort(),
|
|
217
|
+
warnings: [{ kind: 'catalog-generated', message: `agent curated ${memberOf.size}/${tools.length} tools into ${groups.length} groups` }],
|
|
218
|
+
}
|
|
219
|
+
return catalog
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 创建"子代理分组器"。
|
|
224
|
+
* @param ctx - 插件 ctx(需要 ctx.get('subagents'))。
|
|
225
|
+
* @param config - 插件 config(catalogProvider / catalogTimeoutMs)。
|
|
226
|
+
* @param logger - 可选 logger。
|
|
227
|
+
*/
|
|
228
|
+
export function createAgentGrouper(ctx, config, logger, sharedStats = undefined) {
|
|
229
|
+
// stats 可由插件实例之外的共享对象提供:HMR 换实例后仍能看到累计成功/失败/重试次数。
|
|
230
|
+
const stats = sharedStats ?? { runs: 0, cacheHits: 0, failures: 0, retries: 0, deduped: 0, staleUses: 0, incrementalRuns: 0, lastError: null, lastOkAt: null }
|
|
231
|
+
stats.retries ??= 0
|
|
232
|
+
stats.deduped ??= 0
|
|
233
|
+
stats.staleUses ??= 0
|
|
234
|
+
delete stats.fallbacks
|
|
235
|
+
const provider = config?.catalogProvider ?? AGENT_GROUP_DEFAULTS.provider
|
|
236
|
+
const timeoutMs = config?.catalogTimeoutMs ?? AGENT_GROUP_DEFAULTS.timeoutMs
|
|
237
|
+
/** 连续"形状不兼容"失败计数;跨 generator 实例不保留(插件重载即重试)。 */
|
|
238
|
+
let shapeFailures = 0
|
|
239
|
+
|
|
240
|
+
const service = () => {
|
|
241
|
+
const subagents = ctx.get?.('subagents')
|
|
242
|
+
if (subagents === undefined || typeof subagents.startActivation !== 'function') return undefined
|
|
243
|
+
if (typeof subagents.getProvider === 'function' && subagents.getProvider(provider) === undefined) return undefined
|
|
244
|
+
return subagents
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 生成一次分组;失败返回 error(不抛)。 */
|
|
248
|
+
async function generate({ agent, tools, previousGroups }) {
|
|
249
|
+
// 形状不兼容暂停:到期自动解除;期间不计数、不重试,继续用 last-known-good。
|
|
250
|
+
if (stats.paused !== undefined) {
|
|
251
|
+
if (Date.now() < stats.paused.retryAt) return { error: { code: stats.paused.code, message: stats.paused.message } }
|
|
252
|
+
delete stats.paused
|
|
253
|
+
shapeFailures = 0
|
|
254
|
+
}
|
|
255
|
+
const subagents = service()
|
|
256
|
+
if (subagents === undefined) {
|
|
257
|
+
// 观测缺口修复:provider 不可用也计入 failures / lastError(否则状态里看不到真实失败原因)。
|
|
258
|
+
const error = { code: 'NO_SUBAGENT_SERVICE', message: `subagent provider ${provider} unavailable` }
|
|
259
|
+
stats.failures += 1
|
|
260
|
+
stats.lastError = { ...error, at: Date.now() }
|
|
261
|
+
return { error }
|
|
262
|
+
}
|
|
263
|
+
stats.runs += 1
|
|
264
|
+
let feedback
|
|
265
|
+
const canIncremental = Array.isArray(previousGroups) && previousGroups.length > 0
|
|
266
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
267
|
+
const useIncremental = canIncremental && attempt === 0
|
|
268
|
+
if (useIncremental) stats.incrementalRuns = (stats.incrementalRuns ?? 0) + 1
|
|
269
|
+
const attemptController = new AbortController()
|
|
270
|
+
const attemptTimer = setTimeout(() => attemptController.abort(), timeoutMs)
|
|
271
|
+
let activation
|
|
272
|
+
try {
|
|
273
|
+
// 版本形状差异只在 subagent-compat.js 里处理:顶层字段服务 0.2.2-rc.2+,
|
|
274
|
+
// 不可枚举的 request 兼容 0.2.2-rc.1 及更早。
|
|
275
|
+
activation = await startSubagentActivation(subagents, {
|
|
276
|
+
provider,
|
|
277
|
+
label: 'agentic-tool-catalog generation',
|
|
278
|
+
parent: agent,
|
|
279
|
+
prompt: [{ type: 'text', text: buildAgentGroupPrompt(tools, { feedback, previousGroups: useIncremental ? previousGroups : undefined }) }],
|
|
280
|
+
maxDepth: 1,
|
|
281
|
+
outputSchema: AGENT_GROUP_SCHEMA,
|
|
282
|
+
signal: attemptController.signal,
|
|
283
|
+
delivery: 'caller',
|
|
284
|
+
})
|
|
285
|
+
const result = await activation.result
|
|
286
|
+
// 宿主接受了 spec 并返回结果:形状回退计数清零(即使下面校验失败,也与形状无关)。
|
|
287
|
+
shapeFailures = 0
|
|
288
|
+
delete stats.paused
|
|
289
|
+
const structured = result?.structured
|
|
290
|
+
const validated = validateAgentGroups(tools, structured)
|
|
291
|
+
if (validated.error === undefined) {
|
|
292
|
+
stats.lastOkAt = Date.now()
|
|
293
|
+
return { groups: validated.groups }
|
|
294
|
+
}
|
|
295
|
+
stats.lastError = { ...validated.error, at: Date.now() }
|
|
296
|
+
if (attempt === 0) {
|
|
297
|
+
// 校验失败:带错误摘要重试一次(模型大多能把漏/错的名字改对)
|
|
298
|
+
stats.retries += 1
|
|
299
|
+
feedback = `${validated.error.code}: ${validated.error.message}`.slice(0, 200)
|
|
300
|
+
logger?.warn?.(`dsh-agentic-tool-catalog generation: retry after ${validated.error.code}`)
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
stats.failures += 1
|
|
304
|
+
return { error: validated.error }
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const message = String(error?.message ?? error).slice(0, 160)
|
|
307
|
+
const aborted = attemptController.signal.aborted
|
|
308
|
+
const shapeFailure = !aborted && isActivationShapeError(error)
|
|
309
|
+
if (shapeFailure) shapeFailures += 1
|
|
310
|
+
else if (!aborted) shapeFailures = 0
|
|
311
|
+
stats.lastError = {
|
|
312
|
+
code: aborted ? 'TIMEOUT' : shapeFailure ? 'UNSUPPORTED_HOST' : 'AGENT_FAILED',
|
|
313
|
+
message,
|
|
314
|
+
at: Date.now(),
|
|
315
|
+
}
|
|
316
|
+
logger?.warn?.(`dsh-agentic-tool-catalog generation: ${message}`)
|
|
317
|
+
if (shapeFailure && shapeFailures >= SHAPE_FAILURE_PAUSE_THRESHOLD && stats.paused === undefined) {
|
|
318
|
+
// 软闸门:不为"宿主形状不认识"反复刷失败;保留 last-known-good,显式标 paused。
|
|
319
|
+
stats.paused = {
|
|
320
|
+
code: 'UNSUPPORTED_HOST',
|
|
321
|
+
message: `host rejected the ${ACTIVATION_SHAPES.join('+')} subagent activation spec (${message}); `
|
|
322
|
+
+ 'automatic catalog generation paused for 1h; the last successful grouping keeps serving',
|
|
323
|
+
at: Date.now(),
|
|
324
|
+
retryAt: Date.now() + SHAPE_FAILURE_PAUSE_MS,
|
|
325
|
+
}
|
|
326
|
+
logger?.warn?.(`dsh-agentic-tool-catalog: ${stats.paused.message}`)
|
|
327
|
+
}
|
|
328
|
+
stats.failures += 1
|
|
329
|
+
return { error: stats.lastError }
|
|
330
|
+
} finally {
|
|
331
|
+
clearTimeout(attemptTimer)
|
|
332
|
+
if (activation !== undefined) {
|
|
333
|
+
try { await activation.dispose() } catch {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
stats.failures += 1
|
|
338
|
+
return { error: stats.lastError ?? { code: 'UNKNOWN', message: 'generation failed' } }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return { generate, stats, provider, activationShapes: ACTIVATION_SHAPES, enabled: () => service() !== undefined }
|
|
342
|
+
}
|