From efa17d64ed096b818cfbfe7555467efe9c317fc0 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 14 Nov 2025 13:37:22 +0900 Subject: [PATCH] fix: Omit tools parameter when prompt ID is set but tools in the agent is absent --- .changeset/plain-breads-enjoy.md | 6 ++ packages/agents-core/src/agent.ts | 6 ++ packages/agents-core/src/model.ts | 7 ++ packages/agents-core/src/run.ts | 4 + .../agents-openai/src/openaiResponsesModel.ts | 7 +- .../test/openaiResponsesModel.test.ts | 92 +++++++++++++++++++ 6 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 .changeset/plain-breads-enjoy.md diff --git a/.changeset/plain-breads-enjoy.md b/.changeset/plain-breads-enjoy.md new file mode 100644 index 00000000..4bfcfe2c --- /dev/null +++ b/.changeset/plain-breads-enjoy.md @@ -0,0 +1,6 @@ +--- +'@openai/agents-openai': patch +'@openai/agents-core': patch +--- + +fix: Omit tools parameter when prompt ID is set but tools in the agent is absent diff --git a/packages/agents-core/src/agent.ts b/packages/agents-core/src/agent.ts index 75e64f16..2139b635 100644 --- a/packages/agents-core/src/agent.ts +++ b/packages/agents-core/src/agent.ts @@ -382,6 +382,7 @@ export class Agent< outputType: TOutput = 'text' as TOutput; toolUseBehavior: ToolUseBehavior; resetToolChoice: boolean; + private readonly _toolsExplicitlyConfigured: boolean; constructor(config: AgentOptions) { super(); @@ -396,6 +397,7 @@ export class Agent< this.model = config.model ?? ''; this.modelSettings = config.modelSettings ?? getDefaultModelSettings(); this.tools = config.tools ?? []; + this._toolsExplicitlyConfigured = config.tools !== undefined; this.mcpServers = config.mcpServers ?? []; this.inputGuardrails = config.inputGuardrails ?? []; this.outputGuardrails = config.outputGuardrails ?? []; @@ -679,6 +681,10 @@ export class Agent< return [...mcpTools, ...enabledTools]; } + hasExplicitToolConfig(): boolean { + return this._toolsExplicitlyConfigured; + } + /** * Returns the handoffs that should be exposed to the model for the current run. * diff --git a/packages/agents-core/src/model.ts b/packages/agents-core/src/model.ts index 97ddb6f3..d2e96054 100644 --- a/packages/agents-core/src/model.ts +++ b/packages/agents-core/src/model.ts @@ -263,6 +263,13 @@ export type ModelRequest = { */ tools: SerializedTool[]; + /** + * When true, the caller explicitly configured the tools list (even if empty). + * Providers can use this to avoid overwriting prompt-defined tools when an agent + * does not specify its own tools. + */ + toolsExplicitlyProvided?: boolean; + /** * The type of the output to use for the model. */ diff --git a/packages/agents-core/src/run.ts b/packages/agents-core/src/run.ts index 6101fed3..fdc5645b 100644 --- a/packages/agents-core/src/run.ts +++ b/packages/agents-core/src/run.ts @@ -779,6 +779,7 @@ export class Runner extends RunHooks> { conversationId: preparedCall.conversationId, modelSettings: preparedCall.modelSettings, tools: preparedCall.serializedTools, + toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided, outputType: convertAgentOutputTypeToSerializable( state._currentAgent.outputType, ), @@ -1052,6 +1053,7 @@ export class Runner extends RunHooks> { conversationId: preparedCall.conversationId, modelSettings: preparedCall.modelSettings, tools: preparedCall.serializedTools, + toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided, handoffs: preparedCall.serializedHandoffs, outputType: convertAgentOutputTypeToSerializable( currentAgent.outputType, @@ -1946,6 +1948,7 @@ type AgentArtifacts = { tools: Tool[]; serializedHandoffs: SerializedHandoff[]; serializedTools: SerializedTool[]; + toolsExplicitlyProvided: boolean; }; /** @@ -1980,6 +1983,7 @@ async function prepareAgentArtifacts< tools, serializedHandoffs: handoffs.map((handoff) => serializeHandoff(handoff)), serializedTools: tools.map((tool) => serializeTool(tool)), + toolsExplicitlyProvided: state._currentAgent.hasExplicitToolConfig(), }; } diff --git a/packages/agents-openai/src/openaiResponsesModel.ts b/packages/agents-openai/src/openaiResponsesModel.ts index 943f8fc1..f63acb12 100644 --- a/packages/agents-openai/src/openaiResponsesModel.ts +++ b/packages/agents-openai/src/openaiResponsesModel.ts @@ -1599,12 +1599,17 @@ export class OpenAIResponsesModel implements Model { const shouldSendModel = !request.prompt || request.overridePromptModel === true; + const shouldSendTools = + tools.length > 0 || + request.toolsExplicitlyProvided === true || + !request.prompt; + const requestData = { ...(shouldSendModel ? { model: this.#model } : {}), instructions: normalizeInstructions(request.systemInstructions), input, include, - tools, + ...(shouldSendTools ? { tools } : {}), previous_response_id: request.previousResponseId, conversation: request.conversationId, prompt, diff --git a/packages/agents-openai/test/openaiResponsesModel.test.ts b/packages/agents-openai/test/openaiResponsesModel.test.ts index bc6beb82..bf4a317a 100644 --- a/packages/agents-openai/test/openaiResponsesModel.test.ts +++ b/packages/agents-openai/test/openaiResponsesModel.test.ts @@ -74,6 +74,36 @@ describe('OpenAIResponsesModel', () => { }); }); + it('still sends an empty tools array when no prompt is provided', async () => { + await withTrace('test', async () => { + const fakeResponse = { id: 'res-no-prompt', usage: {}, output: [] }; + const createMock = vi.fn().mockResolvedValue(fakeResponse); + const fakeClient = { + responses: { create: createMock }, + } as unknown as OpenAI; + const model = new OpenAIResponsesModel(fakeClient, 'gpt-default'); + + const request = { + systemInstructions: undefined, + input: 'hello', + modelSettings: {}, + tools: [], + toolsExplicitlyProvided: false, + outputType: 'text', + handoffs: [], + tracing: false, + signal: undefined, + }; + + await model.getResponse(request as any); + + expect(createMock).toHaveBeenCalledTimes(1); + const [args] = createMock.mock.calls[0]; + expect(args.tools).toEqual([]); + expect(args.prompt).toBeUndefined(); + }); + }); + it('omits model when a prompt is provided', async () => { await withTrace('test', async () => { const fakeResponse = { id: 'res-prompt', usage: {}, output: [] }; @@ -135,6 +165,68 @@ describe('OpenAIResponsesModel', () => { }); }); + it('omits tools when agent did not configure any and prompt should supply them', async () => { + await withTrace('test', async () => { + const fakeResponse = { id: 'res-no-tools', usage: {}, output: [] }; + const createMock = vi.fn().mockResolvedValue(fakeResponse); + const fakeClient = { + responses: { create: createMock }, + } as unknown as OpenAI; + const model = new OpenAIResponsesModel(fakeClient, 'gpt-default'); + + const request = { + systemInstructions: undefined, + prompt: { promptId: 'pmpt_789' }, + input: 'hello', + modelSettings: {}, + tools: [], + toolsExplicitlyProvided: false, + outputType: 'text', + handoffs: [], + tracing: false, + signal: undefined, + }; + + await model.getResponse(request as any); + + expect(createMock).toHaveBeenCalledTimes(1); + const [args] = createMock.mock.calls[0]; + expect('tools' in args).toBe(false); + expect(args.prompt).toMatchObject({ id: 'pmpt_789' }); + }); + }); + + it('sends an explicit empty tools array when the agent intentionally disabled tools', async () => { + await withTrace('test', async () => { + const fakeResponse = { id: 'res-empty-tools', usage: {}, output: [] }; + const createMock = vi.fn().mockResolvedValue(fakeResponse); + const fakeClient = { + responses: { create: createMock }, + } as unknown as OpenAI; + const model = new OpenAIResponsesModel(fakeClient, 'gpt-default'); + + const request = { + systemInstructions: undefined, + prompt: { promptId: 'pmpt_999' }, + input: 'hello', + modelSettings: {}, + tools: [], + toolsExplicitlyProvided: true, + outputType: 'text', + handoffs: [], + tracing: false, + signal: undefined, + }; + + await model.getResponse(request as any); + + expect(createMock).toHaveBeenCalledTimes(1); + const [args] = createMock.mock.calls[0]; + expect(args.tools).toEqual([]); + expect(args.prompt).toMatchObject({ id: 'pmpt_999' }); + }); + }); + it('normalizes systemInstructions so empty strings are omitted', async () => { await withTrace('test', async () => { const fakeResponse = {