How to Extend ego-browser with Custom Agent Helpers in agent_helpers.js

Create an agent_helpers.js file in your agent's workspace directory; ego-browser automatically loads and merges its exported functions into the helper context at runtime.

The ego-browser runtime from citrolabs/ego-lite provides a built-in extension system that lets you add custom functionality without modifying core source code. When you implement custom agent helpers with the agent_helpers.js extension system, your functions become first-class citizens alongside built-in helpers like click(), navigate(), and cdp().

This guide covers exactly how the loading mechanism works, where to place your file, and how to structure your exports for seamless integration.

How the agent_helpers.js Loader Works

The extension system is implemented in src/helpers.ts within the helperContext() function. At line 853, the runtime checks for the presence of agent_helpers.js in the current agent workspace and dynamically imports it:

// src/helpers.ts (excerpt around line 853)
const path = join(state.agentWorkspace(), "agent_helpers.js");
if (await exists(path)) {
  const mod = await import(path);
  Object.assign(helpers, mod);   // merge exports into helper surface
}

This loading occurs once per agent session, immediately before your script executes. The Object.assign() call means both named exports and default export members become available as top-level helper functions.

The state.agentWorkspace() call resolves to the active task's working directory, ensuring isolation between different agent workspaces.

File Location and Naming Requirements

Your agent_helpers.js file must follow strict placement rules:

  • Exact filename: agent_helpers.js (case-sensitive)
  • Location: Root of the agent workspace directory returned by state.agentWorkspace()
  • Format: Standard JavaScript (ES modules) with import/export syntax

The runtime uses join() to construct the path, so no subdirectories or alternative naming patterns are recognized.

Export Patterns for Custom Helpers

Export individual functions for clear, discoverable APIs:

// agent_helpers.js
/**
 * Returns the current page title in uppercase.
 */
export async function pageTitleUpper() {
  const title = await ego.eval(`document.title`);
  return String(title).toUpperCase();
}

/**
 * Clicks an element and waits for navigation to complete.
 */
export async function clickAndWait(selector) {
  await ego.click(selector);
  await ego.waitForNavigation({ timeout: 30 });
}

Default Export (Object Pattern)

Bundle multiple helpers in a single object:

// agent_helpers.js
export default {
  /** Returns URL without query parameters. */
  async cleanUrl() {
    const url = await ego.eval('window.location.href');
    return url.split('?')[0];
  },
  
  /** Checks if element exists with retry logic. */
  async exists(selector, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
      const found = await ego.eval(`!!document.querySelector(${JSON.stringify(selector)})`);
      if (found) return true;
      await ego.sleep(500);
    }
    return false;
  }
};

Both patterns merge into the same helper namespace. Avoid naming collisions with built-in helpers: click, navigate, cdp, eval, sleep, type, press, and waitForNavigation are reserved.

Using Custom Helpers in Agent Scripts

Once loaded, custom helpers are invoked identically to built-ins:

// stdin script passed to ego-browser
await clickAndWait('a.login');
const title = await pageTitleUpper();
console.log('PAGE TITLE:', title);

const url = await cleanUrl();  // from default export
console.log('Clean URL:', url);

The runtime resolves these calls through the merged helpers object, ensuring consistent await semantics and error handling.

Complete Working Example

Directory structure:


/workspace/task-001/
├── agent_helpers.js
└── script.js   # optional: can also pass via stdin

agent_helpers.js:

/**
 * Smart form helper: fills inputs using label text matching.
 */
export async function fillByLabel(labelText, value) {
  const selector = await ego.eval(`
    [...document.querySelectorAll('label')]
      .find(l => l.textContent.includes(${JSON.stringify(labelText)}))
      ?.getAttribute('for')
  `);
  if (!selector) throw new Error(`Label "${labelText}" not found`);
  await ego.type(`#${selector}`, value);
}

/**
 * Extracts all links matching a domain pattern.
 */
export async function extractLinks(domainPattern) {
  const links = await ego.eval(`
    [...document.querySelectorAll('a[href]')]
      .map(a => a.href)
      .filter(href => /${domainPattern}/.test(href))
  `);
  return links;
}

Invocation:

echo 'await fillByLabel("Email", "user@example.com");' | ego-browser

Runtime Behavior and Lifecycle

Aspect Behavior
Load timing Once per session, before script execution
Reload trigger New workspace initialization or task reset
Caching File is re-imported fresh each session
Scope Same sandbox as built-in helpers; no host filesystem access
Error handling Import failures throw and terminate agent startup

Changes to agent_helpers.js require a new session to take effect—there is no hot-reload mechanism.

Source Reference Files

File Purpose
package/ego-browser/src/helpers.ts Core loader implementation at line 853
AGENTS.md High-level documentation of the helper system
package/ego-browser/src/helpers.test.mjs Test coverage for custom helper loading

Summary

  • Place agent_helpers.js in the root of your agent workspace directory
  • Export functions using named exports or default export object patterns
  • Access custom helpers directly by name with standard await syntax
  • Reload happens automatically on new sessions—modify, then restart

The extension system in ego-browser transforms agent_helpers.js from a simple module into an integrated part of the agent's capability surface, maintaining the same security boundaries and ergonomic patterns as core functionality.

Frequently Asked Questions

Can I use TypeScript for agent_helpers.js?

No—the runtime specifically looks for agent_helpers.js and uses dynamic import() on that exact filename. You must transpile TypeScript to JavaScript before deployment. The source analysis shows no .ts extension handling in the loader logic.

What happens if my helper name conflicts with a built-in?

The Object.assign(helpers, mod) call at line 853 means your export overwrites the built-in. This is powerful but dangerous—avoid redefining core helpers like click or navigate unless intentionally patching behavior.

Can I import external npm packages in agent_helpers.js?

Only if the package is pre-installed in the workspace and resolvable by Node.js's module resolution. The dynamic import() runs in the same context as the main process, but network-dependent imports may fail in sandboxed environments. Stick to built-in Node.js modules for reliability.

How do I debug a helper that isn't loading?

Verify three things: (1) exact filename agent_helpers.js in workspace root, (2) valid ES module syntax (no require()), and (3) no syntax errors preventing import(). Add console.log at the top level of agent_helpers.js—output appears in agent logs if loading succeeds.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →