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/exportsyntax
The runtime uses join() to construct the path, so no subdirectories or alternative naming patterns are recognized.
Export Patterns for Custom Helpers
Named Exports (Recommended)
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.jsin 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
awaitsyntax - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →