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 ;
}
}
/ * *
* 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
* /
public registerScopedFilesystemTool ( basePath : string ) : void {
const scopedTool = new FilesystemTool ( { basePath } ) ;
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
this . driver = new DriverAgent ( this . driverProvider , this . options . driverSystemMessage ) ;
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 ) ;
while (
iterations < this . options . maxIterations ! &&
consecutiveRejections < this . options . maxConsecutiveRejections ! &&
! completed
) {
iterations ++ ;
// Check if task is complete
if ( this . driver . isTaskComplete ( driverResponse . content ) ) {
completed = true ;
finalResult = this . driver . extractTaskResult ( driverResponse . content ) || driverResponse . content ;
break ;
}
// Check if driver needs clarification
if ( this . driver . needsClarification ( driverResponse . content ) ) {
// 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 ] ;
// Quick validation first
const quickDecision = this . guardian . quickValidate ( proposal ) ;
let decision : interfaces.IGuardianDecision ;
if ( quickDecision ) {
decision = quickDecision ;
} else {
// Full AI evaluation
decision = await this . guardian . evaluate ( proposal , task ) ;
}
if ( decision . decision === 'approve' ) {
consecutiveRejections = 0 ;
// 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 {
const result = await tool . execute ( proposal . action , proposal . params ) ;
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 ++ ;
// 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' ;
} else if ( consecutiveRejections >= this . options . maxConsecutiveRejections ! ) {
status = 'max_rejections_reached' ;
}
}
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 ( ) ) ;
}
}