diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index f677076d3f..31d6dce113 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -93,9 +93,13 @@ export interface Config { export const Config: z = z.object({ provider: z.string().required(), toolName: z.string().default('subagent'), + // Omitted-object discipline (see the toolFilter note below): without the + // forced default an omitted `agentOptions` materializes `{}`, which reads as + // present — the request would carry `agentOptions: {}` and the presence + // check in execute() could never be false through config. agentOptions: z.object({ model: z.string(), - }), + }).default(undefined as unknown as { model: string }), persona: z.string(), // A schemastery object materializes {} (with [] for nested arrays) when the // key is omitted — for toolFilter that would mean an EMPTY ALLOW-LIST, i.e. @@ -229,7 +233,7 @@ export function apply(ctx: Context, config: Config): void { prompt: [{ type: 'text', text: args.prompt }], parent, ...exec.signal ? { signal: exec.signal } : {}, - ...config.agentOptions ? { agentOptions: config.agentOptions } : {}, + ...config.agentOptions !== undefined ? { agentOptions: config.agentOptions } : {}, ...config.persona !== undefined ? { persona: config.persona } : {}, ...config.toolFilter !== undefined ? { toolFilter: config.toolFilter } : {}, ...config.maxDepth !== undefined ? { maxDepth: config.maxDepth } : {}, diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index da560350e4..9d9941fdff 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -511,6 +511,36 @@ describe('dsh-tool-subagent', () => { expect(seen?.toolFilter).not.toHaveProperty('allow') }) + it('an omitted agentOptions does not materialize an empty object onto the request', async () => { + // Same schemastery trap as toolFilter, adjacent field: an omitted + // `agentOptions` config key materializes `{}` without the forced default, + // which reads as present and puts a dishonest `agentOptions: {}` on every + // start request. + let seen: { agentOptions?: unknown } | undefined + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(SubagentService) + ctx.subagents.registerProvider({ + name: 'capture4', + capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + start: (request) => { + seen = request + return { + id: AgentId('capture4-child'), + result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }), + cancel() {}, + dispose: async () => {}, + } + }, + }) + await ctx.plugin(tool, { provider: 'capture4' }) + await callSubagent(ctx, { description: 'd', prompt: 'p' }) + expect(seen).toBeDefined() + expect(seen).not.toHaveProperty('agentOptions') + }) + it('an explicit empty toolFilter fails at plugin load, not at first delegation', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt)