Skip to content

Commit 544ed4b

Browse files
meganetaaanclaude
andauthored
Fix #61 Continue agent execution when function calls are pending (#62)
* Continue agent execution when function calls are pending * Add tests * Add changeset for tool execution fix 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Fix mock object --------- Co-authored-by: Claude <[email protected]>
1 parent 10ddebc commit 544ed4b

File tree

5 files changed

+166
-3
lines changed

5 files changed

+166
-3
lines changed

.changeset/wet-regions-fold.md

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+
Continue agent execution when function calls are pending

packages/agents-core/src/runImplementation.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export type ProcessedResponse<TContext = UnknownContext> = {
5353
functions: ToolRunFunction<TContext>[];
5454
computerActions: ToolRunComputer[];
5555
toolsUsed: string[];
56+
hasToolsOrApprovalsToRun(): boolean;
5657
};
5758

5859
/**
@@ -147,6 +148,13 @@ export function processModelResponse<TContext>(
147148
functions: runFunctions,
148149
computerActions: runComputerActions,
149150
toolsUsed: toolsUsed,
151+
hasToolsOrApprovalsToRun(): boolean {
152+
return (
153+
runHandoffs.length > 0 ||
154+
runFunctions.length > 0 ||
155+
runComputerActions.length > 0
156+
);
157+
},
150158
};
151159
}
152160

@@ -414,7 +422,10 @@ export async function executeToolsAndSideEffects<TContext>(
414422
);
415423
}
416424

417-
if (agent.outputType === 'text') {
425+
if (
426+
agent.outputType === 'text' &&
427+
!processedResponse.hasToolsOrApprovalsToRun()
428+
) {
418429
return new SingleStepResult(
419430
originalInput,
420431
newResponse,
@@ -425,7 +436,8 @@ export async function executeToolsAndSideEffects<TContext>(
425436
output: potentialFinalOutput,
426437
},
427438
);
428-
} else {
439+
} else if (agent.outputType !== 'text' && potentialFinalOutput) {
440+
// Structured output schema => always leads to a final output if we have text
429441
const { parser } = getSchemaAndParserFromInputType(
430442
agent.outputType,
431443
'final_output',

packages/agents-core/src/runState.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ async function deserializeProcessedResponse<TContext = UnknownContext>(
694694
}),
695695
);
696696

697-
return {
697+
const result = {
698698
newItems: serializedProcessedResponse.newItems.map((item) =>
699699
deserializeItem(item, agentMap),
700700
),
@@ -735,4 +735,15 @@ async function deserializeProcessedResponse<TContext = UnknownContext>(
735735
},
736736
),
737737
};
738+
739+
return {
740+
...result,
741+
hasToolsOrApprovalsToRun(): boolean {
742+
return (
743+
result.handoffs.length > 0 ||
744+
result.functions.length > 0 ||
745+
result.computerActions.length > 0
746+
);
747+
},
748+
};
738749
}

packages/agents-core/test/runImplementation.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
executeFunctionToolCalls,
2626
executeComputerActions,
2727
executeHandoffCalls,
28+
executeToolsAndSideEffects,
2829
} from '../src/runImplementation';
2930
import { FunctionTool, FunctionToolResult, tool } from '../src/tool';
3031
import { handoff } from '../src/handoff';
@@ -78,6 +79,7 @@ describe('processModelResponse', () => {
7879
expect(result.newItems[1].rawItem).toEqual(
7980
TEST_MODEL_RESPONSE_WITH_FUNCTION.output[1],
8081
);
82+
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
8183
});
8284
});
8385

@@ -438,6 +440,7 @@ describe('processModelResponse edge cases', () => {
438440
expect(result.computerActions[0]?.toolCall).toBe(compCall);
439441
expect(result.handoffs[0]?.toolCall).toBe(handCall);
440442
expect(result.toolsUsed).toEqual(['test', 'computer_use', h.toolName]);
443+
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
441444
expect(result.newItems[3]).toBeInstanceOf(MessageOutputItem);
442445
});
443446
});
@@ -790,3 +793,133 @@ describe('empty execution helpers', () => {
790793
expect(comp).toEqual([]);
791794
});
792795
});
796+
797+
describe('hasToolsOrApprovalsToRun method', () => {
798+
it('returns true when handoffs are pending', () => {
799+
const target = new Agent({ name: 'Target' });
800+
const h = handoff(target);
801+
const response: ModelResponse = {
802+
output: [{ ...TEST_MODEL_FUNCTION_CALL, name: h.toolName }],
803+
usage: new Usage(),
804+
} as any;
805+
806+
const result = processModelResponse(response, TEST_AGENT, [], [h]);
807+
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
808+
});
809+
810+
it('returns true when function calls are pending', () => {
811+
const result = processModelResponse(
812+
TEST_MODEL_RESPONSE_WITH_FUNCTION,
813+
TEST_AGENT,
814+
[TEST_TOOL],
815+
[],
816+
);
817+
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
818+
});
819+
820+
it('returns true when computer actions are pending', () => {
821+
const computer = computerTool({
822+
computer: {
823+
environment: 'mac',
824+
dimensions: [10, 10],
825+
screenshot: vi.fn(async () => 'img'),
826+
click: vi.fn(async () => {}),
827+
doubleClick: vi.fn(async () => {}),
828+
drag: vi.fn(async () => {}),
829+
keypress: vi.fn(async () => {}),
830+
move: vi.fn(async () => {}),
831+
scroll: vi.fn(async () => {}),
832+
type: vi.fn(async () => {}),
833+
wait: vi.fn(async () => {}),
834+
},
835+
});
836+
const compCall: protocol.ComputerUseCallItem = {
837+
id: 'c1',
838+
type: 'computer_call',
839+
callId: 'c1',
840+
status: 'completed',
841+
action: { type: 'screenshot' },
842+
};
843+
const response: ModelResponse = {
844+
output: [compCall],
845+
usage: new Usage(),
846+
} as any;
847+
848+
const result = processModelResponse(response, TEST_AGENT, [computer], []);
849+
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
850+
});
851+
852+
it('returns false when no tools or approvals are pending', () => {
853+
const response: ModelResponse = {
854+
output: [TEST_MODEL_MESSAGE],
855+
usage: new Usage(),
856+
} as any;
857+
858+
const result = processModelResponse(response, TEST_AGENT, [], []);
859+
expect(result.hasToolsOrApprovalsToRun()).toBe(false);
860+
});
861+
});
862+
863+
describe('executeToolsAndSideEffects', () => {
864+
let runner: Runner;
865+
let state: RunState<any, any>;
866+
867+
beforeEach(() => {
868+
runner = new Runner({ tracingDisabled: true });
869+
state = new RunState(new RunContext(), 'test input', TEST_AGENT, 1);
870+
});
871+
872+
it('continues execution when text agent has tools pending', async () => {
873+
const textAgent = new Agent({ name: 'TextAgent', outputType: 'text' });
874+
const processedResponse = processModelResponse(
875+
TEST_MODEL_RESPONSE_WITH_FUNCTION,
876+
textAgent,
877+
[TEST_TOOL],
878+
[],
879+
);
880+
881+
expect(processedResponse.hasToolsOrApprovalsToRun()).toBe(true);
882+
883+
const result = await withTrace('test', () =>
884+
executeToolsAndSideEffects(
885+
textAgent,
886+
'test input',
887+
[],
888+
TEST_MODEL_RESPONSE_WITH_FUNCTION,
889+
processedResponse,
890+
runner,
891+
state,
892+
),
893+
);
894+
895+
expect(result.nextStep.type).toBe('next_step_run_again');
896+
});
897+
898+
it('returns final output when text agent has no tools pending', async () => {
899+
const textAgent = new Agent({ name: 'TextAgent', outputType: 'text' });
900+
const response: ModelResponse = {
901+
output: [TEST_MODEL_MESSAGE],
902+
usage: new Usage(),
903+
} as any;
904+
const processedResponse = processModelResponse(response, textAgent, [], []);
905+
906+
expect(processedResponse.hasToolsOrApprovalsToRun()).toBe(false);
907+
908+
const result = await withTrace('test', () =>
909+
executeToolsAndSideEffects(
910+
textAgent,
911+
'test input',
912+
[],
913+
response,
914+
processedResponse,
915+
runner,
916+
state,
917+
),
918+
);
919+
920+
expect(result.nextStep.type).toBe('next_step_final_output');
921+
if (result.nextStep.type === 'next_step_final_output') {
922+
expect(result.nextStep.output).toBe('Hello World');
923+
}
924+
});
925+
});

packages/agents-core/test/runState.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ describe('deserialize helpers', () => {
253253
handoffs: [],
254254
computerActions: [{ toolCall: call, computer: tool }],
255255
toolsUsed: [],
256+
hasToolsOrApprovalsToRun: () => true,
256257
};
257258

258259
const restored = await RunState.fromString(agent, state.toString());
@@ -277,6 +278,7 @@ describe('deserialize helpers', () => {
277278
handoffs: [],
278279
computerActions: [{ toolCall: call, computer: tool }],
279280
toolsUsed: [],
281+
hasToolsOrApprovalsToRun: () => true,
280282
};
281283
state._currentStep = {
282284
type: 'next_step_handoff',

0 commit comments

Comments
 (0)