Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-regions-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-core': patch
---

Continue agent execution when function calls are pending
16 changes: 14 additions & 2 deletions packages/agents-core/src/runImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type ProcessedResponse<TContext = UnknownContext> = {
functions: ToolRunFunction<TContext>[];
computerActions: ToolRunComputer[];
toolsUsed: string[];
hasToolsOrApprovalsToRun(): boolean;
};

/**
Expand Down Expand Up @@ -147,6 +148,13 @@ export function processModelResponse<TContext>(
functions: runFunctions,
computerActions: runComputerActions,
toolsUsed: toolsUsed,
hasToolsOrApprovalsToRun(): boolean {
return (
runHandoffs.length > 0 ||
runFunctions.length > 0 ||
runComputerActions.length > 0
);
},
};
}

Expand Down Expand Up @@ -414,7 +422,10 @@ export async function executeToolsAndSideEffects<TContext>(
);
}

if (agent.outputType === 'text') {
if (
agent.outputType === 'text' &&
!processedResponse.hasToolsOrApprovalsToRun()
) {
return new SingleStepResult(
originalInput,
newResponse,
Expand All @@ -425,7 +436,8 @@ export async function executeToolsAndSideEffects<TContext>(
output: potentialFinalOutput,
},
);
} else {
} else if (agent.outputType !== 'text' && potentialFinalOutput) {
// Structured output schema => always leads to a final output if we have text
const { parser } = getSchemaAndParserFromInputType(
agent.outputType,
'final_output',
Expand Down
13 changes: 12 additions & 1 deletion packages/agents-core/src/runState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ async function deserializeProcessedResponse<TContext = UnknownContext>(
}),
);

return {
const result = {
newItems: serializedProcessedResponse.newItems.map((item) =>
deserializeItem(item, agentMap),
),
Expand Down Expand Up @@ -735,4 +735,15 @@ async function deserializeProcessedResponse<TContext = UnknownContext>(
},
),
};

return {
...result,
hasToolsOrApprovalsToRun(): boolean {
return (
result.handoffs.length > 0 ||
result.functions.length > 0 ||
result.computerActions.length > 0
);
},
};
}
133 changes: 133 additions & 0 deletions packages/agents-core/test/runImplementation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
executeFunctionToolCalls,
executeComputerActions,
executeHandoffCalls,
executeToolsAndSideEffects,
} from '../src/runImplementation';
import { FunctionTool, FunctionToolResult, tool } from '../src/tool';
import { handoff } from '../src/handoff';
Expand Down Expand Up @@ -78,6 +79,7 @@ describe('processModelResponse', () => {
expect(result.newItems[1].rawItem).toEqual(
TEST_MODEL_RESPONSE_WITH_FUNCTION.output[1],
);
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
});
});

Expand Down Expand Up @@ -438,6 +440,7 @@ describe('processModelResponse edge cases', () => {
expect(result.computerActions[0]?.toolCall).toBe(compCall);
expect(result.handoffs[0]?.toolCall).toBe(handCall);
expect(result.toolsUsed).toEqual(['test', 'computer_use', h.toolName]);
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
expect(result.newItems[3]).toBeInstanceOf(MessageOutputItem);
});
});
Expand Down Expand Up @@ -790,3 +793,133 @@ describe('empty execution helpers', () => {
expect(comp).toEqual([]);
});
});

describe('hasToolsOrApprovalsToRun method', () => {
it('returns true when handoffs are pending', () => {
const target = new Agent({ name: 'Target' });
const h = handoff(target);
const response: ModelResponse = {
output: [{ ...TEST_MODEL_FUNCTION_CALL, name: h.toolName }],
usage: new Usage(),
} as any;

const result = processModelResponse(response, TEST_AGENT, [], [h]);
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
});

it('returns true when function calls are pending', () => {
const result = processModelResponse(
TEST_MODEL_RESPONSE_WITH_FUNCTION,
TEST_AGENT,
[TEST_TOOL],
[],
);
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
});

it('returns true when computer actions are pending', () => {
const computer = computerTool({
computer: {
environment: 'mac',
dimensions: [10, 10],
screenshot: vi.fn(async () => 'img'),
click: vi.fn(async () => {}),
doubleClick: vi.fn(async () => {}),
drag: vi.fn(async () => {}),
keypress: vi.fn(async () => {}),
move: vi.fn(async () => {}),
scroll: vi.fn(async () => {}),
type: vi.fn(async () => {}),
wait: vi.fn(async () => {}),
},
});
const compCall: protocol.ComputerUseCallItem = {
id: 'c1',
type: 'computer_call',
callId: 'c1',
status: 'completed',
action: { type: 'screenshot' },
};
const response: ModelResponse = {
output: [compCall],
usage: new Usage(),
} as any;

const result = processModelResponse(response, TEST_AGENT, [computer], []);
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
});

it('returns false when no tools or approvals are pending', () => {
const response: ModelResponse = {
output: [TEST_MODEL_MESSAGE],
usage: new Usage(),
} as any;

const result = processModelResponse(response, TEST_AGENT, [], []);
expect(result.hasToolsOrApprovalsToRun()).toBe(false);
});
});

describe('executeToolsAndSideEffects', () => {
let runner: Runner;
let state: RunState<any, any>;

beforeEach(() => {
runner = new Runner({ tracingDisabled: true });
state = new RunState(new RunContext(), 'test input', TEST_AGENT, 1);
});

it('continues execution when text agent has tools pending', async () => {
const textAgent = new Agent({ name: 'TextAgent', outputType: 'text' });
const processedResponse = processModelResponse(
TEST_MODEL_RESPONSE_WITH_FUNCTION,
textAgent,
[TEST_TOOL],
[],
);

expect(processedResponse.hasToolsOrApprovalsToRun()).toBe(true);

const result = await withTrace('test', () =>
executeToolsAndSideEffects(
textAgent,
'test input',
[],
TEST_MODEL_RESPONSE_WITH_FUNCTION,
processedResponse,
runner,
state,
),
);

expect(result.nextStep.type).toBe('next_step_run_again');
});

it('returns final output when text agent has no tools pending', async () => {
const textAgent = new Agent({ name: 'TextAgent', outputType: 'text' });
const response: ModelResponse = {
output: [TEST_MODEL_MESSAGE],
usage: new Usage(),
} as any;
const processedResponse = processModelResponse(response, textAgent, [], []);

expect(processedResponse.hasToolsOrApprovalsToRun()).toBe(false);

const result = await withTrace('test', () =>
executeToolsAndSideEffects(
textAgent,
'test input',
[],
response,
processedResponse,
runner,
state,
),
);

expect(result.nextStep.type).toBe('next_step_final_output');
if (result.nextStep.type === 'next_step_final_output') {
expect(result.nextStep.output).toBe('Hello World');
}
});
});
2 changes: 2 additions & 0 deletions packages/agents-core/test/runState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ describe('deserialize helpers', () => {
handoffs: [],
computerActions: [{ toolCall: call, computer: tool }],
toolsUsed: [],
hasToolsOrApprovalsToRun: () => true,
};

const restored = await RunState.fromString(agent, state.toString());
Expand All @@ -277,6 +278,7 @@ describe('deserialize helpers', () => {
handoffs: [],
computerActions: [{ toolCall: call, computer: tool }],
toolsUsed: [],
hasToolsOrApprovalsToRun: () => true,
};
state._currentStep = {
type: 'next_step_handoff',
Expand Down