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/open-streets-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-core': patch
---

fix: Enable creating and disposing Computer per request ref: #663
55 changes: 49 additions & 6 deletions examples/tools/computer-use.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { chromium, Browser, Page } from 'playwright';
import { Agent, run, withTrace, Computer, computerTool } from '@openai/agents';
import { Agent, run, withTrace, computerTool, Computer } from '@openai/agents';

async function main() {
async function singletonComputer() {
// If your app never runs multiple computer using agents at the same time,
// you can create a singleton computer and use it in all your agents.
const computer = await new LocalPlaywrightComputer().init();
try {
const agent = new Agent({
Expand All @@ -20,6 +22,36 @@ async function main() {
}
}

async function computerPerRequest() {
// If your app runs multiple computer using agents at the same time,
// you can create a computer per request.
const agent = new Agent({
name: 'Browser user',
model: 'computer-use-preview',
instructions: 'You are a helpful agent.',
tools: [
computerTool({
// initialize a new computer for each run and dispose it after the run is complete
computer: {
create: async ({ runContext }) => {
console.log('Initializing computer for run context:', runContext);
return await new LocalPlaywrightComputer().init();
},
dispose: async ({ runContext, computer }) => {
console.log('Disposing of computer for run context:', runContext);
await computer.dispose();
},
},
}),
],
modelSettings: { truncation: 'auto' },
});
await withTrace('CUA Example', async () => {
const result = await run(agent, "What's the weather in Tokyo?");
console.log(`\nFinal response:\n${result.finalOutput}`);
});
}

// --- CUA KEY TO PLAYWRIGHT KEY MAP ---

const CUA_KEY_TO_PLAYWRIGHT_KEY: Record<string, string> = {
Expand Down Expand Up @@ -186,7 +218,18 @@ class LocalPlaywrightComputer implements Computer {
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
const mode = (process.argv[2] ?? '').toLowerCase();

if (mode === 'singleton') {
// Choose singleton mode for cases where concurrent runs are not expected.
singletonComputer().catch((error) => {
console.error(error);
process.exit(1);
});
} else {
// Default to per-request mode to avoid sharing state across runs.
computerPerRequest().catch((error) => {
console.error(error);
process.exit(1);
});
}
5 changes: 3 additions & 2 deletions packages/agents-core/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ShellTool,
ApplyPatchTool,
} from './tool';
import { Computer } from './computer';
import { Handoff } from './handoff';
import {
AgentInputItem,
Expand Down Expand Up @@ -178,8 +179,8 @@ export type SerializedFunctionTool = {
export type SerializedComputerTool = {
type: ComputerTool['type'];
name: ComputerTool['name'];
environment: ComputerTool['computer']['environment'];
dimensions: ComputerTool['computer']['dimensions'];
environment: Computer['environment'];
dimensions: Computer['dimensions'];
};

export type SerializedShellTool = {
Expand Down
33 changes: 32 additions & 1 deletion packages/agents-core/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ import {
prepareInputItemsWithSession,
} from './runImplementation';
import { RunItem } from './items';
import { Tool } from './tool';
import {
ComputerTool,
Tool,
resolveComputer,
disposeResolvedComputers,
} from './tool';
import {
getOrCreateTrace,
addErrorToCurrentSpan,
Expand Down Expand Up @@ -953,6 +958,13 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
}
throw err;
} finally {
if (state._currentStep?.type !== 'next_step_interruption') {
try {
await disposeResolvedComputers({ runContext: state._context });
} catch (error) {
logger.warn(`Failed to dispose computers after run: ${error}`);
}
}
if (state._currentAgentSpan) {
if (state._currentStep?.type !== 'next_step_interruption') {
// don't end the span if the run was interrupted
Expand Down Expand Up @@ -1333,6 +1345,13 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
}
throw error;
} finally {
if (result.state._currentStep?.type !== 'next_step_interruption') {
try {
await disposeResolvedComputers({ runContext: result.state._context });
} catch (error) {
logger.warn(`Failed to dispose computers after run: ${error}`);
}
}
if (result.state._currentAgentSpan) {
if (result.state._currentStep?.type !== 'next_step_interruption') {
result.state._currentAgentSpan.end();
Expand Down Expand Up @@ -2103,6 +2122,18 @@ async function prepareAgentArtifacts<
const handoffs = await state._currentAgent.getEnabledHandoffs(state._context);
const tools = await state._currentAgent.getAllTools(state._context);

const computerTools = tools.filter(
(tool) => tool.type === 'computer',
) as ComputerTool<TContext, any>[];

if (computerTools.length > 0) {
await Promise.all(
computerTools.map(async (tool) => {
await resolveComputer({ tool, runContext: state._context });
}),
);
}

if (!state._currentAgentSpan) {
const handoffNames = handoffs.map((h) => h.agentName);
state._currentAgentSpan = createAgentSpan({
Expand Down
37 changes: 28 additions & 9 deletions packages/agents-core/src/runImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
HostedMCPTool,
ShellTool,
ApplyPatchTool,
resolveComputer,
} from './tool';
import type { ShellResult } from './shell';
import { AgentInputItem, UnknownContext } from './types';
Expand Down Expand Up @@ -76,7 +77,7 @@ type ToolRunFunction<TContext = UnknownContext> = {
// Holds a pending computer-use action so we can dispatch to the configured computer tool.
type ToolRunComputer = {
toolCall: protocol.ComputerUseCallItem;
computer: ComputerTool;
computer: ComputerTool<any, any>;
};

// Captures a shell invocation emitted by the model.
Expand Down Expand Up @@ -235,7 +236,9 @@ export function processModelResponse<TContext>(
const functionMap = new Map(
tools.filter((t) => t.type === 'function').map((t) => [t.name, t]),
);
const computerTool = tools.find((t) => t.type === 'computer');
const computerTool = tools.find(
(t): t is ComputerTool<TContext, any> => t.type === 'computer',
);
const shellTool = tools.find((t): t is ShellTool => t.type === 'shell');
const applyPatchTool = tools.find(
(t): t is ApplyPatchTool => t.type === 'apply_patch',
Expand Down Expand Up @@ -1360,12 +1363,25 @@ export async function executeFunctionToolCalls<TContext = UnknownContext>(

// Emit agent_tool_end even on error to maintain consistent event lifecycle
const errorResult = String(error);
runner.emit('agent_tool_end', state._context, agent, toolRun.tool, errorResult, {
toolCall: toolRun.toolCall,
});
agent.emit('agent_tool_end', state._context, toolRun.tool, errorResult, {
toolCall: toolRun.toolCall,
});
runner.emit(
'agent_tool_end',
state._context,
agent,
toolRun.tool,
errorResult,
{
toolCall: toolRun.toolCall,
},
);
agent.emit(
'agent_tool_end',
state._context,
toolRun.tool,
errorResult,
{
toolCall: toolRun.toolCall,
},
);

throw error;
}
Expand Down Expand Up @@ -1753,7 +1769,6 @@ export async function executeComputerActions(
const _logger = customLogger ?? logger;
const results: RunItem[] = [];
for (const action of actions) {
const computer = action.computer.computer;
const toolCall = action.toolCall;

// Hooks: on_tool_start (global + agent)
Expand All @@ -1767,6 +1782,10 @@ export async function executeComputerActions(
// Run the action and get screenshot
let output: string;
try {
const computer = await resolveComputer({
tool: action.computer,
runContext,
});
output = await _runComputerActionAndScreenshot(computer, toolCall);
} catch (err) {
_logger.error('Failed to execute computer action:', err);
Expand Down
Loading