fix(subagent): tolerate policy-only preset states

This commit is contained in:
Dudu-0223
2026-08-27 13:25:51 +08:00
parent a7b054b8f6
commit 2722c202ad
2 changed files with 43 additions and 14 deletions
@@ -15,20 +15,22 @@ export const name = 'tool-subagent-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** Assert that a durable opt-in is represented by both model-facing definitions. */
/** Assert that model-selectable definitions are complete and reconstructable. */
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
ctx.on('agent/pre-step', async ({ agent }, next) => {
if (subagentModelSelectionPolicy(agent.session) !== undefined) {
const schemas = ctx.tools.schemas(agent)
const selectable = schemas.some((schema) => {
const properties = (schema.parameters as { properties?: Record<string, unknown> }).properties
return properties?.['provider'] !== undefined
&& properties['model'] !== undefined
&& properties['reasoning_effort'] !== undefined
})
if (!selectable || !schemas.some(schema => schema.name === 'list_subagent_models')) {
fail('a subagent/model-selection-policy session must expose route fields and list_subagent_models')
}
const schemas = ctx.tools.schemas(agent)
const selectable = schemas.some((schema) => {
const properties = (schema.parameters as { properties?: Record<string, unknown> }).properties
return properties?.['provider'] !== undefined
&& properties['model'] !== undefined
&& properties['reasoning_effort'] !== undefined
})
const discoverable = schemas.some(schema => schema.name === 'list_subagent_models')
if (
(selectable || discoverable)
&& (subagentModelSelectionPolicy(agent.session) === undefined || !selectable || !discoverable)
) {
fail('model-selectable subagent definitions require a durable policy, route fields, and list_subagent_models')
}
return next()
}, { global: true })
@@ -189,6 +189,8 @@ describe('SubagentModelSelectionConfig', () => {
it('installs per-Agent definitions for a shared preset scope', async () => {
const ctx = await boot()
await ctx.plugin(InvariantRegistry, { enabled: true })
await ctx.plugin(ToolInvariant)
const preset = createScope(ctx, { preset: 'standard' })
const other = createScope(ctx, { preset: 'minimal' })
await preset.ctx.plugin(tool, {
@@ -219,9 +221,22 @@ describe('SubagentModelSelectionConfig', () => {
enabledBinding!.rebind(scopeOf(other.ctx)!)
ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
await vi.waitFor(() => { expect(selectable(ctx, enabled.agent)).toBe(false) })
const next = () => Promise.resolve({ kind: 'enter' as const, messages: [] })
const payload = {
agent: enabled.agent,
messages: [],
turn: 1,
step: 1,
signal: new AbortController().signal,
}
await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next))
.resolves.toEqual({ kind: 'enter', messages: [] })
enabledBinding!.rebind(scopeOf(preset.ctx)!)
ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
await vi.waitFor(() => { expect(selectable(ctx, enabled.agent)).toBe(true) })
await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next))
.resolves.toEqual({ kind: 'enter', messages: [] })
await enabled.dispose()
ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
@@ -322,7 +337,7 @@ describe('SubagentModelSelectionConfig', () => {
await withoutAgent.fiber.dispose()
})
it('checks the durable decision against the published tool definitions', async () => {
it('checks model-selectable definitions without rejecting a policy-only preset', async () => {
const ctx = await boot()
await ctx.plugin(InvariantRegistry, { enabled: true })
await ctx.plugin(ToolInvariant)
@@ -341,7 +356,7 @@ describe('SubagentModelSelectionConfig', () => {
disabled.session.append('subagent/model-selection-policy', { allowedModels: ALLOWED_MODELS })
await expect(ctx.waterfall(ctx as never, 'agent/pre-step', payload, next))
.rejects.toThrow('must expose route fields and list_subagent_models')
.resolves.toEqual({ kind: 'enter', messages: [] })
await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
enabled: true,
@@ -350,6 +365,18 @@ describe('SubagentModelSelectionConfig', () => {
const enabled = await createAgent(ctx, 'invariant-enabled')
await expect(ctx.waterfall(ctx as never, 'agent/pre-step', { ...payload, agent: enabled }, next))
.resolves.toEqual({ kind: 'enter', messages: [] })
const enabledSchemas = ctx.tools.schemas(enabled)
const schemas = vi.spyOn(ctx.tools, 'schemas')
schemas.mockReturnValue(enabledSchemas.filter(schema => schema.name !== 'list_subagent_models'))
await expect(ctx.waterfall(ctx as never, 'agent/pre-step', { ...payload, agent: enabled }, next))
.rejects.toThrow('require a durable policy, route fields, and list_subagent_models')
schemas.mockReturnValue(enabledSchemas)
await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false })
const withoutPolicy = await createAgent(ctx, 'invariant-without-policy')
await expect(ctx.waterfall(ctx as never, 'agent/pre-step', { ...payload, agent: withoutPolicy }, next))
.rejects.toThrow('require a durable policy, route fields, and list_subagent_models')
await ctx.fiber.dispose()
})
})