A Vision-to-Action browser automation engine that enables LLM-driven web interactions through screenshot analysis and natural language reasoning.
The ComputerUse package provides a sophisticated abstraction layer for LLM-controlled browser automation. Unlike traditional browser automation tools that require explicit selectors and DOM knowledge, ComputerUse allows AI models to interact with web pages the same way humans do: by seeing screenshots and reasoning about what actions to take next.
Core Concept: The engine captures screenshots of web pages, sends them to vision-capable LLMs, and executes the actions the model determines are necessary to accomplish a natural language goal. A separate judge component evaluates progress and determines goal completion.
This package serves as the foundational layer for MemberJunction's Computer Use capabilities, designed to be extended by the @memberjunction/computer-use-engine package for full MJ integration.
ComputerUseTool<TInput, TOutput> with JSON Schema*.example.com, exact domains, or wildcard *BaseBrowserAdapter for custom browsersBrowserConfig.Connect — see External Browser Attach GuideComputerUseResultStop() method for graceful terminationnpm install @memberjunction/computer-use
Required Peer Dependency: Install Playwright for the default browser adapter:
npm install playwright
npx playwright install chromium
import { ComputerUseEngine, RunComputerUseParams, ModelConfig } from '@memberjunction/computer-use';
// Create engine instance
const engine = new ComputerUseEngine();
// Configure the run
const params = new RunComputerUseParams();
params.Goal = "Navigate to news.ycombinator.com and find the top story title";
params.StartUrl = "https://news.ycombinator.com";
params.MaxSteps = 10;
params.Headless = false; // Watch the browser work
// Configure models (controller analyzes screenshots, judge evaluates completion)
params.ControllerModel = new ModelConfig("anthropic", "claude-3-5-sonnet-20241022");
params.JudgeModel = new ModelConfig("anthropic", "claude-3-5-sonnet-20241022");
// Execute
const result = await engine.Run(params);
if (result.Success) {
console.log(`Goal completed in ${result.TotalSteps} steps!`);
console.log(`Final URL: ${result.FinalUrl}`);
console.log(`Judge verdict: ${result.FinalJudgeVerdict?.Reason}`);
} else {
console.error(`Failed: ${result.Status}`);
if (result.Error) {
console.error(`Error: ${result.Error.Message}`);
}
}
// Access step-by-step history
for (const step of result.Steps) {
console.log(`Step ${step.StepNumber}: ${step.ControllerReasoning}`);
console.log(`Actions: ${step.ActionsRequested.length}`);
// step.Screenshot contains base64 PNG for audit/debugging
}
import {
RunComputerUseParams,
DomainAuthBinding,
BasicAuthMethod,
BearerTokenAuthMethod,
ComputerUseAuthConfig
} from '@memberjunction/computer-use';
const params = new RunComputerUseParams();
params.Goal = "Login to example.com and check notifications";
params.StartUrl = "https://example.com/login";
// Configure per-domain authentication
const authConfig = new ComputerUseAuthConfig();
// Basic auth for login form (credentials shown to LLM)
authConfig.Bindings.push(new DomainAuthBinding(
["example.com"],
new BasicAuthMethod({
Username: "user@example.com",
Password: "secure-password",
FormLogin: true // LLM will enter credentials into form
})
));
// Bearer token for API subdomain
authConfig.Bindings.push(new DomainAuthBinding(
["api.example.com"],
new BearerTokenAuthMethod({
Token: "eyJhbGciOi..."
})
));
params.Auth = authConfig;
const result = await engine.Run(params);
import { ComputerUseTool, RunComputerUseParams } from '@memberjunction/computer-use';
// Define a tool for the controller LLM to invoke
const weatherTool = new ComputerUseTool<{ city: string }, { temperature: number; condition: string }>(
"get_weather",
"Get current weather for a city",
{
type: "object",
properties: {
city: { type: "string", description: "City name" }
},
required: ["city"]
},
async (args) => {
// Your implementation (API call, database lookup, etc.)
const weather = await fetchWeatherAPI(args.city);
return {
temperature: weather.temp,
condition: weather.condition
};
}
);
const params = new RunComputerUseParams();
params.Goal = "Check the weather in San Francisco using the weather tool";
params.Tools = [weatherTool];
const result = await engine.Run(params);
// Access tool call history
for (const step of result.Steps) {
for (const toolCall of step.ToolCalls || []) {
console.log(`Tool: ${toolCall.ToolName}`);
console.log(`Input: ${JSON.stringify(toolCall.Input)}`);
console.log(`Output: ${JSON.stringify(toolCall.Output)}`);
}
}
const params = new RunComputerUseParams();
params.Goal = "Research competitor pricing on trusted sites only";
params.StartUrl = "https://google.com";
// Only allow specific trusted domains
params.AllowedDomains = [
"google.com",
"competitor1.com",
"competitor2.com",
"*.trusteddomain.com" // Wildcard for subdomains
];
// Block known problematic domains
params.BlockedDomains = [
"ads.example.com",
"tracker.analytics.com"
];
const result = await engine.Run(params);
// Browser will be prevented from navigating to blocked/non-allowed domains
import { RunComputerUseParams, EveryNStepsFrequency, OnStagnationFrequency } from '@memberjunction/computer-use';
const params = new RunComputerUseParams();
params.Goal = "Find and save the top 5 tech news headlines";
// Option 1: Evaluate every 3 steps (cost/accuracy balance)
params.JudgeFrequency = new EveryNStepsFrequency(3);
// Option 2: Evaluate only when stuck (cost optimization)
params.JudgeFrequency = new OnStagnationFrequency(3); // After 3 identical screenshots
// Option 3: Evaluate every step (maximum accuracy, higher cost)
// params.JudgeFrequency = new EveryStepFrequency(); // Default
const result = await engine.Run(params);
// Check judge evaluations
for (const step of result.Steps) {
if (step.JudgeVerdict) {
console.log(`Step ${step.StepNumber} Judge:`);
console.log(` Done: ${step.JudgeVerdict.Done}`);
console.log(` Confidence: ${step.JudgeVerdict.Confidence}`);
console.log(` Reason: ${step.JudgeVerdict.Reason}`);
console.log(` Feedback: ${step.JudgeVerdict.Feedback}`);
}
}
The main orchestrator that manages the entire vision-to-action loop:
Run(params)
→ Initialize Components (NavGuard, AuthHandler, ToolProvider, Judge)
→ Launch Browser
→ Apply Global Auth Callback
→ Navigate to StartUrl
→ Main Step Loop:
├─ Capture Screenshot → Ring Buffer
├─ Build Controller Prompt (goal, screenshots, tools, feedback)
├─ Execute Controller Prompt → LLM reasoning
├─ Parse Response → Actions + Tool Calls
├─ Execute Tool Calls → Wrapped results
├─ Scale Actions (1000x1000 → viewport pixels)
├─ Execute Browser Actions (with NavGuard + Auth)
├─ Evaluate Judge (if frequency indicates)
├─ Check Judge verdict (Done=true → exit)
└─ Track empty steps (3+ consecutive → abort)
→ Close Browser
→ Return ComputerUseResult
Extension Points (protected virtual methods):
executeControllerPrompt() - Override to use custom LLM routingexecuteJudgePrompt() - Override for custom judge executiononStepComplete() - Hook called after each step (logging, persistence)onRunComplete() - Hook called at run completion (cleanup, finalization)BaseBrowserAdapter (abstract interface):
abstract class BaseBrowserAdapter {
abstract Launch(config: BrowserConfig): Promise<void>;
abstract Close(): Promise<void>;
abstract Navigate(url: string): Promise<void>;
abstract CaptureScreenshot(): Promise<string>; // Base64 PNG
abstract ExecuteAction(action: BrowserAction): Promise<ActionResult>;
abstract SetExtraHeaders(headers: Record<string, string>): Promise<void>;
abstract SetCookies(cookies: Cookie[]): Promise<void>;
abstract SetLocalStorage(domain: string, items: Record<string, string>): Promise<void>;
}
PlaywrightBrowserAdapter (default implementation):
Custom Adapters: Implement BaseBrowserAdapter to use Selenium, Puppeteer, or custom browsers.
Judge Verdict Structure:
interface JudgeVerdict {
Done: boolean; // Has the goal been accomplished?
Confidence: number; // 0.0 - 1.0 certainty score
Reason: string; // Explanation of the verdict
Feedback: string; // Actionable guidance for controller
SuggestedNextAction?: string; // Optional hint (not enforced)
}
Judge Types:
HeuristicJudge (zero-cost):
Confidence > 0 when problem detected, Confidence = 0 when inconclusiveLLMJudge (vision-based):
HybridJudge (recommended default):
Lifecycle:
appliedDomains Set prevents re-applicationAuth Methods:
// Basic Auth (FormLogin variant with credential exposure)
new BasicAuthMethod({
Username: "user",
Password: "pass",
FormLogin: true // Credentials shown to controller LLM for form entry
})
// Bearer Token (Authorization header)
new BearerTokenAuthMethod({
Token: "eyJhbGci..."
})
// API Key Header
new APIKeyHeaderAuthMethod({
HeaderName: "X-API-Key",
HeaderValue: "sk-..."
})
// OAuth2 Client Credentials
new OAuthClientCredentialsAuthMethod({
ClientId: "client-id",
ClientSecret: "secret",
TokenEndpoint: "https://auth.example.com/token",
Scopes: ["read", "write"]
})
// Cookie Injection
new CookieInjectionAuthMethod({
Cookies: [{
Name: "session",
Value: "abc123",
Domain: "example.com",
Path: "/",
Secure: true
}]
})
// LocalStorage Injection (pre-populated at context creation)
new LocalStorageInjectionAuthMethod({
Items: {
"auth_token": "xyz789",
"user_id": "12345"
}
})
// Custom Callback
new CustomCallbackAuthMethod({
Callback: async (adapter) => {
await adapter.Navigate("https://example.com/custom-auth");
// Custom auth logic here
}
})
Tool Definition:
const tool = new ComputerUseTool<TInput, TOutput>(
name: string, // Alphanumeric + underscores only
description: string, // LLM-friendly explanation
inputSchema: object, // JSON Schema for validation
handler: async (args: TInput) => Promise<TOutput>,
outputSchema?: object // Optional output documentation
);
Tool Execution:
ToolCallRecordExample Tool Call Record:
interface ToolCallRecord {
ToolName: string;
Input: TInput;
Output?: TOutput;
Error?: string;
DurationMs: number;
}
Purpose: Enforces domain restrictions to prevent the controller from navigating to untrusted sites.
Pattern Matching:
* - Allow all domains (open navigation)example.com - Exact match only*.example.com - All subdomains of example.comPrecedence Rules:
BlockedDomains → BLOCKEDAllowedDomains → ALLOWEDAllowedDomains is empty → ALLOWED (open navigation)The controller LLM reasons about actions in a normalized 1000x1000 space regardless of actual viewport size. This provides viewport independence and consistent prompting.
Flow:
Click(500, 300) = center)(500, 300) normalized → (640, 216) at 1280x720 viewportBenefits:
Purpose: Maintain bounded screenshot history without unbounded memory growth.
Behavior:
ScreenshotHistoryDepth (default: 5)Example with depth=3:
Step 1: [A] → Buffer: [A]
Step 2: [B] → Buffer: [A, B]
Step 3: [C] → Buffer: [A, B, C]
Step 4: [D] → Buffer: [B, C, D] (A evicted)
Step 5: [E] → Buffer: [C, D, E] (B evicted)
Philosophy: Never throw exceptions; wrap all errors in results for graceful handling.
Error Categories (ErrorCategory enum):
BrowserCrash - Browser process terminated unexpectedlyNavigationTimeout - Page load exceeded timeoutElementNotFound - Action target not found in pageLLMError - LLM API call failedLLMParseError - LLM response parsing failedToolExecutionError - Custom tool handler threw exceptionAuthenticationError - Auth method application failedDomainBlocked - Navigation blocked by NavigationGuardCancelled - User called Stop() methodError Wrapping:
interface ComputerUseError {
Message: string;
Category: ErrorCategory;
Step?: number;
OriginalError?: Error;
}
Recovery Strategies:
Required:
params.Goal = "Natural language description of what to accomplish";
Model Configuration:
params.ControllerModel = new ModelConfig("anthropic", "claude-3-5-sonnet-20241022");
params.JudgeModel = new ModelConfig("anthropic", "claude-3-5-sonnet-20241022");
// Optional: Override driver class
params.ControllerModel = new ModelConfig("anthropic", "claude-3-5-sonnet-20241022", "AnthropicLLM");
Browser Configuration:
params.Headless = true; // Hide browser window
params.BrowserConfig.ViewportWidth = 1280;
params.BrowserConfig.ViewportHeight = 720;
params.BrowserConfig.UserAgent = "Custom/1.0";
params.BrowserConfig.NavigationTimeoutMs = 30000; // Page load timeout
params.BrowserConfig.ActionTimeoutMs = 10000; // Action execution timeout
params.BrowserConfig.SlowMo = 100; // Debugging: slow down actions by 100ms
Engine Configuration:
params.MaxSteps = 30; // Maximum controller loop iterations
params.ScreenshotHistoryDepth = 5; // Screenshots in ring buffer
params.ScreenshotDelayMs = 500; // Wait after action before screenshot
Judge Configuration:
import { EveryStepFrequency, EveryNStepsFrequency, OnStagnationFrequency } from '@memberjunction/computer-use';
params.JudgeFrequency = new EveryStepFrequency(); // Default: every step
params.JudgeFrequency = new EveryNStepsFrequency(5); // Every 5 steps
params.JudgeFrequency = new OnStagnationFrequency(3); // After 3 stuck/loop indicators
Navigation Configuration:
params.AllowedDomains = ["example.com", "*.trusted.com", "partner.com"];
params.BlockedDomains = ["ads.example.com", "tracker.com"];
Authentication Configuration:
const authConfig = new ComputerUseAuthConfig();
// Per-domain bindings
authConfig.Bindings.push(new DomainAuthBinding(
["example.com", "*.example.com"],
new BearerTokenAuthMethod({ Token: "..." })
));
// Global callback for SSO/MFA
authConfig.GlobalCallback = async (adapter) => {
await adapter.Navigate("https://sso.example.com");
// Custom SSO flow
};
params.Auth = authConfig;
Custom Prompts:
// Override controller prompt (template variables supported)
params.ControllerPrompt = `
You are controlling a browser to accomplish this goal: {{goal}}
Current step: {{stepNumber}}/{{maxSteps}}
Current URL: {{url}}
Previous judge feedback: {{judgeFeedback}}
Analyze the screenshot and determine the next action...
`;
// Override judge prompt
params.JudgePrompt = `
Goal: {{goal}}
Current URL: {{url}}
Has the goal been accomplished? Respond with JSON...
`;
Available Template Variables:
{{goal}} - User's natural language goal{{stepNumber}} - Current step number{{maxSteps}} - Maximum steps configured{{url}} - Current page URL{{judgeFeedback}} - Feedback from most recent judge verdict{{credentials}} - Available auth credentials (for FormLogin scenarios){{tools}} - JSON schema of available toolsThe controller LLM can request these actions in its response:
// Click at normalized coordinates
{
Type: "Click",
X: 500, // 0-1000 normalized
Y: 300, // 0-1000 normalized
Button: "Left", // "Left" | "Right" | "Middle"
ClickCount: 1 // Single, double, triple click
}
// Type text
{ Type: "Type", Text: "hello world" }
// Press keys (supports combinations)
{ Type: "Keypress", Key: "Enter" }
{ Type: "Keypress", Key: "Shift+A" }
{ Type: "Keypress", Key: "ControlOrMeta+C" } // Ctrl on Windows/Linux, Cmd on Mac
// Individual key state
{ Type: "KeyDown", Key: "Shift" }
{ Type: "KeyUp", Key: "Shift" }
// Scroll
{ Type: "Scroll", DeltaX: 0, DeltaY: -100 } // Negative = up
// Wait (for loading, animations)
{ Type: "Wait", DurationMs: 2000 }
// Navigate to URL
{ Type: "Navigate", Url: "https://example.com" }
// Browser navigation
{ Type: "GoBack" }
{ Type: "GoForward" }
{ Type: "Refresh" }
interface ComputerUseResult {
Success: boolean; // Goal accomplished?
Status: ComputerUseStatus; // Terminal state
Steps: StepRecord[]; // Complete history
TotalSteps: number;
TotalDurationMs: number;
FinalScreenshot?: string; // Base64 PNG
FinalUrl?: string;
FinalJudgeVerdict?: JudgeVerdict;
Error?: ComputerUseError; // If Status is 'Error'
}
type ComputerUseStatus =
| "Completed" // Goal achieved (judge verdict Done=true)
| "Failed" // Judge determined goal failed
| "MaxStepsReached" // Hit MaxSteps without completion
| "Error" // Fatal error occurred
| "Cancelled"; // Stop() called
interface StepRecord {
StepNumber: number; // 1-indexed
Screenshot: string; // Base64 PNG
ControllerReasoning: string; // LLM's explanation
ActionsRequested: BrowserAction[];
ActionResults: ActionResult[];
ToolCalls?: ToolCallRecord[];
JudgeVerdict?: JudgeVerdict; // If evaluated this step
DurationMs: number;
Error?: ComputerUseError;
Url: string;
}
The engine is designed to be subclassed for custom integrations:
import { ComputerUseEngine, RunComputerUseParams } from '@memberjunction/computer-use';
class CustomComputerUseEngine extends ComputerUseEngine {
// Override to use custom LLM routing
protected override async executeControllerPrompt(
request: ControllerPromptRequest
): Promise<string> {
// Your custom LLM integration
const response = await myCustomLLMService.call({
prompt: request.Prompt,
images: request.Screenshots,
tools: request.Tools
});
return response.text;
}
// Override to use custom judge execution
protected override async executeJudgePrompt(
request: JudgePromptRequest
): Promise<string> {
// Your custom judge logic
return await myJudgeService.evaluate(request);
}
// Hook called after each step
protected override async onStepComplete(
step: StepRecord,
context: RunContext
): Promise<void> {
// Custom logging, persistence, etc.
await this.saveStepToDatabase(step);
}
// Hook called at run completion
protected override async onRunComplete(
result: ComputerUseResult,
context: RunContext
): Promise<void> {
// Cleanup, finalization, etc.
await this.updateDashboard(result);
}
}
const engine = new CustomComputerUseEngine();
const result = await engine.Run(params);
Implement BaseBrowserAdapter to use alternative browser automation libraries:
import { BaseBrowserAdapter, BrowserConfig, BrowserAction, ActionResult } from '@memberjunction/computer-use';
class SeleniumBrowserAdapter extends BaseBrowserAdapter {
private driver: WebDriver;
async Launch(config: BrowserConfig): Promise<void> {
// Initialize Selenium WebDriver
this.driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(new ChromeOptions().headless())
.build();
}
async CaptureScreenshot(): Promise<string> {
return await this.driver.takeScreenshot();
}
async ExecuteAction(action: BrowserAction): Promise<ActionResult> {
switch (action.Type) {
case "Click":
// Selenium click implementation
break;
case "Type":
// Selenium type implementation
break;
// ... implement all action types
}
}
// ... implement remaining abstract methods
}
// Use custom adapter
const engine = new ComputerUseEngine();
const params = new RunComputerUseParams();
params.BrowserAdapter = new SeleniumBrowserAdapter();
For long-running tasks, support graceful cancellation:
const engine = new ComputerUseEngine();
const params = new RunComputerUseParams();
params.Goal = "Long-running research task...";
// Start execution (non-blocking)
const resultPromise = engine.Run(params);
// Cancel after 60 seconds
setTimeout(() => {
engine.Stop(); // Graceful cancellation after current step
}, 60000);
const result = await resultPromise;
if (result.Status === "Cancelled") {
console.log("Run was cancelled");
console.log(`Completed ${result.TotalSteps} steps before cancellation`);
}
Handle complex authentication flows with global callbacks:
const authConfig = new ComputerUseAuthConfig();
// Global callback for SSO flow
authConfig.GlobalCallback = async (adapter) => {
// Navigate to SSO login
await adapter.Navigate("https://sso.example.com/login");
// Wait for redirect
await adapter.ExecuteAction({ Type: "Wait", DurationMs: 2000 });
// Enter credentials (these would be from secure storage)
await adapter.ExecuteAction({ Type: "Type", Text: "username" });
await adapter.ExecuteAction({ Type: "Keypress", Key: "Tab" });
await adapter.ExecuteAction({ Type: "Type", Text: "password" });
await adapter.ExecuteAction({ Type: "Keypress", Key: "Enter" });
// Wait for MFA prompt
await adapter.ExecuteAction({ Type: "Wait", DurationMs: 3000 });
// Handle MFA (could integrate with authenticator app)
const mfaCode = await getMFACode();
await adapter.ExecuteAction({ Type: "Type", Text: mfaCode });
await adapter.ExecuteAction({ Type: "Keypress", Key: "Enter" });
};
params.Auth = authConfig;
any types - this package maintains full type safety*) for security-sensitive tasksEveryNStepsFrequency(5) or OnStagnationFrequency(3) for long tasksAllowedDomains for production tasksFormLogin: true - credentials are shown to LLMHeadless: false to watch the browserSlowMo: 1000 to see each action clearlystep.ControllerReasoning to understand LLM decisionsstep.Screenshot base64 to see what the LLM sawEveryStepFrequency() for detailed evaluationstep.ActionResults for execution detailsstep.ToolCalls for tool execution historystep.JudgeVerdict.Feedback for guidanceCause: Vision model may not detect state changes from previous actions.
Solution:
ScreenshotDelayMs to allow more render timeCause: Page load exceeds NavigationTimeoutMs.
Solution:
params.BrowserConfig.NavigationTimeoutMs = 60000Cause: Goal may be ambiguous or too complex for vision-based evaluation.
Solution:
Cause: JSON Schema mismatch or handler exceptions.
Solution:
step.ToolCalls[].Error for specific failure reasonsCause: Goal too complex, controller stuck, or judge not evaluating.
Solution:
MaxSteps for complex tasksWe welcome contributions! Please see the main MemberJunction Contributing Guide for details.
# Clone repository
git clone https://github.com/memberjunction/mj.git
cd mj
# Install dependencies
npm install
# Build the package
cd packages/AI/ComputerUse
npm run build
# Run tests
npm test
The package ships standalone default prompts for the controller and judge LLMs — DEFAULT_CONTROLLER_PROMPT / DEFAULT_JUDGE_PROMPT — used by the MJ-independent ComputerUseEngine / LLMJudge when no custom prompt is supplied. These are not the package's own copy of the text — they are composed from shared parts so this layer (Layer 1) and the MJ metadata templates (Layer 2) can never silently diverge.
How it works:
metadata/prompts/templates/computer-use/_includes/*.md (controller-actions.md, controller-response-format.md, judge-core.md).prebuild step (scripts/generate-prompt-parts.mjs, runs automatically before npm run build) reads those partials and emits src/prompts/prompt-parts.generated.ts — each partial as an escaped string const (CONTROLLER_ACTIONS, CONTROLLER_RESPONSE_FORMAT, JUDGE_CORE).default-controller.ts / default-judge.ts then compose DEFAULT_CONTROLLER_PROMPT / DEFAULT_JUDGE_PROMPT from those generated consts plus their per-layer top section (intro, goal, current state) and dynamic-section markers.{@include ./_includes/...} directive.A drift-guard test (src/__tests__/prompt-single-source.test.ts) asserts both layers carry the same shared blocks.
To change shared prompt text, edit the relevant _includes/*.md partial and re-run npm run prebuild — both layers update from the single source. Never hand-edit src/prompts/prompt-parts.generated.ts; it is regenerated on every build.
For more details, see scripts/README.md.
This package is part of the MemberJunction project and is licensed under the MIT License. See LICENSE for details.