fix(subagent): initialize model selection before agent publication
This commit is contained in:
1 parent
b361989a45
commit
feff25e8f7
5 files changed
+78
-6
No files matched your search
@@ -27,8 +27,8 @@
|
||||
en: 1950ef92bab0f46e
|
||||
zh: 593f5f64ed9b5631
|
||||
/deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-llm:
|
||||
en: 628ec29adbe175e6
|
||||
zh: 82df946ca7eef167
|
||||
en: 5cf892e4acab3f38
|
||||
zh: c282b79620bc3de5
|
||||
/deepseek-ai-dsh-tool-subagent/understand-the-implementation:
|
||||
en: 6ce5f749256a7e6c
|
||||
zh: a4f2adb04a70d9cf
|
||||
|
||||
@@ -74,7 +74,7 @@ Compose persona and tools in the [preset plugin list](../../preset/agent-preset-
|
||||
|
||||
### Selecting a child LLM
|
||||
|
||||
Set `modelSelection: true` to expose optional `provider`, `model`, and `reasoning_effort` fields; instances without it reject selection fields. Set `modelDiscovery: true` on one instance per tool scope to register `list_subagent_models`; it defaults off independently of `modelSelection`. A second discovery owner in the same scope fails to register. Discovery follows its owning plugin instance, remains available when its delegation provider is absent, and is removed when that instance unloads. Web and Desktop presets enable discovery and route fields on spawn and leave both disabled on fork. The Host’s `/model-selection-settings` service samples the user preference during Agent creation, independently of tool installation. When enabled, its exact route list is recorded in each fresh top-level Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. Route fields require a backend that advertises `agentOptions`; in-process Spawn and Fork and DSH SDK qualify, while ACP, Codex, and Claude Code fail the mount.
|
||||
Set `modelSelection: true` to expose optional `provider`, `model`, and `reasoning_effort` fields; instances without it reject selection fields. Set `modelDiscovery: true` on one instance per tool scope to register `list_subagent_models`; it defaults off independently of `modelSelection`. A second discovery owner in the same scope fails to register. Discovery follows its owning plugin instance, remains available when its delegation provider is absent, and is removed when that instance unloads. Web and Desktop presets enable discovery and route fields on spawn and leave both disabled on fork. The Host’s `/model-selection-settings` service samples the user preference during Agent creation, independently of tool installation. Its creation listener subscribes before projection registration completes and awaits the handle; registration failure or owner disposal rejects Agent publication. When enabled, its exact route list is recorded in each fresh top-level Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. Route fields require a backend that advertises `agentOptions`; in-process Spawn and Fork and DSH SDK qualify, while ACP, Codex, and Claude Code fail the mount.
|
||||
|
||||
A call supplies `provider` and `model` together, or supplies only an effort when configured, parent, or provider-owned defaults provide the route. Static `provider.agentRouteDefaults`, when present, form the provider/model baseline; tool configuration and model fields overlay it before route-aware effort merging and exact-route preflight. Providers without these defaults use compatible values from the parent's latest logged request, then the parent's creation options before its first request, while retaining the configured `maxTokens`. Changing the route without an explicit effort clears the inherited route-owned effort, so the selected model resolves its default. The live LLM adapter validates the effective route before child creation. Catalog membership remains advisory, so a model can use an unlisted id when its adapter accepts it.
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ kind: "package-reference"
|
||||
|
||||
### 选择子级 LLM
|
||||
|
||||
设置 `modelSelection: true` 后,该实例公开可选的 `provider`、`model` 和 `reasoning_effort` 字段;未设置的实例拒绝选择字段。在每个工具作用域的一个实例上设置 `modelDiscovery: true`,以注册 `list_subagent_models`;该选项独立于 `modelSelection`,默认关闭。同一作用域的第二个发现工具所有者会注册失败。发现工具随其所属插件实例存续,在委派提供方缺席时仍然可用,并在该实例卸载时移除。Web 与 Desktop preset 为 spawn 启用发现和路由字段,为 fork 保持两者关闭。Host 的 `/model-selection-settings` 服务在 Agent 创建时读取用户偏好,与工具安装无关。启用时,精确路由列表记录在每个新顶层 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。路由字段要求后端公布 `agentOptions`;进程内 Spawn、Fork 和 DSH SDK 满足条件,ACP、Codex 和 Claude Code 会使挂载失败。
|
||||
设置 `modelSelection: true` 后,该实例公开可选的 `provider`、`model` 和 `reasoning_effort` 字段;未设置的实例拒绝选择字段。在每个工具作用域的一个实例上设置 `modelDiscovery: true`,以注册 `list_subagent_models`;该选项独立于 `modelSelection`,默认关闭。同一作用域的第二个发现工具所有者会注册失败。发现工具随其所属插件实例存续,在委派提供方缺席时仍然可用,并在该实例卸载时移除。Web 与 Desktop preset 为 spawn 启用发现和路由字段,为 fork 保持两者关闭。Host 的 `/model-selection-settings` 服务在 Agent 创建时读取用户偏好,与工具安装无关。创建监听器在投影注册完成前订阅并等待句柄;注册失败或所属插件卸载会使 Agent 发布失败。启用时,精确路由列表记录在每个新顶层 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。路由字段要求后端公布 `agentOptions`;进程内 Spawn、Fork 和 DSH SDK 满足条件,ACP、Codex 和 Claude Code 会使挂载失败。
|
||||
|
||||
一次调用需同时提供 `provider` 与 `model`;当配置值、父 agent 值或提供方持有的默认值能提供路由时,也可只提供推理等级。静态的 `provider.agentRouteDefaults` 在存在时构成提供方/模型基线;工具配置与模型字段会在路由相关强度合并和确切路由预检前覆盖它。没有这些默认值的提供方会使用父 agent 最新已记录请求中的兼容值,再使用父级首次请求前的创建选项,并保留配置的 `maxTokens`。更改路由但未显式提供推理等级时,会清除继承的路由自有等级,使所选模型解析自己的默认值。实时 LLM 适配器在创建子 agent 前校验有效路由。目录成员资格只提供建议,因此适配器接受时,模型可以使用未列出的 id。
|
||||
|
||||
|
||||
@@ -53,8 +53,10 @@ export class SubagentModelSelectionConfig extends Service {
|
||||
|
||||
protected async [Service.init](): Promise<void> {
|
||||
const ctx = this.ctx
|
||||
const policy = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
const policyReady = ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
|
||||
ctx.on('agent/created', async ({ agent }) => {
|
||||
const policy = await policyReady
|
||||
ctx.fiber.assertActive()
|
||||
const target = agent.session
|
||||
if (subagentModelSelectionPolicy(policy, target) !== undefined) return
|
||||
let allowedModels: AllowedModelRoute[] | undefined
|
||||
@@ -70,6 +72,7 @@ export class SubagentModelSelectionConfig extends Service {
|
||||
}
|
||||
if (allowedModels !== undefined) target.append('subagent/model-selection-policy', { allowedModels })
|
||||
})
|
||||
await policyReady
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/** Agent publication waits for the policy owner while its projection activates. */
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import SubagentModelSelectionConfig from '../src/model-selection-settings.ts'
|
||||
|
||||
const ALLOWED_MODELS = [{ provider: 'alpha', model: 'fast-model' }]
|
||||
|
||||
async function setup() {
|
||||
const ctx = new Context()
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
onTestFinished(async () => {
|
||||
release.resolve(undefined)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
await mountAgentLoopTestDependencies(ctx, { workingDirectory: true })
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
const register = ctx.sessionProjections.register.bind(ctx.sessionProjections)
|
||||
const spy = vi.spyOn(ctx.sessionProjections, 'register').mockImplementationOnce(async (definition) => {
|
||||
const handle = await register(definition)
|
||||
entered.resolve(undefined)
|
||||
await release.promise
|
||||
return handle
|
||||
})
|
||||
onTestFinished(() => { spy.mockRestore() })
|
||||
const announcing = Promise.withResolvers<undefined>()
|
||||
ctx.on('agent/created', () => { announcing.resolve(undefined) }, { prepend: true })
|
||||
const fiber = ctx.plugin(SubagentModelSelectionConfig, { enabled: true, allowedModels: ALLOWED_MODELS })
|
||||
await entered.promise
|
||||
return { ctx, fiber, release, announcing: announcing.promise }
|
||||
}
|
||||
|
||||
describe('SubagentModelSelectionConfig activation', () => {
|
||||
it('records the policy for an Agent announced while registration is pending', async () => {
|
||||
const { ctx, fiber, release, announcing } = await setup()
|
||||
const creation = ctx.agents.create({ sessionId: SessionId('during-policy-activation') })
|
||||
await announcing
|
||||
release.resolve(undefined)
|
||||
const { agent } = await creation
|
||||
await fiber
|
||||
|
||||
expect(agent.session.snapshotEvents().filter(event => event.type === 'subagent/model-selection-policy'))
|
||||
.toMatchObject([{ data: { allowedModels: ALLOWED_MODELS } }])
|
||||
})
|
||||
|
||||
it.each(['dispose', 'reject'] as const)('rejects Agent publication when policy activation ends with %s', async (ending) => {
|
||||
const { ctx, fiber, release, announcing } = await setup()
|
||||
const id = SessionId('failed-policy-activation')
|
||||
const creation = ctx.agents.create({ sessionId: id })
|
||||
const rejected = expect(creation).rejects.toThrow()
|
||||
await announcing
|
||||
if (ending === 'dispose') {
|
||||
const disposal = fiber.dispose()
|
||||
release.resolve(undefined)
|
||||
await disposal
|
||||
} else {
|
||||
const failed = expect(Promise.resolve(fiber)).rejects.toThrow('registration failed')
|
||||
release.reject(new Error('registration failed'))
|
||||
await failed
|
||||
}
|
||||
await rejected
|
||||
|
||||
expect(ctx.agents.get(id)).toBeUndefined()
|
||||
expect(ctx.sessions.get(id)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user