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
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ agents: {
model: 'gpt-oss-20b', // Override default model
enabled: true, // Enable/disable agent
systemPrompt: '...', // Append to system prompt
beforeHook: { /* ... */ }, // Run before agent executes
afterHook: { /* ... */ }, // Run after agent completes
},
}
```
Expand All @@ -172,6 +174,10 @@ agents: {
| `model` | `string` | Model to use for this agent (overrides default) |
| `enabled` | `boolean` | Enable or disable the agent |
| `systemPrompt` | `string` | Additional instructions appended to the agent's system prompt |
| `beforeHook` | `Hook \| HookPatternMap` | Code to run before agent execution |
| `afterHook` | `Hook \| HookPatternMap` | Code to run after agent execution |

See [Agent Hooks](./hooks.md) for detailed hook configuration.

### Researcher Agent Options

Expand Down Expand Up @@ -396,6 +402,7 @@ export default {

- [AI Providers](./providers.md) - Provider setup examples
- [Agents](./agents.md) - Agent descriptions and workflows
- [Agent Hooks](./hooks.md) - Custom code before/after agent execution
- [Researcher Agent](./researcher.md) - Researcher configuration and usage
- [Knowledge Files](./knowledge.md) - Domain knowledge format
- [Observability](./observability.md) - Langfuse integration
238 changes: 238 additions & 0 deletions docs/hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Agent Hooks

Hooks allow you to execute custom code before or after specific agents run. They provide fine-grained control over page preparation and cleanup on a per-agent basis.

> [!NOTE]
> For simple page automation (waiting, clicking cookie banners), prefer using [Knowledge Files](./knowledge.md) with `wait`, `waitForElement`, or `code` fields. Hooks are best when you need different behavior for different agents.

## When to Use Hooks vs Knowledge

| Use Case | Solution |
|----------|----------|
| Wait for element on all page visits | Knowledge: `waitForElement` |
| Dismiss cookie banner on page load | Knowledge: `code` |
| Wait for network idle only during research | Hook: `researcher.beforeHook` |
| Clean up test data after each test | Hook: `tester.afterHook` |
| Different waits for navigation vs testing | Hooks for each agent |

## Configuration

Hooks are configured per-agent in `explorbot.config.js`:

```javascript
export default {
ai: {
provider: myProvider,
model: 'gpt-4o',
agents: {
navigator: {
beforeHook: {
type: 'playwright',
hook: async ({ page, url }) => {
await page.waitForLoadState('networkidle');
}
}
},
tester: {
afterHook: {
type: 'codeceptjs',
hook: async ({ I, url }) => {
await I.executeScript(() => localStorage.clear());
}
}
}
}
}
}
```

## Hook Types

### Playwright Hooks

Direct access to Playwright page object:

```javascript
beforeHook: {
type: 'playwright',
hook: async ({ page, url }) => {
await page.waitForLoadState('networkidle');
await page.locator('.loading').waitFor({ state: 'hidden' });
}
}
```

### CodeceptJS Hooks

Use familiar CodeceptJS `I` actor:

```javascript
beforeHook: {
type: 'codeceptjs',
hook: async ({ I, url }) => {
await I.waitForElement('.page-ready');
await I.wait(1);
}
}
```

## URL Pattern Matching

Apply different hooks based on URL patterns:

```javascript
researcher: {
beforeHook: {
'/login': {
type: 'codeceptjs',
hook: async ({ I }) => await I.waitForElement('#login-form')
},
'/admin/*': {
type: 'playwright',
hook: async ({ page }) => await page.waitForLoadState('networkidle')
},
'/api/*': {
type: 'codeceptjs',
hook: async ({ I }) => await I.wait(2)
}
}
}
```

### Pattern Syntax

| Pattern | Matches |
|---------|---------|
| `/login` | Exact path `/login` |
| `/admin/*` | `/admin` and any path starting with `/admin/` |
| `*` | All URLs (fallback) |
| `^/users/\d+$` | Regex: `/users/` followed by digits |
| `**/*.html` | Glob: any `.html` file |

## Supported Agents

| Agent | beforeHook | afterHook | Description |
|-------|------------|-----------|-------------|
| `navigator` | After navigation | After page capture | Browser navigation |
| `researcher` | After navigation | After research complete | Page analysis |
| `tester` | Before test loop | After test loop | Test execution |
| `captain` | Before handling command | After command complete | User commands |

> [!WARNING]
> The `planner` agent does not support hooks as it doesn't interact with the browser.

## Examples

### Wait for SPA to Load

```javascript
navigator: {
beforeHook: {
type: 'playwright',
hook: async ({ page }) => {
await page.waitForFunction(() => {
return window.__APP_READY__ === true;
});
}
}
}
```

### Dismiss Modals Before Research

```javascript
researcher: {
beforeHook: {
type: 'codeceptjs',
hook: async ({ I }) => {
const modalVisible = await I.grabNumberOfVisibleElements('.modal-overlay');
if (modalVisible > 0) {
await I.click('.modal-close');
await I.wait(0.5);
}
}
}
}
```

### Clean Up After Tests

```javascript
tester: {
afterHook: {
type: 'playwright',
hook: async ({ page }) => {
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
}
}
}
```

### Different Behavior per URL

```javascript
tester: {
beforeHook: {
'/checkout': {
type: 'codeceptjs',
hook: async ({ I }) => {
// Ensure cart has items before checkout tests
await I.executeScript(() => {
if (!localStorage.getItem('cart')) {
localStorage.setItem('cart', JSON.stringify([{ id: 1, qty: 1 }]));
}
});
}
},
'/admin/*': {
type: 'codeceptjs',
hook: async ({ I }) => {
// Ensure admin session
await I.waitForElement('.admin-header', 5);
}
}
}
}
```

## Error Handling

Hook errors are logged but don't stop agent execution:

```javascript
beforeHook: {
type: 'codeceptjs',
hook: async ({ I }) => {
try {
await I.waitForElement('.optional-banner', 2);
await I.click('.dismiss');
} catch {
// Banner not present, continue
}
}
}
```

## Execution Flow

```
┌─────────────────────────────────────────────────────────┐
│ Agent Execution │
├─────────────────────────────────────────────────────────┤
│ 1. Agent starts │
│ 2. Navigate to URL (if applicable) │
│ 3. ▶ beforeHook executes │
│ 4. Agent performs main work │
│ 5. ▶ afterHook executes │
│ 6. Agent completes │
└─────────────────────────────────────────────────────────┘
```

## See Also

- [Knowledge Files](./knowledge.md) - Page-level automation with `wait`, `waitForElement`, `code`
- [Configuration](./configuration.md) - Full configuration reference
- [Agents](./agents.md) - Agent descriptions and workflows
99 changes: 99 additions & 0 deletions docs/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,99 @@ Notes:
| `title` | Human-readable title (optional) |
| Custom fields | Any additional metadata for agents |

## Page Automation

Knowledge files can include automation commands that execute when navigating to matching pages. This is useful for handling loading states, cookie banners, or page-specific setup.

### Available Fields

| Field | Type | Description |
|-------|------|-------------|
| `wait` | `number` | Wait for specified seconds after page load |
| `waitForElement` | `string` | Wait for element to appear (CSS selector) |
| `code` | `string` | Execute CodeceptJS code after navigation |
| `statePush` | `boolean` | Use `history.pushState` instead of full navigation |

### Wait for Page Load

```markdown
---
url: /dashboard
wait: 2
waitForElement: '.dashboard-loaded'
---

Dashboard requires data to load before interaction.
```

### Execute Custom Code

```markdown
---
url: /app/*
code: |
I.waitForElement('.app-ready');
I.click('.cookie-accept');
I.wait(1);
---

App pages need cookie consent dismissed and loading complete.
```

### CodeceptJS Effects

Knowledge code has access to CodeceptJS effects for error handling and retries:

| Effect | Purpose |
|--------|---------|
| `tryTo(fn)` | Execute without failing - returns `true`/`false` |
| `retryTo(fn, maxTries, interval)` | Retry on failure with polling |
| `within(context, fn)` | Execute within a specific element context |

**Example with effects:**

```markdown
---
url: /dashboard
code: |
await tryTo(() => I.click('.cookie-dismiss'));
await retryTo(() => {
I.click('Reload Data');
I.waitForElement('.data-loaded');
}, 5, 500);
---

Dashboard may show cookie banner. Data loads asynchronously - retry reload if needed.
```

> [!NOTE]
> Effects are async - use `await` when calling them in knowledge code.

### SPA Navigation

For single-page apps where full page reload breaks state:

```markdown
---
url: /settings/*
statePush: true
---

Settings uses client-side routing. Use pushState to preserve app state.
```

> [!TIP]
> Use knowledge automation for page-specific behaviors. For agent-specific logic (like running code only during testing), use [Agent Hooks](./hooks.md) instead.

### Execution Order

When navigating to a page, automation executes in this order:

1. Navigation (`I.amOnPage()` or `history.pushState`)
2. `wait` (if specified)
3. `waitForElement` (if specified)
4. `code` (if specified)

## What to Document

### Authentication
Expand Down Expand Up @@ -161,3 +254,9 @@ When an agent operates on a page, it receives relevant knowledge based on URL ma
```

Files are named based on URL pattern. Multiple entries for the same URL are appended to the same file.

## See Also

- [Agent Hooks](./hooks.md) - Per-agent custom code execution
- [Configuration](./configuration.md) - Full configuration reference
- [Page Interaction](./page-interaction.md) - How agents interact with pages
Loading
Loading