Skip to content
5 changes: 5 additions & 0 deletions .changeset/fix-invalid-json-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-core': patch
---

fix: #723 handle invalid JSON in tool call arguments gracefully to prevent agent crashes
57 changes: 47 additions & 10 deletions packages/agents-core/src/runner/toolExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ type FunctionToolCallDeps<TContext = UnknownContext> = {

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

type ParseToolArgumentsResult =
| { success: true; args: any }
| { success: false; error: Error };

/**
* @internal
* Normalizes tool outputs once so downstream code works with fully structured protocol items.
Expand Down Expand Up @@ -109,11 +113,17 @@ export async function executeFunctionToolCalls<TContext = UnknownContext>(
try {
const results = await Promise.all(
toolRuns.map(async (toolRun) => {
const parsedArgs = parseToolArguments(toolRun);
const parseResult = parseToolArguments(toolRun);

// Handle parse errors gracefully instead of crashing
if (!parseResult.success) {
return buildParseErrorResult(deps, toolRun, parseResult.error);
}

const approvalOutcome = await handleFunctionApproval(
deps,
toolRun,
parsedArgs,
parseResult.args,
);
if (approvalOutcome !== 'approved') {
return approvalOutcome;
Expand All @@ -131,16 +141,25 @@ export async function executeFunctionToolCalls<TContext = UnknownContext>(
}
}

function parseToolArguments<TContext>(toolRun: ToolRunFunction<TContext>) {
let parsedArgs: any = toolRun.toolCall.arguments;
if (toolRun.tool.parameters) {
if (isZodObject(toolRun.tool.parameters)) {
parsedArgs = toolRun.tool.parameters.parse(parsedArgs);
} else {
parsedArgs = JSON.parse(parsedArgs);
function parseToolArguments<TContext>(
toolRun: ToolRunFunction<TContext>,
): ParseToolArgumentsResult {
try {
let parsedArgs: any = toolRun.toolCall.arguments;
if (toolRun.tool.parameters) {
if (isZodObject(toolRun.tool.parameters)) {
parsedArgs = toolRun.tool.parameters.parse(parsedArgs);
} else {
parsedArgs = JSON.parse(parsedArgs);
}
}
return { success: true, args: parsedArgs };
} catch (error) {
logger.debug(
`Failed to parse tool arguments for ${toolRun.tool.name}: ${error}`,
);
return { success: false, error: error as Error };
}
return parsedArgs;
}

function buildApprovalRequestResult<TContext>(
Expand All @@ -154,6 +173,24 @@ function buildApprovalRequestResult<TContext>(
};
}

function buildParseErrorResult<TContext>(
deps: FunctionToolCallDeps<TContext>,
toolRun: ToolRunFunction<TContext>,
error: Error,
): FunctionToolResult<TContext> {
const errorMessage = `An error occurred while parsing tool arguments. Please try again with valid JSON. Error: ${error.message}`;
return {
type: 'function_output',
tool: toolRun.tool,
output: errorMessage,
runItem: new RunToolCallOutputItem(
getToolCallOutputItem(toolRun.toolCall, errorMessage),
deps.agent,
errorMessage,
),
};
}

async function buildApprovalRejectionResult<TContext>(
deps: FunctionToolCallDeps<TContext>,
toolRun: ToolRunFunction<TContext>,
Expand Down
40 changes: 40 additions & 0 deletions packages/agents-core/test/runner/toolExecution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,46 @@ describe('executeShellActions', () => {
expect(firstResult.agentRunResult).toBe(nestedRunResult);
expect(firstResult.interruptions).toEqual([approval]);
});

it('handles invalid JSON in tool call arguments gracefully instead of crashing', async () => {
// Reproduces issue #723: SyntaxError stops agent when LLM generates invalid JSON
const t = tool({
name: 'checkTagActivity',
description: 'Check tag activity',
parameters: z.object({
tagIds: z.array(z.string()),
since: z.string(),
}),
execute: vi.fn(async () => 'success'),
}) as unknown as FunctionTool;

const invalidToolCall = {
...toolCall,
name: 'checkTagActivity',
arguments:
'{"{"tagIds":["65aafb7e-4293-4376-baf6-1f9d197e960a"],"since":"2025-09-04T13:26:13.991Z"}',
};

const res = await withTrace('test', () =>
executeFunctionToolCalls(
state._currentAgent,
[{ toolCall: invalidToolCall, tool: t }],
runner,
state,
),
);

expect(res).toHaveLength(1);
const firstResult = res[0];

expect(firstResult.type).toBe('function_output');
if (firstResult.type === 'function_output') {
expect(String(firstResult.output)).toContain(
'An error occurred while parsing tool arguments',
);
expect(String(firstResult.output)).toContain('valid JSON');
}
});
});

describe('executeComputerActions', () => {
Expand Down