diff --git a/ts/smartagent.classes.dualagent.ts b/ts/smartagent.classes.dualagent.ts index b121648..b610a21 100644 --- a/ts/smartagent.classes.dualagent.ts +++ b/ts/smartagent.classes.dualagent.ts @@ -70,6 +70,60 @@ export class DualAgentOrchestrator { } } + /** + * Emit a progress event if callback is configured + */ + private emitProgress(event: Omit): 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, + 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 */ @@ -126,7 +180,10 @@ export class DualAgentOrchestrator { : this.driverProvider; // NOW create agents with initialized providers - this.driver = new DriverAgent(this.driverProvider, this.options.driverSystemMessage); + 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 @@ -192,6 +249,12 @@ export class DualAgentOrchestrator { 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! && @@ -199,15 +262,36 @@ export class DualAgentOrchestrator { ) { 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, @@ -234,6 +318,15 @@ export class DualAgentOrchestrator { // 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; @@ -241,6 +334,14 @@ export class DualAgentOrchestrator { 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); } @@ -248,6 +349,14 @@ export class DualAgentOrchestrator { 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) { @@ -260,8 +369,25 @@ export class DualAgentOrchestrator { } 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) { @@ -306,6 +432,15 @@ export class DualAgentOrchestrator { // 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`; @@ -337,8 +472,21 @@ export class DualAgentOrchestrator { 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`, + }); } } diff --git a/ts/smartagent.interfaces.ts b/ts/smartagent.interfaces.ts index 6a32f5e..fd5b560 100644 --- a/ts/smartagent.interfaces.ts +++ b/ts/smartagent.interfaces.ts @@ -30,6 +30,10 @@ export interface IDualAgentOptions extends plugins.smartai.ISmartAiOptions { maxResultChars?: number; /** Maximum history messages to pass to API (default: 20). Set to 0 for unlimited. */ maxHistoryMessages?: number; + /** Optional callback for live progress updates during execution */ + onProgress?: (event: IProgressEvent) => void; + /** Prefix for log messages (e.g., "[README]", "[Commit]"). Default: empty */ + logPrefix?: string; } // ================================ @@ -201,6 +205,58 @@ export interface IDualAgentRunResult { error?: string; } +// ================================ +// Progress Event Interfaces +// ================================ + +/** + * Progress event types for live feedback during agent execution + */ +export type TProgressEventType = + | 'task_started' + | 'iteration_started' + | 'tool_proposed' + | 'guardian_evaluating' + | 'tool_approved' + | 'tool_rejected' + | 'tool_executing' + | 'tool_completed' + | 'task_completed' + | 'clarification_needed' + | 'max_iterations' + | 'max_rejections'; + +/** + * Log level for progress events + */ +export type TLogLevel = 'info' | 'warn' | 'error' | 'success'; + +/** + * Progress event for live feedback during agent execution + */ +export interface IProgressEvent { + /** Type of progress event */ + type: TProgressEventType; + /** Current iteration number */ + iteration?: number; + /** Maximum iterations configured */ + maxIterations?: number; + /** Name of the tool being used */ + toolName?: string; + /** Action being performed */ + action?: string; + /** Reason for rejection or other explanation */ + reason?: string; + /** Human-readable message about the event */ + message?: string; + /** Timestamp of the event */ + timestamp: Date; + /** Log level for this event (info, warn, error, success) */ + logLevel: TLogLevel; + /** Pre-formatted log message ready for output */ + logMessage: string; +} + // ================================ // Utility Types // ================================ diff --git a/ts/smartagent.tools.filesystem.ts b/ts/smartagent.tools.filesystem.ts index a381684..0a1d193 100644 --- a/ts/smartagent.tools.filesystem.ts +++ b/ts/smartagent.tools.filesystem.ts @@ -494,32 +494,27 @@ export class FilesystemTool extends BaseToolWrapper { const items = await dir.list(); for (const item of items) { - const itemPath = plugins.path.join(dirPath, item); - const itemRelPath = relativePath ? `${relativePath}/${item}` : item; + // item is IDirectoryEntry with name, path, isFile, isDirectory properties + const itemPath = item.path; + const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name; + const isDir = item.isDirectory; - try { - const stats = await this.smartfs.file(itemPath).stat(); - const isDir = stats.isDirectory; + const entry: ITreeEntry = { + path: itemPath, + relativePath: itemRelPath, + isDir, + depth, + }; - const entry: ITreeEntry = { - path: itemPath, - relativePath: itemRelPath, - isDir, - depth, - }; + if (showSizes && !isDir && item.stats) { + entry.size = item.stats.size; + } - if (showSizes && !isDir) { - entry.size = stats.size; - } + entries.push(entry); - entries.push(entry); - - // Recurse into directories - if (isDir && depth < maxDepth) { - await collectEntries(itemPath, depth + 1, itemRelPath); - } - } catch { - // Skip items we can't stat + // Recurse into directories + if (isDir && depth < maxDepth) { + await collectEntries(itemPath, depth + 1, itemRelPath); } } }; @@ -607,13 +602,20 @@ export class FilesystemTool extends BaseToolWrapper { const dir = this.smartfs.directory(basePath).recursive().filter(pattern); const matches = await dir.list(); + // Return file paths relative to base path for readability + const files = matches.map((entry) => ({ + path: entry.path, + relativePath: plugins.path.relative(basePath, entry.path), + isDirectory: entry.isDirectory, + })); + return { success: true, result: { pattern, basePath, - matches, - count: matches.length, + files, + count: files.length, }, }; }