Files
smartagent/ts/smartagent.classes.dualagent.ts
2025-12-15 15:11:22 +00:00

559 lines
18 KiB
TypeScript

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';
import { DenoTool } from './smartagent.tools.deno.js';
/**
* 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[] = [];
private ownsSmartAi = true; // true if we created the SmartAi instance, false if it was provided
constructor(options: interfaces.IDualAgentOptions) {
this.options = {
maxIterations: 20,
maxConsecutiveRejections: 3,
defaultProvider: 'openai',
maxResultChars: 15000,
maxHistoryMessages: 20,
...options,
};
// 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
}
/**
* 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;
}
}
/**
* 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}` };
}
}
/**
* Register a custom tool
*/
public registerTool(tool: BaseToolWrapper): void {
this.tools.set(tool.name, tool);
// 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);
}
}
/**
* Register all standard tools
*/
public registerStandardTools(): void {
const standardTools = [
new FilesystemTool(),
new HttpTool(),
new ShellTool(),
new BrowserTool(),
new DenoTool(),
];
for (const tool of standardTools) {
this.registerTool(tool);
}
}
/**
* 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);
}
/**
* Initialize all tools (eager loading)
*/
public async start(): Promise<void> {
// 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, {
systemMessage: this.options.driverSystemMessage,
maxHistoryMessages: this.options.maxHistoryMessages,
});
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);
}
// 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);
// Only stop smartai if we created it (don't stop external instances)
if (this.ownsSmartAi) {
await this.smartai.stop();
}
this.isRunning = false;
if (this.driver) {
this.driver.reset();
}
}
/**
* 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);
// Emit task started event
this.emitProgress({
type: 'task_started',
message: task.length > 100 ? task.substring(0, 100) + '...' : task,
});
while (
iterations < this.options.maxIterations! &&
consecutiveRejections < this.options.maxConsecutiveRejections! &&
!completed
) {
iterations++;
// Emit iteration started event
this.emitProgress({
type: 'iteration_started',
iteration: iterations,
maxIterations: this.options.maxIterations,
});
// Check if task is complete
if (this.driver.isTaskComplete(driverResponse.content)) {
completed = true;
finalResult = this.driver.extractTaskResult(driverResponse.content) || driverResponse.content;
// Emit task completed event
this.emitProgress({
type: 'task_completed',
iteration: iterations,
message: 'Task completed successfully',
});
break;
}
// Check if driver needs clarification
if (this.driver.needsClarification(driverResponse.content)) {
// Emit clarification needed event
this.emitProgress({
type: 'clarification_needed',
iteration: iterations,
message: 'Driver needs clarification from user',
});
// 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];
// Emit tool proposed event
this.emitProgress({
type: 'tool_proposed',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
message: `${proposal.toolName}.${proposal.action}`,
});
// Quick validation first
const quickDecision = this.guardian.quickValidate(proposal);
let decision: interfaces.IGuardianDecision;
if (quickDecision) {
decision = quickDecision;
} else {
// Emit guardian evaluating event
this.emitProgress({
type: 'guardian_evaluating',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
});
// Full AI evaluation
decision = await this.guardian.evaluate(proposal, task);
}
if (decision.decision === 'approve') {
consecutiveRejections = 0;
// Emit tool approved event
this.emitProgress({
type: 'tool_approved',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
});
// 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 {
// Emit tool executing event
this.emitProgress({
type: 'tool_executing',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
});
const result = await tool.execute(proposal.action, proposal.params);
// Emit tool completed event
this.emitProgress({
type: 'tool_completed',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
message: result.success ? 'success' : result.error,
});
// 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}`;
}
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++;
// Emit tool rejected event
this.emitProgress({
type: 'tool_rejected',
iteration: iterations,
toolName: proposal.toolName,
action: proposal.action,
reason: decision.reason,
});
// 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';
// Emit max iterations event
this.emitProgress({
type: 'max_iterations',
iteration: iterations,
maxIterations: this.options.maxIterations,
message: `Maximum iterations (${this.options.maxIterations}) reached`,
});
} else if (consecutiveRejections >= this.options.maxConsecutiveRejections!) {
status = 'max_rejections_reached';
// Emit max rejections event
this.emitProgress({
type: 'max_rejections',
iteration: iterations,
message: `Maximum consecutive rejections (${this.options.maxConsecutiveRejections}) 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());
}
}