提出方:dsh-insight(配置与插件树洞察面板) 结论:当前 dsh 无法回答「这个工具是谁注册的」,缺一个字段。
用户在 Insight 面板里问的原话:
我想以工具名为切面。可能一个插件或者一个服务会提供很多个子工具,我想列出所有的工具。 可能很多个工具来自同一个插件来自同一个服务,我也想追踪是谁提供、谁开启、谁禁用的这个工具。
这是个很合理的诉求——用户要决定「哪些工具留、哪些关掉」,就得先看到工具清单, 以及每个工具背后是哪个插件(因为关工具的动作是关插件)。
在 dsh 0.1.1-rc.2 的运行实例上,从一个装载中的插件的 Context 探测:
| 探测 | 结果 |
|---|---|
ctx.get('tools').schemas() |
0 |
tools.layers.global.tools.data |
Map(0) |
tools.layers.scoped |
Map(0) |
ctx.get('skills').list() |
[] |
ctx.get('commands').list() |
4(这个能读) |
ctx.get('agents').list() |
[] |
agentPresets.serviceFor(presetId) |
抛错——要的是真实 scope 对象(Symbol(dsh.scope)),不是 id |
agentPresets.mount(agentCtx, id) |
签名要 agent 的 scope Context |
两个结论:
- 工具只在 agent 活着时存在。 它们不是插件加载时注册的,是 agent 实例化、
预设被 mount 成一个 scope 之后才
tools.register()进去的。静止的 harness 里 全局层和 scoped 层都是空的。 - 即使读到工具,也反查不回插件。
ToolDefinition继承ToolSchema,字段是name / description / input / output / execute / timeoutMs / …——没有任何一项 记录是谁注册的。
第 2 条是关键:第 1 条还能靠「面板打开时若有活跃 agent 就快照」绕过,第 2 条绕不过。
在工具注册时记录注册者,并在读取面上暴露出来。
最小改动:ToolRuntime.register(definition) 内部已经能拿到调用方的
Context / fiber,把它对应的 loader entry id 记进注册表条目,并在 schemas() /
get() 的返回上带出来。
interface ToolSchema {
name: string
description: string
// …
/** 注册它的 loader entry 完整 id,如 include:agent-presets:tool-bash。 */
readonly registrant?: string
}registrant 只读、可选(拿不到时不填),不影响任何现有调用方。
- 工具清单按工具名列,而不是按插件列——一个插件注册多个工具时能看清细分
- 每个工具能追到注册它的插件,进而追到「哪一层配置把这个插件插进来的、哪一层 把它关掉的」(这两段 Insight 已经有了,只差 tool → plugin 这一跳)
- 「关掉某个工具」这个动作能给出确切的落点:禁用哪个 entry
让工具在没有活跃 agent 时也可枚举。 比如 agentPresets 暴露一个纯查询接口,
返回「某个预设会挂载哪些工具插件 / 工具」,不需要真的 mount 出一个 scope。
这条不如上一条要紧——面板可以退而求其次,只在有活跃会话时展示运行时快照。 但如果有,就能回答「我换到 PTC 模式会多出哪些工具」这类问题,而不必先切过去试。
在拿到上面的字段之前,Insight 面板里做的是插件粒度:
- 一行一个工具插件(
tool-*的 loader entry),22 条 - 同名两份归并(宿主面一份常被 bundle 层禁用、agent 预设 realm 里一份活着), 给出净状态;两份不一致时明确标出来——单看任一份都会得出错误结论
- 每条能追到:来源包、磁盘路径、哪一层插入、哪一层禁用
- 界面上写明这一层的边界,不假装是工具清单
够用来做「关掉哪些」的决策,但粒度是插件不是工具。
同一个字段缺口,出现在 settings 上。
Insight 面板列插件时,有两行没有包名,看着像坏掉的插件:
| ns | 实际情况 |
|---|---|
shell |
设置归 include:shell-env 插件,但 ns 叫 shell、entry 短 id 叫 shell-env,对不上 |
ui-onboarding |
ctx.loader.entries() 里根本没有这个 entry——注册它的是客户端插件 |
dsh 0.1.1-rc.2,FileSettingsProvider(基类 SettingsProvider):
| 探测 | 结果 |
|---|---|
settings.describe({ redactSecrets: true }) 每行的字段 |
ns / schema / value / revision / base / user / applies / secrets |
settings.registrations 里每条注册的字段 |
ns / schema / base / applies / resolved / revision / watchers |
applies |
全是 'live',是生效时机不是归属 |
装观察器包 SettingsProvider.register |
来不及——设置在插件加载时注册,dsh-insight 排在后面,绝大多数已经注册完了 |
从 fiber 反查 settings.register("shell") 那个 effect |
fiber._disposables 是 DisposableList,里面是没有标签的裸函数,取不回 |
工具那边至少还能靠「注册发生在 agent 跑起来之后」旁听到;设置这边连这条路都没有。
register() 内部已经拿着调用方的 Context(this.ctx.effect(...) 用的就是它),
把它对应的 loader entry id 记进 registration,并在 describe() 的返回上带出来:
interface SettingsDescription {
ns: string
// …
/** 注册它的 loader entry 完整 id,如 include:shell-env;客户端注册的没有。 */
readonly registrant?: string
}只读、可选,不影响任何现有调用方。
- 设置能挂回它的插件,不再有「没有包名的插件行」
- 「我改过的这条设置属于哪个插件、那个插件是哪一层插进来的」能一路追下去
- 客户端注册的(
registrant为空)可以据实标成「客户端」,而不是和前一种混在一起
不猜。挂不上的如实标成「设置命名空间」,并说清两种成因——设置本身是真的, 改动照样生效,只是追不到注册者。