Skip to content

Commit 0a808d2

Browse files
authored
fix: Omit tools parameter when prompt ID is set but tools in the agent is absent (#658)
1 parent 184e5d0 commit 0a808d2

File tree

6 files changed

+121
-1
lines changed

6 files changed

+121
-1
lines changed

.changeset/plain-breads-enjoy.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@openai/agents-openai': patch
3+
'@openai/agents-core': patch
4+
---
5+
6+
fix: Omit tools parameter when prompt ID is set but tools in the agent is absent

packages/agents-core/src/agent.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ export class Agent<
382382
outputType: TOutput = 'text' as TOutput;
383383
toolUseBehavior: ToolUseBehavior;
384384
resetToolChoice: boolean;
385+
private readonly _toolsExplicitlyConfigured: boolean;
385386

386387
constructor(config: AgentOptions<TContext, TOutput>) {
387388
super();
@@ -396,6 +397,7 @@ export class Agent<
396397
this.model = config.model ?? '';
397398
this.modelSettings = config.modelSettings ?? getDefaultModelSettings();
398399
this.tools = config.tools ?? [];
400+
this._toolsExplicitlyConfigured = config.tools !== undefined;
399401
this.mcpServers = config.mcpServers ?? [];
400402
this.inputGuardrails = config.inputGuardrails ?? [];
401403
this.outputGuardrails = config.outputGuardrails ?? [];
@@ -679,6 +681,10 @@ export class Agent<
679681
return [...mcpTools, ...enabledTools];
680682
}
681683

684+
hasExplicitToolConfig(): boolean {
685+
return this._toolsExplicitlyConfigured;
686+
}
687+
682688
/**
683689
* Returns the handoffs that should be exposed to the model for the current run.
684690
*

packages/agents-core/src/model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,13 @@ export type ModelRequest = {
264264
*/
265265
tools: SerializedTool[];
266266

267+
/**
268+
* When true, the caller explicitly configured the tools list (even if empty).
269+
* Providers can use this to avoid overwriting prompt-defined tools when an agent
270+
* does not specify its own tools.
271+
*/
272+
toolsExplicitlyProvided?: boolean;
273+
267274
/**
268275
* The type of the output to use for the model.
269276
*/

packages/agents-core/src/run.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
779779
conversationId: preparedCall.conversationId,
780780
modelSettings: preparedCall.modelSettings,
781781
tools: preparedCall.serializedTools,
782+
toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided,
782783
outputType: convertAgentOutputTypeToSerializable(
783784
state._currentAgent.outputType,
784785
),
@@ -1052,6 +1053,7 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
10521053
conversationId: preparedCall.conversationId,
10531054
modelSettings: preparedCall.modelSettings,
10541055
tools: preparedCall.serializedTools,
1056+
toolsExplicitlyProvided: preparedCall.toolsExplicitlyProvided,
10551057
handoffs: preparedCall.serializedHandoffs,
10561058
outputType: convertAgentOutputTypeToSerializable(
10571059
currentAgent.outputType,
@@ -1946,6 +1948,7 @@ type AgentArtifacts<TContext = unknown> = {
19461948
tools: Tool<TContext>[];
19471949
serializedHandoffs: SerializedHandoff[];
19481950
serializedTools: SerializedTool[];
1951+
toolsExplicitlyProvided: boolean;
19491952
};
19501953

19511954
/**
@@ -1980,6 +1983,7 @@ async function prepareAgentArtifacts<
19801983
tools,
19811984
serializedHandoffs: handoffs.map((handoff) => serializeHandoff(handoff)),
19821985
serializedTools: tools.map((tool) => serializeTool(tool)),
1986+
toolsExplicitlyProvided: state._currentAgent.hasExplicitToolConfig(),
19831987
};
19841988
}
19851989

packages/agents-openai/src/openaiResponsesModel.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1599,12 +1599,17 @@ export class OpenAIResponsesModel implements Model {
15991599
const shouldSendModel =
16001600
!request.prompt || request.overridePromptModel === true;
16011601

1602+
const shouldSendTools =
1603+
tools.length > 0 ||
1604+
request.toolsExplicitlyProvided === true ||
1605+
!request.prompt;
1606+
16021607
const requestData = {
16031608
...(shouldSendModel ? { model: this.#model } : {}),
16041609
instructions: normalizeInstructions(request.systemInstructions),
16051610
input,
16061611
include,
1607-
tools,
1612+
...(shouldSendTools ? { tools } : {}),
16081613
previous_response_id: request.previousResponseId,
16091614
conversation: request.conversationId,
16101615
prompt,

packages/agents-openai/test/openaiResponsesModel.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,36 @@ describe('OpenAIResponsesModel', () => {
7474
});
7575
});
7676

77+
it('still sends an empty tools array when no prompt is provided', async () => {
78+
await withTrace('test', async () => {
79+
const fakeResponse = { id: 'res-no-prompt', usage: {}, output: [] };
80+
const createMock = vi.fn().mockResolvedValue(fakeResponse);
81+
const fakeClient = {
82+
responses: { create: createMock },
83+
} as unknown as OpenAI;
84+
const model = new OpenAIResponsesModel(fakeClient, 'gpt-default');
85+
86+
const request = {
87+
systemInstructions: undefined,
88+
input: 'hello',
89+
modelSettings: {},
90+
tools: [],
91+
toolsExplicitlyProvided: false,
92+
outputType: 'text',
93+
handoffs: [],
94+
tracing: false,
95+
signal: undefined,
96+
};
97+
98+
await model.getResponse(request as any);
99+
100+
expect(createMock).toHaveBeenCalledTimes(1);
101+
const [args] = createMock.mock.calls[0];
102+
expect(args.tools).toEqual([]);
103+
expect(args.prompt).toBeUndefined();
104+
});
105+
});
106+
77107
it('omits model when a prompt is provided', async () => {
78108
await withTrace('test', async () => {
79109
const fakeResponse = { id: 'res-prompt', usage: {}, output: [] };
@@ -135,6 +165,68 @@ describe('OpenAIResponsesModel', () => {
135165
});
136166
});
137167

168+
it('omits tools when agent did not configure any and prompt should supply them', async () => {
169+
await withTrace('test', async () => {
170+
const fakeResponse = { id: 'res-no-tools', usage: {}, output: [] };
171+
const createMock = vi.fn().mockResolvedValue(fakeResponse);
172+
const fakeClient = {
173+
responses: { create: createMock },
174+
} as unknown as OpenAI;
175+
const model = new OpenAIResponsesModel(fakeClient, 'gpt-default');
176+
177+
const request = {
178+
systemInstructions: undefined,
179+
prompt: { promptId: 'pmpt_789' },
180+
input: 'hello',
181+
modelSettings: {},
182+
tools: [],
183+
toolsExplicitlyProvided: false,
184+
outputType: 'text',
185+
handoffs: [],
186+
tracing: false,
187+
signal: undefined,
188+
};
189+
190+
await model.getResponse(request as any);
191+
192+
expect(createMock).toHaveBeenCalledTimes(1);
193+
const [args] = createMock.mock.calls[0];
194+
expect('tools' in args).toBe(false);
195+
expect(args.prompt).toMatchObject({ id: 'pmpt_789' });
196+
});
197+
});
198+
199+
it('sends an explicit empty tools array when the agent intentionally disabled tools', async () => {
200+
await withTrace('test', async () => {
201+
const fakeResponse = { id: 'res-empty-tools', usage: {}, output: [] };
202+
const createMock = vi.fn().mockResolvedValue(fakeResponse);
203+
const fakeClient = {
204+
responses: { create: createMock },
205+
} as unknown as OpenAI;
206+
const model = new OpenAIResponsesModel(fakeClient, 'gpt-default');
207+
208+
const request = {
209+
systemInstructions: undefined,
210+
prompt: { promptId: 'pmpt_999' },
211+
input: 'hello',
212+
modelSettings: {},
213+
tools: [],
214+
toolsExplicitlyProvided: true,
215+
outputType: 'text',
216+
handoffs: [],
217+
tracing: false,
218+
signal: undefined,
219+
};
220+
221+
await model.getResponse(request as any);
222+
223+
expect(createMock).toHaveBeenCalledTimes(1);
224+
const [args] = createMock.mock.calls[0];
225+
expect(args.tools).toEqual([]);
226+
expect(args.prompt).toMatchObject({ id: 'pmpt_999' });
227+
});
228+
});
229+
138230
it('normalizes systemInstructions so empty strings are omitted', async () => {
139231
await withTrace('test', async () => {
140232
const fakeResponse = {

0 commit comments

Comments
 (0)