Files
BrowserOS/reference-code/old-lib/tools/browser-navigation/NavigationTool.ts
Felarof 8245dfe0ff Rewrite Agent Loop (#7)
* clean-up bunch of files for re-write

* more clean-up and adding basic agent

* Minor fix moved types into respective files.

* Deleted bunch of old files

backup

Update gitignore

Deleted a bunch of files

Remove message manager

Deleted old docs

Update rules

rename Profiler to profiler

* Temporarily adding old code

* Adding two small things back

* backup

* Implemented LangChainProvider and updated cursor rules

backup

LangChainProvider

curosr rules

* Implement tests for LangChainProvider -- unit test and integration test

integration test passes

integration test backup

* Tool Design

Tools Desing

tools design

* NavigationTool ready

NavigationTool ready

NavigationTool ready

NaivgationTool ready

backup

* MessageManager

MessageManager

backup

* Fixed integration test

* Agent design new

Updated agent design and added bunch of /NTN commands

agent new design

* Delete old agent design

* MessageManagerReadOnly class

* PlannerTool ready

PlannerTool almost ready

* ToolManager and DoneTool

* Integration of BrowserAgent

* BrowserAgent implementation v0.1

* BrowserAgent small fix v0.2

* Tool calling design

too call design

tool design claude

* Update agent tool design with // NTN

* add zod-to-json npm install

* BrowserAGent v0.3

* BrowserAgent v0.4

* BrowserAgent v0.5

* fixes

* Build error fixes in my NEWLY added code

build errors fix

* Build error fixes in old code (integration work)

backup

* Comment StreamEventProcessor for now, it is not used

* Small build error fix

* Small rename

* Added integration test to check structuredLLM and changed to 4o-mini

change default to nxtscape

integration test

* Small docstring

* Simplified BrowserAgent code and added integration test

Simplified BrowserAgent code

BrowserAGent integrationt est

* Update CLAUDE.md with project memory and instructions on how to write code

Update CLAUDE.md with project memory and instructions on how to write code

Project Memory

* Just a mova.. Moved ToolManager outside. Build works.

* TabOperations tool

TabOperations Tool and fixing some test

tab operations

* Update CLAUDE.md

* Added ClassificationTool

classifiction tool

classification prommpt

* Refactored and simplified PlannerTool unit test and integration test

* Updated Plnnaer tool

* Update CLAUDE.md

* BrowserAgent modified to do classification

BrowserAgent with classification

* minor fix to ToolManager

* Instead of ToolCall and ToolResult -- just updating message manager once

* minor fix to BrowserAgent integration test

* Changed done to "done_tool"

* Updated CLAUDE.md to reflect understanding of claude

* Uncommented stream event processor

* Renamed EventBus to StreamEventBus

* Commented StreamEventProcessor

* Event Processor

* Integrated EventProcessor with BrowserAgent

Added EventProcessor to BrowserAgetn

* Renamed StreamEventBus to EventBus

* Made EventBus required parameter in ExecutionContext

* PlanGenerator rewrite

PlanGenerator rewrite

backup

* For simple task, explicitly tell it to call done tool

* Max attempts for simple task

* backup

* Revert "backup"

This reverts commit 7d79a3d4d5774bfef79ec9827878b74edad3593f.

* Consolidating where EventBus and EventProcessor are created and initialized

backup

* Update CLAUDE.md

Update CLAUDE.md

* Improving agent loop code

Cleaned up processTooCall

classification task

* Create test-writer subAgent

test-agent-prompt

test agent prompt

test-agent-prompt

Update test-writer.md

* BrowserAgent test

Browseragent test

BrowserAgent test

* BrowserAgent refactor

backup

backup

* Minor fixes

* Minor fix

* minor change -- NEW AGENT LOOP IS WORKING WELL

* Update cursor rules

* Small change

* Improved BrowserAgent integration test

Improved BrowserAgent integration test

* Small change

* Update CLAUDE.md

* Different tools

* FindElementTool is ready

Find element update

backup

find element backup

* Updated to test strings to say "tests..."

* ScrollTool is ready

* RefreshStateTool is updated as well

* MessageManager updated

* SearchTool is ready

backup

* Interaction Element is also ready

* Add debugMessage emitter

* ValidatorTool ready and tests are passing

Validation Tool

validator tool

backup

backup

* GroupTabs tool ready

* Registered all the tools

* Planning changed to 5 steps

* BrowserAgent integration test fix

* Minor string changes

* backup

* Removed too many confusing events in EventProcessor -- there is only event.info right now

* Abort control implemented

backup

Abort

* Formatter for toolResult

Formatter for toolResult

backup

* Always render using Markdown

* Minor fix

---------

Co-authored-by: Nikhil Sonti <nikhilsv92@gmail.com>
2025-07-29 08:14:45 -07:00

414 lines
12 KiB
TypeScript

import { z } from 'zod';
import { NxtscapeTool } from '../base/NxtscapeTool';
import { ToolConfig } from '../base/ToolConfig';
import { ExecutionContext } from '@/lib/runtime/ExecutionContext';
import { BrowserPage } from '@/lib/browser/BrowserPage';
/**
* Enum for navigation operations
*/
export const NavigationOperationTypeEnum = z.enum([
'go_to_url', // Navigate to a URL
'go_back', // Go back to previous page
'go_forward', // Go forward to next page
'refresh' // Refresh current page
]);
export type NavigationOperationType = z.infer<typeof NavigationOperationTypeEnum>;
/**
* Schema for navigation tool input
*/
export const NavigationInputSchema = z.object({
operationType: NavigationOperationTypeEnum, // The operation to perform
url: z.string().optional(), // URL for go_to_url operation
intent: z.string().optional() // Optional description of why this navigation is being performed
});
export type NavigationInput = z.infer<typeof NavigationInputSchema>;
/**
* Schema for navigation tool output
*/
export const NavigationOutputSchema = z.object({
success: z.boolean(), // Whether the operation succeeded
operationType: NavigationOperationTypeEnum, // Operation that was performed
message: z.string(), // Human-readable result message
url: z.string().optional(), // Current URL after navigation
title: z.string().optional() // Current page title after navigation
});
export type NavigationOutput = z.infer<typeof NavigationOutputSchema>;
/**
* Unified tool for navigation operations
*/
export class NavigationTool extends NxtscapeTool<NavigationInput, NavigationOutput> {
constructor(executionContext: ExecutionContext) {
const config: ToolConfig<NavigationInput, NavigationOutput> = {
name: 'navigate',
description: 'Perform navigation operations. Operations: "go_to_url" (navigate to a URL), "go_back" (go to previous page), "go_forward" (go to next page), "refresh" (refresh current page). Always pass operationType. Only pass url when using go_to_url.',
category: 'navigation',
version: '1.0.0',
inputSchema: NavigationInputSchema,
outputSchema: NavigationOutputSchema,
examples: [
{
description: 'Navigate to a URL',
input: {
operationType: 'go_to_url',
url: 'https://www.google.com',
intent: 'Going to Google homepage'
},
output: {
success: true,
operationType: 'go_to_url',
message: 'Navigated to https://www.google.com',
url: 'https://www.google.com',
title: 'Google'
}
},
{
description: 'Go back to previous page',
input: {
operationType: 'go_back',
intent: 'Returning to previous page'
},
output: {
success: true,
operationType: 'go_back',
message: 'Navigated back to previous page',
url: 'https://example.com',
title: 'Example Domain'
}
},
{
description: 'Refresh current page',
input: {
operationType: 'refresh',
intent: 'Refreshing to get latest content'
},
output: {
success: true,
operationType: 'refresh',
message: 'Page refreshed successfully',
url: 'https://example.com',
title: 'Example Domain'
}
}
],
streamingConfig: {
displayName: 'Navigate',
icon: '🧭',
progressMessage: 'Performing navigation...'
}
};
super(config, executionContext);
}
/**
* Override: Generate contextual display message based on operation
*/
getProgressMessage(args: NavigationInput): string {
try {
// Note: args should already be parsed by StreamEventProcessor
const operationType = args?.operationType;
const intent = args?.intent;
// Use intent if provided, otherwise generate based on operation
if (intent) {
return intent;
}
switch (operationType) {
case 'go_to_url':
return args?.url ? `Navigating to ${args.url}` : 'Navigating to URL';
case 'go_back':
return 'Going back to previous page';
case 'go_forward':
return 'Going forward to next page';
case 'refresh':
return 'Refreshing the page';
default:
return 'Performing navigation...';
}
} catch {
return 'Performing navigation...';
}
}
/**
* Override: Format result based on operation type
*/
FormatResultForUI(output: NavigationOutput): string {
if (!output.success) {
return `${output.message}`;
}
let result = '';
switch (output.operationType) {
case 'go_to_url':
result = '🌐 Navigated to ';
if (output.url) {
try {
const hostname = new URL(output.url).hostname;
result += hostname;
} catch {
result += output.url;
}
} else {
result += 'new page';
}
// Add page title if available
if (output.title) {
result += `\n📄 **Page:** ${output.title}`;
}
// Add full URL if available
if (output.url) {
result += `\n🔗 **URL:** ${output.url}`;
}
return result;
case 'go_back':
result = '⬅️ Went back to previous page';
if (output.title) {
result += `\n📄 **Page:** ${output.title}`;
}
return result;
case 'go_forward':
result = '➡️ Went forward to next page';
if (output.title) {
result += `\n📄 **Page:** ${output.title}`;
}
return result;
case 'refresh':
result = '🔄 Page refreshed';
if (output.title) {
result += `\n📄 **Page:** ${output.title}`;
}
return result;
default:
return `${output.message}`;
}
}
protected async execute(input: NavigationInput): Promise<NavigationOutput> {
// Validate inputs for operations that need them
if (input.operationType === 'go_to_url' && !input.url) {
return {
success: false,
operationType: input.operationType,
message: 'go_to_url operation requires a url'
};
}
try {
// Get the current page
const page = await this.executionContext.browserContext.getCurrentPage();
// Execute the operation
switch (input.operationType) {
case 'go_to_url':
return await this.navigateToUrl(page, input.url!);
case 'go_back':
return await this.goBack(page);
case 'go_forward':
return await this.goForward(page);
case 'refresh':
return await this.refreshPage(page);
default:
return {
success: false,
operationType: 'go_to_url',
message: 'Invalid operation type specified'
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
operationType: input.operationType,
message: `Navigation failed: ${errorMessage}`
};
}
}
/**
* Navigate to a URL
*/
private async navigateToUrl(page: BrowserPage, url: string): Promise<NavigationOutput> {
try {
// Normalize URL - add protocol if missing
let normalizedUrl = url.trim();
if (!normalizedUrl.match(/^https?:\/\//i)) {
// Check if it looks like a domain
if (normalizedUrl.match(/^[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}/)) {
normalizedUrl = `https://${normalizedUrl}`;
} else {
// Might be a search query, use Google search
normalizedUrl = `https://www.google.com/search?q=${encodeURIComponent(normalizedUrl)}`;
}
}
await page.navigateTo(normalizedUrl);
// Wait a bit for the page to settle
await new Promise(resolve => setTimeout(resolve, 1000));
// Get current page info
const currentUrl = page.url();
const currentTitle = await page.title();
return {
success: true,
operationType: 'go_to_url',
message: `Navigated to ${normalizedUrl}`,
url: currentUrl,
title: currentTitle
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Check for specific error types
if (errorMessage.includes('not allowed')) {
return {
success: false,
operationType: 'go_to_url',
message: `URL not allowed: ${url}. This URL is restricted by security policy.`
};
}
return {
success: false,
operationType: 'go_to_url',
message: `Failed to navigate to ${url}: ${errorMessage}`
};
}
}
/**
* Go back to previous page
*/
private async goBack(page: BrowserPage): Promise<NavigationOutput> {
try {
await page.goBack();
// Wait a bit for the page to settle
await new Promise(resolve => setTimeout(resolve, 1000));
// Get current page info
const currentUrl = page.url();
const currentTitle = await page.title();
return {
success: true,
operationType: 'go_back',
message: 'Navigated back to previous page',
url: currentUrl,
title: currentTitle
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Check if there's no history to go back to
if (errorMessage.includes('Cannot navigate back')) {
return {
success: false,
operationType: 'go_back',
message: 'Cannot go back - no previous page in history'
};
}
return {
success: false,
operationType: 'go_back',
message: `Failed to go back: ${errorMessage}`
};
}
}
/**
* Go forward to next page
*/
private async goForward(page: BrowserPage): Promise<NavigationOutput> {
try {
await page.goForward();
// Wait a bit for the page to settle
await new Promise(resolve => setTimeout(resolve, 1000));
// Get current page info
const currentUrl = page.url();
const currentTitle = await page.title();
return {
success: true,
operationType: 'go_forward',
message: 'Navigated forward to next page',
url: currentUrl,
title: currentTitle
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Check if there's no history to go forward to
if (errorMessage.includes('Cannot navigate forward')) {
return {
success: false,
operationType: 'go_forward',
message: 'Cannot go forward - no next page in history'
};
}
return {
success: false,
operationType: 'go_forward',
message: `Failed to go forward: ${errorMessage}`
};
}
}
/**
* Refresh the current page
*/
private async refreshPage(page: BrowserPage): Promise<NavigationOutput> {
try {
await page.refreshPage();
// Wait a bit for the page to settle
await new Promise(resolve => setTimeout(resolve, 1000));
// Get current page info
const currentUrl = page.url();
const currentTitle = await page.title();
return {
success: true,
operationType: 'refresh',
message: 'Page refreshed successfully',
url: currentUrl,
title: currentTitle
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
operationType: 'refresh',
message: `Failed to refresh page: ${errorMessage}`
};
}
}
}