Repository navigation
[Bug] Switching a blank session to an already-loaded preset drops subagent tools #6166
Replies: 2 comments
|
补充验证:官方 npm 测试环境与未打补丁证明
实际结果先加载目标 scope,再加载来源 scope;Agent 从来源重绑目标,两个工具同时消失,再触发 可直接运行的最小复现将以下代码保存为 node repro-subagent.mjs /path/to/node_modules/@deepseek-ai/dsh这里使用真实 Cordis、AgentLoop、scope、tool-subagent 等插件,以及预设重组所用的 import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const root = process.argv[2];
assert(root, '请传入已安装的 @deepseek-ai/dsh 包目录');
const require = createRequire(pathToFileURL(resolve(root, 'package.json')));
const load = async name => import(pathToFileURL(require.resolve('@deepseek-ai/' + name)).href);
const { Context } = await load('cordis');
const { createScope, scopeOf, bindScopeParent } = await load('dsh-scope');
const tool = await load('dsh-tool-subagent');
const ctx = new Context();
let handle;
try {
for (const name of ['dsh-llm', 'dsh-session', 'dsh-session-projection', 'dsh-system-prompt', 'dsh-tools', 'dsh-agent']) {
await ctx.plugin((await load(name)).default);
}
await ctx.plugin((await load('dsh-agent-loop')).default, { agents: [] });
await ctx.plugin((await load('dsh-subagent')).default);
await ctx.plugin(await load('dsh-subagent-spawn-in-process'), { providerName: 'spawn' });
await ctx.plugin((await load('dsh-tool-subagent/model-selection-settings')).default, {
enabled: true, allowedModels: [{ provider: 'synthetic', model: 'never-called' }],
});
const first = createScope(ctx, { preset: 'first' });
const second = createScope(ctx, { preset: 'second' });
for (const preset of [first, second]) {
await preset.ctx.plugin(tool, {
provider: 'spawn', modelSelectionSettings: true, backgroundMode: 'continuable',
});
}
let binding;
handle = await ctx.agents.create({
sessionId: 'synthetic-recompose',
setup(agentCtx) { binding = bindScopeParent(scopeOf(agentCtx), scopeOf(second.ctx)); },
});
const settle = () => new Promise(resolve => setImmediate(resolve));
const names = () => ctx.tools.schemas(handle.agent).map(tool => tool.name);
await settle();
console.log('before:', names());
assert(names().includes('subagent'));
binding.rebind(scopeOf(first.ctx));
ctx.emit('tools/change');
await settle(); await settle();
console.log('after:', names());
ctx.emit('tools/change');
await settle();
console.log('after another tools/change:', names());
assert(names().includes('subagent'), 'subagent must survive preset reparenting');
assert(names().includes('list_subagent_models'), 'model discovery must survive preset reparenting');
} finally {
if (handle) await handle.dispose();
await ctx.fiber.dispose();
}修复方向的补充与楼主分析一致:各预设实例分别维护 此前在 rc.1 上做过一个本地候选修复:替换安装等待已离开预设的整个 fiber 清理完成,等待后排除已经销毁的安装回调,预设卸载也等待已开始的清理。候选通过了类型构建和该包 130 项合成测试(含新增双向切换、快速往返、延迟清理、卸载重挂回归);该候选未应用到本次复测的官方 rc.2 安装,也不是已提交或已合并的上游修复。 不建议只在各预设自己的 |
|
在 0.2.0-rc.2 上实测了同一条,来交叉引用一下:切换后的会话拿到 13 个工具( 你那个 完整的代码定位写在 #6751 的这条评论里:#6751 (comment) 那个 in-process 复现脚本是这几条报告里唯一不依赖 GUI 的,维护者要定位的话应该会先用它,多谢留下。 |
Uh oh!
There was an error while loading. Please reload this page.
环境
0.1.5-rc.1183f08e9c6dde7e36cd2318eaee70b0da08fb35e24.20.0standard、ptc预设复现条件
在同一个 Host 中,目标预设的加载顺序早于来源预设。
已通过真实 Cordis/AgentLoop/AgentPresets 的隔离环境验证以下原生 API 顺序:
agentPresets.standingKeyFor('ptc'),加载目标预设。standard的空白 Agent,尚未发送消息。agentPresets.select(agent, 'ptc')。tools.schemas(agent)。实际结果
切换前存在:
切换后只剩:
同时日志出现:
会话保存的
subagentModelSelectionPolicy仍然存在,但模型选择和创建子代理的工具消失。两个独立空白 Agent 均复现,验证过程没有模型请求。对照结果
standard、再首次加载并切到ptc:正常。ptc的会话,不经过切换:正常。ptc会话:仍正常。预期结果
空白会话切换预设后,应保留新预设应提供的完整工具。若重新注册失败,应明确报错或恢复一致状态,而不是切换成功但静默丢失工具。
可能原因
tool-subagent的各预设实例独立监听tools/change,分别安装或移除会话级工具。当目标预设的监听器先执行时,旧工具尚未注销,新注册发生重名冲突;随后来源预设清理旧工具,导致两者都不存在。失败的安装 fiber 又可能仍被
scopedInstalls当作已安装,阻止重试。这是根据报错、对照实验及相关注册逻辑提出的原因分析,供维护者进一步确认。
临时规避
让新会话直接使用目标预设创建,避免先用默认预设创建空白会话再切换。
All reactions