2025-12-02 10:59:09 +00:00
import * as plugins from './plugins.js' ;
import * as interfaces from './smartagent.interfaces.js' ;
import { BaseToolWrapper } from './smartagent.tools.base.js' ;
import { DriverAgent } from './smartagent.classes.driveragent.js' ;
import { GuardianAgent } from './smartagent.classes.guardianagent.js' ;
import { FilesystemTool } from './smartagent.tools.filesystem.js' ;
import { HttpTool } from './smartagent.tools.http.js' ;
import { ShellTool } from './smartagent.tools.shell.js' ;
import { BrowserTool } from './smartagent.tools.browser.js' ;
2025-12-02 12:11:31 +00:00
import { DenoTool } from './smartagent.tools.deno.js' ;
2025-12-02 10:59:09 +00:00
/ * *
* DualAgentOrchestrator - Coordinates Driver and Guardian agents
* Manages the complete lifecycle of task execution with tool approval
* /
export class DualAgentOrchestrator {
private options : interfaces.IDualAgentOptions ;
private smartai : plugins.smartai.SmartAi ;
private driverProvider : plugins.smartai.MultiModalModel ;
private guardianProvider : plugins.smartai.MultiModalModel ;
private driver : DriverAgent ;
private guardian : GuardianAgent ;
private tools : Map < string , BaseToolWrapper > = new Map ( ) ;
private isRunning = false ;
private conversationHistory : interfaces.IAgentMessage [ ] = [ ] ;
2025-12-15 12:37:19 +00:00
private ownsSmartAi = true ; // true if we created the SmartAi instance, false if it was provided
2025-12-02 10:59:09 +00:00
constructor ( options : interfaces.IDualAgentOptions ) {
this . options = {
maxIterations : 20 ,
maxConsecutiveRejections : 3 ,
defaultProvider : 'openai' ,
2025-12-15 14:49:26 +00:00
maxResultChars : 15000 ,
maxHistoryMessages : 20 ,
2025-12-02 10:59:09 +00:00
. . . options ,
} ;
2025-12-15 12:37:19 +00:00
// Use existing SmartAi instance if provided, otherwise create a new one
if ( options . smartAiInstance ) {
this . smartai = options . smartAiInstance ;
this . ownsSmartAi = false ; // Don't manage lifecycle of provided instance
} else {
this . smartai = new plugins . smartai . SmartAi ( options ) ;
this . ownsSmartAi = true ;
}
// Note: Don't access providers here - they don't exist until start() is called
2025-12-02 10:59:09 +00:00
}
/ * *
* Get provider by name
* /
private getProviderByName ( providerName : plugins.smartai.TProvider ) : plugins . smartai . MultiModalModel {
switch ( providerName ) {
case 'openai' :
return this . smartai . openaiProvider ;
case 'anthropic' :
return this . smartai . anthropicProvider ;
case 'perplexity' :
return this . smartai . perplexityProvider ;
case 'ollama' :
return this . smartai . ollamaProvider ;
case 'groq' :
return this . smartai . groqProvider ;
case 'xai' :
return this . smartai . xaiProvider ;
case 'exo' :
return this . smartai . exoProvider ;
default :
return this . smartai . openaiProvider ;
}
}
2025-12-15 15:11:22 +00:00
/ * *
* Emit a progress event if callback is configured
* /
private emitProgress ( event : Omit < interfaces.IProgressEvent , 'timestamp' | 'logLevel' | 'logMessage' > ) : void {
if ( this . options . onProgress ) {
const prefix = this . options . logPrefix ? ` ${ this . options . logPrefix } ` : '' ;
const { logLevel , logMessage } = this . formatProgressEvent ( event , prefix ) ;
this . options . onProgress ( {
. . . event ,
timestamp : new Date ( ) ,
logLevel ,
logMessage ,
} ) ;
}
}
/ * *
* Format a progress event into a log level and message
* /
private formatProgressEvent (
event : Omit < interfaces.IProgressEvent , 'timestamp' | 'logLevel' | 'logMessage' > ,
prefix : string
) : { logLevel : interfaces.TLogLevel ; logMessage : string } {
switch ( event . type ) {
case 'task_started' :
return { logLevel : 'info' , logMessage : ` ${ prefix } Task started ` } ;
case 'iteration_started' :
return { logLevel : 'info' , logMessage : ` ${ prefix } Iteration ${ event . iteration } / ${ event . maxIterations } ` } ;
case 'tool_proposed' :
return { logLevel : 'info' , logMessage : ` ${ prefix } → Proposing: ${ event . toolName } . ${ event . action } ` } ;
case 'guardian_evaluating' :
return { logLevel : 'info' , logMessage : ` ${ prefix } ⏳ Guardian evaluating... ` } ;
case 'tool_approved' :
return { logLevel : 'info' , logMessage : ` ${ prefix } ✓ Approved: ${ event . toolName } . ${ event . action } ` } ;
case 'tool_rejected' :
return { logLevel : 'warn' , logMessage : ` ${ prefix } ✗ Rejected: ${ event . toolName } . ${ event . action } - ${ event . reason } ` } ;
case 'tool_executing' :
return { logLevel : 'info' , logMessage : ` ${ prefix } ⚡ Executing: ${ event . toolName } . ${ event . action } ... ` } ;
case 'tool_completed' :
return { logLevel : 'info' , logMessage : ` ${ prefix } ✓ Completed: ${ event . message } ` } ;
case 'task_completed' :
return { logLevel : 'success' , logMessage : ` ${ prefix } Task completed in ${ event . iteration } iterations ` } ;
case 'clarification_needed' :
return { logLevel : 'warn' , logMessage : ` ${ prefix } Clarification needed from user ` } ;
case 'max_iterations' :
return { logLevel : 'error' , logMessage : ` ${ prefix } ${ event . message } ` } ;
case 'max_rejections' :
return { logLevel : 'error' , logMessage : ` ${ prefix } ${ event . message } ` } ;
default :
return { logLevel : 'info' , logMessage : ` ${ prefix } ${ event . type } ` } ;
}
}
2025-12-02 10:59:09 +00:00
/ * *
* Register a custom tool
* /
public registerTool ( tool : BaseToolWrapper ) : void {
this . tools . set ( tool . name , tool ) ;
2025-12-15 12:37:19 +00:00
// Register with agents if they exist (they're created in start())
if ( this . driver ) {
this . driver . registerTool ( tool ) ;
}
if ( this . guardian ) {
this . guardian . registerTool ( tool ) ;
}
2025-12-02 10:59:09 +00:00
}
/ * *
* Register all standard tools
* /
public registerStandardTools ( ) : void {
const standardTools = [
new FilesystemTool ( ) ,
new HttpTool ( ) ,
new ShellTool ( ) ,
new BrowserTool ( ) ,
2025-12-02 12:11:31 +00:00
new DenoTool ( ) ,
2025-12-02 10:59:09 +00:00
] ;
for ( const tool of standardTools ) {
this . registerTool ( tool ) ;
}
}
2025-12-15 14:23:53 +00:00
/ * *
* Register a scoped filesystem tool that can only access files within the specified directory
* @param basePath The directory to scope filesystem operations to
2025-12-15 15:29:56 +00:00
* @param excludePatterns Optional glob patterns to exclude from listings ( e . g . , [ '.nogit/**' , 'node_modules/**' ] )
2025-12-15 14:23:53 +00:00
* /
2025-12-15 15:29:56 +00:00
public registerScopedFilesystemTool ( basePath : string , excludePatterns? : string [ ] ) : void {
const scopedTool = new FilesystemTool ( { basePath , excludePatterns } ) ;
2025-12-15 14:23:53 +00:00
this . registerTool ( scopedTool ) ;
}
2025-12-02 10:59:09 +00:00
/ * *
* Initialize all tools ( eager loading )
* /
public async start ( ) : Promise < void > {
2025-12-15 12:37:19 +00:00
// Start smartai only if we created it (external instances should already be started)
if ( this . ownsSmartAi ) {
await this . smartai . start ( ) ;
}
// NOW get providers (after they've been initialized by smartai.start())
this . driverProvider = this . getProviderByName ( this . options . defaultProvider ! ) ;
this . guardianProvider = this . options . guardianProvider
? this . getProviderByName ( this . options . guardianProvider )
: this . driverProvider ;
// NOW create agents with initialized providers
2025-12-15 15:11:22 +00:00
this . driver = new DriverAgent ( this . driverProvider , {
systemMessage : this.options.driverSystemMessage ,
maxHistoryMessages : this.options.maxHistoryMessages ,
} ) ;
2025-12-15 12:37:19 +00:00
this . guardian = new GuardianAgent ( this . guardianProvider , this . options . guardianPolicyPrompt ) ;
// Register any tools that were added before start() with the agents
for ( const tool of this . tools . values ( ) ) {
this . driver . registerTool ( tool ) ;
this . guardian . registerTool ( tool ) ;
}
2025-12-02 10:59:09 +00:00
// Initialize all tools
const initPromises : Promise < void > [ ] = [ ] ;
for ( const tool of this . tools . values ( ) ) {
initPromises . push ( tool . initialize ( ) ) ;
}
await Promise . all ( initPromises ) ;
this . isRunning = true ;
}
/ * *
* Cleanup all tools
* /
public async stop ( ) : Promise < void > {
const cleanupPromises : Promise < void > [ ] = [ ] ;
for ( const tool of this . tools . values ( ) ) {
cleanupPromises . push ( tool . cleanup ( ) ) ;
}
await Promise . all ( cleanupPromises ) ;
2025-12-15 12:37:19 +00:00
// Only stop smartai if we created it (don't stop external instances)
if ( this . ownsSmartAi ) {
await this . smartai . stop ( ) ;
}
2025-12-02 10:59:09 +00:00
this . isRunning = false ;
2025-12-15 12:37:19 +00:00
if ( this . driver ) {
this . driver . reset ( ) ;
}
2025-12-02 10:59:09 +00:00
}
/ * *
* Run a task through the dual - agent system
* /
public async run ( task : string ) : Promise < interfaces.IDualAgentRunResult > {
if ( ! this . isRunning ) {
throw new Error ( 'Orchestrator not started. Call start() first.' ) ;
}
this . conversationHistory = [ ] ;
let iterations = 0 ;
let consecutiveRejections = 0 ;
let completed = false ;
let finalResult : string | null = null ;
// Add initial task to history
this . conversationHistory . push ( {
role : 'user' ,
content : task ,
} ) ;
// Start the driver with the task
let driverResponse = await this . driver . startTask ( task ) ;
this . conversationHistory . push ( driverResponse ) ;
2025-12-15 15:11:22 +00:00
// Emit task started event
this . emitProgress ( {
type : 'task_started' ,
message : task.length > 100 ? task . substring ( 0 , 100 ) + '...' : task ,
} ) ;
2025-12-02 10:59:09 +00:00
while (
iterations < this . options . maxIterations ! &&
consecutiveRejections < this . options . maxConsecutiveRejections ! &&
! completed
) {
iterations ++ ;
2025-12-15 15:11:22 +00:00
// Emit iteration started event
this . emitProgress ( {
type : 'iteration_started' ,
iteration : iterations ,
maxIterations : this.options.maxIterations ,
} ) ;
2025-12-02 10:59:09 +00:00
// Check if task is complete
if ( this . driver . isTaskComplete ( driverResponse . content ) ) {
completed = true ;
finalResult = this . driver . extractTaskResult ( driverResponse . content ) || driverResponse . content ;
2025-12-15 15:11:22 +00:00
// Emit task completed event
this . emitProgress ( {
type : 'task_completed' ,
iteration : iterations ,
message : 'Task completed successfully' ,
} ) ;
2025-12-02 10:59:09 +00:00
break ;
}
// Check if driver needs clarification
if ( this . driver . needsClarification ( driverResponse . content ) ) {
2025-12-15 15:11:22 +00:00
// Emit clarification needed event
this . emitProgress ( {
type : 'clarification_needed' ,
iteration : iterations ,
message : 'Driver needs clarification from user' ,
} ) ;
2025-12-02 10:59:09 +00:00
// Return with clarification needed status
return {
success : false ,
completed : false ,
result : driverResponse.content ,
iterations ,
history : this.conversationHistory ,
status : 'clarification_needed' ,
} ;
}
// Parse tool call proposals
const proposals = this . driver . parseToolCallProposals ( driverResponse . content ) ;
if ( proposals . length === 0 ) {
// No tool calls, continue the conversation
driverResponse = await this . driver . continueWithMessage (
'Please either use a tool to make progress on the task, or indicate that the task is complete with <task_complete>summary</task_complete>.'
) ;
this . conversationHistory . push ( driverResponse ) ;
continue ;
}
// Process the first proposal (one at a time)
const proposal = proposals [ 0 ] ;
2025-12-15 15:11:22 +00:00
// Emit tool proposed event
this . emitProgress ( {
type : 'tool_proposed' ,
iteration : iterations ,
toolName : proposal.toolName ,
action : proposal.action ,
message : ` ${ proposal . toolName } . ${ proposal . action } ` ,
} ) ;
2025-12-02 10:59:09 +00:00
// Quick validation first
const quickDecision = this . guardian . quickValidate ( proposal ) ;
let decision : interfaces.IGuardianDecision ;
if ( quickDecision ) {
decision = quickDecision ;
} else {
2025-12-15 15:11:22 +00:00
// Emit guardian evaluating event
this . emitProgress ( {
type : 'guardian_evaluating' ,
iteration : iterations ,
toolName : proposal.toolName ,
action : proposal.action ,
} ) ;
2025-12-02 10:59:09 +00:00
// Full AI evaluation
decision = await this . guardian . evaluate ( proposal , task ) ;
}
if ( decision . decision === 'approve' ) {
consecutiveRejections = 0 ;
2025-12-15 15:11:22 +00:00
// Emit tool approved event
this . emitProgress ( {
type : 'tool_approved' ,
iteration : iterations ,
toolName : proposal.toolName ,
action : proposal.action ,
} ) ;
2025-12-02 10:59:09 +00:00
// Execute the tool
const tool = this . tools . get ( proposal . toolName ) ;
if ( ! tool ) {
const errorMessage = ` Tool " ${ proposal . toolName } " not found. ` ;
driverResponse = await this . driver . continueWithMessage (
` TOOL ERROR: ${ errorMessage } \ n \ nPlease try a different approach. `
) ;
this . conversationHistory . push ( driverResponse ) ;
continue ;
}
try {
2025-12-15 15:11:22 +00:00
// Emit tool executing event
this . emitProgress ( {
type : 'tool_executing' ,
iteration : iterations ,
toolName : proposal.toolName ,
action : proposal.action ,
} ) ;
2025-12-02 10:59:09 +00:00
const result = await tool . execute ( proposal . action , proposal . params ) ;
2025-12-15 15:11:22 +00:00
// Emit tool completed event
this . emitProgress ( {
type : 'tool_completed' ,
iteration : iterations ,
toolName : proposal.toolName ,
action : proposal.action ,
message : result.success ? 'success' : result . error ,
} ) ;
2025-12-15 14:49:26 +00:00
// Build result message (prefer summary if provided, otherwise stringify result)
let resultMessage : string ;
if ( result . success ) {
if ( result . summary ) {
// Use tool-provided summary
resultMessage = ` TOOL RESULT ( ${ proposal . toolName } . ${ proposal . action } ): \ n ${ result . summary } ` ;
} else {
// Stringify and potentially truncate
const resultStr = JSON . stringify ( result . result , null , 2 ) ;
const maxChars = this . options . maxResultChars ? ? 15000 ;
if ( maxChars > 0 && resultStr . length > maxChars ) {
// Truncate the result
const truncated = resultStr . substring ( 0 , maxChars ) ;
const omittedTokens = Math . round ( ( resultStr . length - maxChars ) / 4 ) ;
resultMessage = ` TOOL RESULT ( ${ proposal . toolName } . ${ proposal . action } ): \ n ${ truncated } \ n \ n[... output truncated, ~ ${ omittedTokens } tokens omitted. Use more specific parameters to reduce output size.] ` ;
} else {
resultMessage = ` TOOL RESULT ( ${ proposal . toolName } . ${ proposal . action } ): \ n ${ resultStr } ` ;
}
}
} else {
resultMessage = ` TOOL ERROR ( ${ proposal . toolName } . ${ proposal . action } ): \ n ${ result . error } ` ;
}
2025-12-02 10:59:09 +00:00
this . conversationHistory . push ( {
role : 'system' ,
content : resultMessage ,
toolCall : proposal ,
toolResult : result ,
} ) ;
driverResponse = await this . driver . continueWithMessage ( resultMessage ) ;
this . conversationHistory . push ( driverResponse ) ;
} catch ( error ) {
const errorMessage = ` Tool execution failed: ${ error instanceof Error ? error.message : String ( error ) } ` ;
driverResponse = await this . driver . continueWithMessage (
` TOOL ERROR: ${ errorMessage } \ n \ nPlease try a different approach. `
) ;
this . conversationHistory . push ( driverResponse ) ;
}
} else {
// Rejected
consecutiveRejections ++ ;
2025-12-15 15:11:22 +00:00
// Emit tool rejected event
this . emitProgress ( {
type : 'tool_rejected' ,
iteration : iterations ,
toolName : proposal.toolName ,
action : proposal.action ,
reason : decision.reason ,
} ) ;
2025-12-02 10:59:09 +00:00
// Build rejection feedback
let feedback = ` TOOL CALL REJECTED by Guardian: \ n ` ;
feedback += ` - Reason: ${ decision . reason } \ n ` ;
if ( decision . concerns && decision . concerns . length > 0 ) {
feedback += ` - Concerns: \ n ${ decision . concerns . map ( c = > ` - ${ c } ` ) . join ( '\n' ) } \ n ` ;
}
if ( decision . suggestions ) {
feedback += ` - Suggestions: ${ decision . suggestions } \ n ` ;
}
feedback += ` \ nPlease adapt your approach based on this feedback. ` ;
this . conversationHistory . push ( {
role : 'system' ,
content : feedback ,
toolCall : proposal ,
guardianDecision : decision ,
} ) ;
driverResponse = await this . driver . continueWithMessage ( feedback ) ;
this . conversationHistory . push ( driverResponse ) ;
}
}
// Determine final status
let status : interfaces.TDualAgentRunStatus = 'completed' ;
if ( ! completed ) {
if ( iterations >= this . options . maxIterations ! ) {
status = 'max_iterations_reached' ;
2025-12-15 15:11:22 +00:00
// Emit max iterations event
this . emitProgress ( {
type : 'max_iterations' ,
iteration : iterations ,
maxIterations : this.options.maxIterations ,
message : ` Maximum iterations ( ${ this . options . maxIterations } ) reached ` ,
} ) ;
2025-12-02 10:59:09 +00:00
} else if ( consecutiveRejections >= this . options . maxConsecutiveRejections ! ) {
status = 'max_rejections_reached' ;
2025-12-15 15:11:22 +00:00
// Emit max rejections event
this . emitProgress ( {
type : 'max_rejections' ,
iteration : iterations ,
message : ` Maximum consecutive rejections ( ${ this . options . maxConsecutiveRejections } ) reached ` ,
} ) ;
2025-12-02 10:59:09 +00:00
}
}
return {
success : completed ,
completed ,
result : finalResult || driverResponse . content ,
iterations ,
history : this.conversationHistory ,
status ,
} ;
}
/ * *
* Continue an existing task with user input
* /
public async continueTask ( userInput : string ) : Promise < interfaces.IDualAgentRunResult > {
if ( ! this . isRunning ) {
throw new Error ( 'Orchestrator not started. Call start() first.' ) ;
}
this . conversationHistory . push ( {
role : 'user' ,
content : userInput ,
} ) ;
const driverResponse = await this . driver . continueWithMessage ( userInput ) ;
this . conversationHistory . push ( driverResponse ) ;
// Continue the run loop
// For simplicity, we return the current state - full continuation would need refactoring
return {
success : false ,
completed : false ,
result : driverResponse.content ,
iterations : 1 ,
history : this.conversationHistory ,
status : 'in_progress' ,
} ;
}
/ * *
* Get the conversation history
* /
public getHistory ( ) : interfaces . IAgentMessage [ ] {
return [ . . . this . conversationHistory ] ;
}
/ * *
* Update the guardian policy
* /
public setGuardianPolicy ( policyPrompt : string ) : void {
this . guardian . setPolicy ( policyPrompt ) ;
}
/ * *
* Check if orchestrator is running
* /
public isActive ( ) : boolean {
return this . isRunning ;
}
/ * *
* Get registered tool names
* /
public getToolNames ( ) : string [ ] {
return Array . from ( this . tools . keys ( ) ) ;
}
}