Skip to content
Open
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
507 changes: 507 additions & 0 deletions agent-server/nodejs/src/api-server.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions agent-server/nodejs/src/lib/BrowserAgentServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,55 @@ export class BrowserAgentServer extends EventEmitter {
}
}

/**
* Execute a tool directly on a connected DevTools client
* This bypasses LLM orchestration and calls the tool directly
* @param {Object} connection - DevTools WebSocket connection
* @param {string} tool - Tool name (e.g., 'perform_action', 'navigate_url')
* @param {Object} args - Tool-specific arguments
* @param {number} timeout - Execution timeout in milliseconds
* @returns {Promise<Object>} Tool execution result
*/
async executeToolDirect(connection, tool, args, timeout = 30000) {
const rpcId = `tool-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;

logger.info('Executing tool directly', {
clientId: connection.clientId,
tool,
timeout
});

try {
// Prepare RPC request for execute_tool method
const response = await connection.rpcClient.callMethod(
connection.ws,
'execute_tool',
{
tool,
args,
timeout
},
timeout + 5000 // Add buffer for network overhead
);

logger.info('Tool execution completed', {
clientId: connection.clientId,
tool,
success: response?.result?.success
});

return response;

} catch (error) {
logger.error('Tool execution failed', {
clientId: connection.clientId,
tool,
error: error.message
});
throw error;
}
}
Comment on lines +1421 to +1468
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify connection readiness before tool execution.

The method doesn't validate that the connection is in a ready state before attempting RPC communication. If the connection isn't ready, the RPC call will fail with a less informative error.

🔎 Suggested validation
  async executeToolDirect(connection, tool, args, timeout = 30000) {
    const rpcId = `tool-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;

+   if (!connection.ready) {
+     throw new Error(`Connection for client ${connection.clientId} is not ready for tool execution`);
+   }
+
    logger.info('Executing tool directly', {
      clientId: connection.clientId,
      tool,
      timeout
    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Execute a tool directly on a connected DevTools client
* This bypasses LLM orchestration and calls the tool directly
* @param {Object} connection - DevTools WebSocket connection
* @param {string} tool - Tool name (e.g., 'perform_action', 'navigate_url')
* @param {Object} args - Tool-specific arguments
* @param {number} timeout - Execution timeout in milliseconds
* @returns {Promise<Object>} Tool execution result
*/
async executeToolDirect(connection, tool, args, timeout = 30000) {
const rpcId = `tool-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
logger.info('Executing tool directly', {
clientId: connection.clientId,
tool,
timeout
});
try {
// Prepare RPC request for execute_tool method
const response = await connection.rpcClient.callMethod(
connection.ws,
'execute_tool',
{
tool,
args,
timeout
},
timeout + 5000 // Add buffer for network overhead
);
logger.info('Tool execution completed', {
clientId: connection.clientId,
tool,
success: response?.result?.success
});
return response;
} catch (error) {
logger.error('Tool execution failed', {
clientId: connection.clientId,
tool,
error: error.message
});
throw error;
}
}
/**
* Execute a tool directly on a connected DevTools client
* This bypasses LLM orchestration and calls the tool directly
* @param {Object} connection - DevTools WebSocket connection
* @param {string} tool - Tool name (e.g., 'perform_action', 'navigate_url')
* @param {Object} args - Tool-specific arguments
* @param {number} timeout - Execution timeout in milliseconds
* @returns {Promise<Object>} Tool execution result
*/
async executeToolDirect(connection, tool, args, timeout = 30000) {
const rpcId = `tool-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
if (!connection.ready) {
throw new Error(`Connection for client ${connection.clientId} is not ready for tool execution`);
}
logger.info('Executing tool directly', {
clientId: connection.clientId,
tool,
timeout
});
try {
// Prepare RPC request for execute_tool method
const response = await connection.rpcClient.callMethod(
connection.ws,
'execute_tool',
{
tool,
args,
timeout
},
timeout + 5000 // Add buffer for network overhead
);
logger.info('Tool execution completed', {
clientId: connection.clientId,
tool,
success: response?.result?.success
});
return response;
} catch (error) {
logger.error('Tool execution failed', {
clientId: connection.clientId,
tool,
error: error.message
});
throw error;
}
}


/**
* Execute JavaScript in a browser tab
* @param {string} tabId - Tab ID (target ID)
Expand Down
2 changes: 1 addition & 1 deletion config/gni/devtools_grd_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,7 @@ grd_files_bundled_sources = [
"front_end/panels/ai_chat/agent_framework/implementation/agents/ScrollActionAgent.js",
"front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.js",
"front_end/panels/ai_chat/agent_framework/implementation/agents/SearchAgent.js",
"front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgentV0.js",
"front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgentV1.js",
"front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgentV2.js",
"front_end/panels/ai_chat/common/MarkdownViewerUtil.js",
"front_end/panels/ai_chat/evaluation/runner/VisionAgentEvaluationRunner.js",
Expand Down
11 changes: 8 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,27 @@ RUN /workspace/depot_tools/ensure_bootstrap
RUN npm run build

# Add Browser Operator fork and switch to it
# Branch is configurable via build arg (default: main)
ARG BROWSER_OPERATOR_BRANCH=main
RUN git remote add upstream https://github.com/BrowserOperator/browser-operator-core.git
RUN git fetch upstream
RUN git checkout upstream/main
RUN git checkout upstream/${BROWSER_OPERATOR_BRANCH}

# Copy local LLM changes on top of the upstream code
# This allows iterative development without breaking BUILD.gn compatibility
# This allows iterative development of LLM providers without rebuilding everything
COPY front_end/panels/ai_chat/LLM /workspace/devtools/devtools-frontend/front_end/panels/ai_chat/LLM

# Regenerate GN build files
RUN gn gen out/Default

# AUTOMATED_MODE: Default true for API mode (Type 2/3). Override with --build-arg AUTOMATED_MODE=false for Type 1.
ARG AUTOMATED_MODE=true
RUN if [ "$AUTOMATED_MODE" = "true" ]; then \
sed -i 's/AUTOMATED_MODE: false/AUTOMATED_MODE: true/' \
front_end/panels/ai_chat/core/BuildConfig.ts; \
fi

# Build Browser Operator version with local changes
# Build Browser Operator version with local changes (fresh GN generation)
RUN npm run build

# ==============================================================================
Expand Down
96 changes: 95 additions & 1 deletion front_end/panels/ai_chat/evaluation/EvaluationAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ import {
EvaluationRequest,
EvaluationSuccessResponse,
EvaluationErrorResponse,
ToolExecutionRequest,
ToolExecutionSuccessResponse,
ToolExecutionErrorResponse,
ErrorCodes,
isWelcomeMessage,
isRegistrationAckMessage,
isEvaluationRequest,
isToolExecutionRequest,
isPongMessage,
createRegisterMessage,
createReadyMessage,
createAuthVerifyMessage,
createStatusMessage,
createSuccessResponse,
createErrorResponse
createErrorResponse,
createToolExecutionSuccessResponse,
createToolExecutionErrorResponse
} from './EvaluationProtocol.js';

const logger = createLogger('EvaluationAgent');
Expand Down Expand Up @@ -171,6 +177,9 @@ export class EvaluationAgent {
else if (isEvaluationRequest(message)) {
await this.handleEvaluationRequest(message);
}
else if (isToolExecutionRequest(message)) {
await this.handleToolExecutionRequest(message);
}
else if (isPongMessage(message)) {
logger.debug('Received pong');
}
Expand Down Expand Up @@ -599,6 +608,91 @@ export class EvaluationAgent {
}
}

/**
* Handle direct tool execution request (no LLM orchestration)
* This allows calling browser automation tools directly via API
*/
private async handleToolExecutionRequest(request: ToolExecutionRequest): Promise<void> {
const { params, id } = request;
const startTime = Date.now();

logger.info('Received tool execution request', {
tool: params.tool,
hasArgs: !!params.args,
timeout: params.timeout
});

try {
// Get the tool from registry
const tool = ToolRegistry.getRegisteredTool(params.tool);
if (!tool) {
const errorResponse = createToolExecutionErrorResponse(
id,
ErrorCodes.INVALID_TOOL,
`Tool not found: ${params.tool}`,
params.tool,
`Tool '${params.tool}' is not registered in the ToolRegistry`
);
if (this.client) {
this.client.send(errorResponse);
}
return;
}

// Execute the tool directly (no LLM, no navigation, no retries)
const timeout = params.timeout || 30000;
const result = await this.executeToolWithTimeout(
tool,
params.args,
timeout,
undefined, // No tracing context for direct tool calls
params.tool
);

const executionTime = Date.now() - startTime;

// Send success response
const successResponse = createToolExecutionSuccessResponse(
id,
params.tool,
result,
executionTime
);

if (this.client) {
this.client.send(successResponse);
}

logger.info('Tool execution completed', {
tool: params.tool,
executionTime,
success: true
});

} catch (error) {
const executionTime = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';

logger.error(`Tool execution failed: ${errorMessage}`, {
tool: params.tool,
executionTime
});

// Send error response
const errorResponse = createToolExecutionErrorResponse(
id,
ErrorCodes.TOOL_EXECUTION_ERROR,
'Tool execution failed',
params.tool,
errorMessage
);

if (this.client) {
this.client.send(errorResponse);
}
}
}

private async executeToolWithTimeout(
tool: any,
input: any,
Expand Down
81 changes: 81 additions & 0 deletions front_end/panels/ai_chat/evaluation/EvaluationProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,44 @@ export interface EvaluationRequest {
id: string;
}

// Direct tool execution request (no LLM orchestration)
export interface ToolExecutionRequest {
jsonrpc: '2.0';
method: 'execute_tool';
params: ToolExecutionParams;
id: string;
}

export interface ToolExecutionParams {
tool: string; // Tool name (e.g., 'perform_action', 'navigate_url')
args: any; // Tool-specific arguments
timeout?: number; // Optional timeout (default 30000ms)
}

export interface ToolExecutionSuccessResponse {
jsonrpc: '2.0';
result: {
success: true;
output: any;
executionTime: number;
tool: string;
};
id: string;
}

export interface ToolExecutionErrorResponse {
jsonrpc: '2.0';
error: {
code: number;
message: string;
data?: {
tool: string;
error: string;
};
};
id: string;
}

export interface EvaluationParams {
evaluationId: string;
name: string;
Expand Down Expand Up @@ -170,6 +208,10 @@ export function isEvaluationRequest(msg: any): msg is EvaluationRequest {
return msg?.jsonrpc === '2.0' && msg?.method === 'evaluate';
}

export function isToolExecutionRequest(msg: any): msg is ToolExecutionRequest {
return msg?.jsonrpc === '2.0' && msg?.method === 'execute_tool';
}

export function isPongMessage(msg: any): msg is PongMessage {
return msg?.type === 'pong';
}
Expand Down Expand Up @@ -254,4 +296,43 @@ export function createErrorResponse(
},
id
};
}

export function createToolExecutionSuccessResponse(
id: string,
tool: string,
output: any,
executionTime: number
): ToolExecutionSuccessResponse {
return {
jsonrpc: '2.0',
result: {
success: true,
output,
executionTime,
tool
},
id
};
}

export function createToolExecutionErrorResponse(
id: string,
code: number,
message: string,
tool: string,
error: string
): ToolExecutionErrorResponse {
return {
jsonrpc: '2.0',
error: {
code,
message,
data: {
tool,
error
}
},
id
};
}
Loading