mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(subagent): tolerate policy-only preset states
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user