Skip to content

Commit 6cc01be

Browse files
OlegHQseratch
andauthored
fix(agents-core): handle invalid JSON in tool call arguments gracefully (#887)
Co-authored-by: Kazuhiro Sera <[email protected]>
1 parent a6008b4 commit 6cc01be

3 files changed

Lines changed: 92 additions & 10 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openai/agents-core': patch
3+
---
4+
5+
fix: #723 handle invalid JSON in tool call arguments gracefully to prevent agent crashes

packages/agents-core/src/runner/toolExecution.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ type FunctionToolCallDeps<TContext = UnknownContext> = {
5656

5757
const TOOL_APPROVAL_REJECTION_MESSAGE = 'Tool execution was not approved.';
5858

59+
type ParseToolArgumentsResult =
60+
| { success: true; args: any }
61+
| { success: false; error: Error };
62+
5963
/**
6064
* @internal
6165
* Normalizes tool outputs once so downstream code works with fully structured protocol items.
@@ -109,11 +113,17 @@ export async function executeFunctionToolCalls<TContext = UnknownContext>(
109113
try {
110114
const results = await Promise.all(
111115
toolRuns.map(async (toolRun) => {
112-
const parsedArgs = parseToolArguments(toolRun);
116+
const parseResult = parseToolArguments(toolRun);
117+
118+
// Handle parse errors gracefully instead of crashing
119+
if (!parseResult.success) {
120+
return buildParseErrorResult(deps, toolRun, parseResult.error);
121+
}
122+
113123
const approvalOutcome = await handleFunctionApproval(
114124
deps,
115125
toolRun,
116-
parsedArgs,
126+
parseResult.args,
117127
);
118128
if (approvalOutcome !== 'approved') {
119129
return approvalOutcome;
@@ -131,16 +141,25 @@ export async function executeFunctionToolCalls<TContext = UnknownContext>(
131141
}
132142
}
133143

134-
function parseToolArguments<TContext>(toolRun: ToolRunFunction<TContext>) {
135-
let parsedArgs: any = toolRun.toolCall.arguments;
136-
if (toolRun.tool.parameters) {
137-
if (isZodObject(toolRun.tool.parameters)) {
138-
parsedArgs = toolRun.tool.parameters.parse(parsedArgs);
139-
} else {
140-
parsedArgs = JSON.parse(parsedArgs);
144+
function parseToolArguments<TContext>(
145+
toolRun: ToolRunFunction<TContext>,
146+
): ParseToolArgumentsResult {
147+
try {
148+
let parsedArgs: any = toolRun.toolCall.arguments;
149+
if (toolRun.tool.parameters) {
150+
if (isZodObject(toolRun.tool.parameters)) {
151+
parsedArgs = toolRun.tool.parameters.parse(parsedArgs);
152+
} else {
153+
parsedArgs = JSON.parse(parsedArgs);
154+
}
141155
}
156+
return { success: true, args: parsedArgs };
157+
} catch (error) {
158+
logger.debug(
159+
`Failed to parse tool arguments for ${toolRun.tool.name}: ${error}`,
160+
);
161+
return { success: false, error: error as Error };
142162
}
143-
return parsedArgs;
144163
}
145164

146165
function buildApprovalRequestResult<TContext>(
@@ -154,6 +173,24 @@ function buildApprovalRequestResult<TContext>(
154173
};
155174
}
156175

176+
function buildParseErrorResult<TContext>(
177+
deps: FunctionToolCallDeps<TContext>,
178+
toolRun: ToolRunFunction<TContext>,
179+
error: Error,
180+
): FunctionToolResult<TContext> {
181+
const errorMessage = `An error occurred while parsing tool arguments. Please try again with valid JSON. Error: ${error.message}`;
182+
return {
183+
type: 'function_output',
184+
tool: toolRun.tool,
185+
output: errorMessage,
186+
runItem: new RunToolCallOutputItem(
187+
getToolCallOutputItem(toolRun.toolCall, errorMessage),
188+
deps.agent,
189+
errorMessage,
190+
),
191+
};
192+
}
193+
157194
async function buildApprovalRejectionResult<TContext>(
158195
deps: FunctionToolCallDeps<TContext>,
159196
toolRun: ToolRunFunction<TContext>,

packages/agents-core/test/runner/toolExecution.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,46 @@ describe('executeShellActions', () => {
12841284
expect(firstResult.agentRunResult).toBe(nestedRunResult);
12851285
expect(firstResult.interruptions).toEqual([approval]);
12861286
});
1287+
1288+
it('handles invalid JSON in tool call arguments gracefully instead of crashing', async () => {
1289+
// Reproduces issue #723: SyntaxError stops agent when LLM generates invalid JSON
1290+
const t = tool({
1291+
name: 'checkTagActivity',
1292+
description: 'Check tag activity',
1293+
parameters: z.object({
1294+
tagIds: z.array(z.string()),
1295+
since: z.string(),
1296+
}),
1297+
execute: vi.fn(async () => 'success'),
1298+
}) as unknown as FunctionTool;
1299+
1300+
const invalidToolCall = {
1301+
...toolCall,
1302+
name: 'checkTagActivity',
1303+
arguments:
1304+
'{"{"tagIds":["65aafb7e-4293-4376-baf6-1f9d197e960a"],"since":"2025-09-04T13:26:13.991Z"}',
1305+
};
1306+
1307+
const res = await withTrace('test', () =>
1308+
executeFunctionToolCalls(
1309+
state._currentAgent,
1310+
[{ toolCall: invalidToolCall, tool: t }],
1311+
runner,
1312+
state,
1313+
),
1314+
);
1315+
1316+
expect(res).toHaveLength(1);
1317+
const firstResult = res[0];
1318+
1319+
expect(firstResult.type).toBe('function_output');
1320+
if (firstResult.type === 'function_output') {
1321+
expect(String(firstResult.output)).toContain(
1322+
'An error occurred while parsing tool arguments',
1323+
);
1324+
expect(String(firstResult.output)).toContain('valid JSON');
1325+
}
1326+
});
12871327
});
12881328

12891329
describe('executeComputerActions', () => {

0 commit comments

Comments
 (0)