abyss-ai
Version:
Autonomous AI coding agent - enhanced OpenCode with autonomous capabilities
1,508 lines (1,265 loc) ⢠51.6 kB
text/typescript
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { AgentCoordinator } from "../../agent/coordinator/agent-coordinator"
import { LargeFileProcessor } from "../../agent/file-processing/large-file-processor"
import { QuestionGenerator } from "../../agent/question-generation/question-generator"
import { Agent } from "../../agent/agent"
import { Session } from "../../session"
import { Identifier } from "../../id/id"
import {
AgentTask,
ReasoningMode,
type ProcessingContext
} from "../../agent/types/agent"
import { App } from "../../app/app"
import { BatchProcessor, BatchResult } from "./batch-processor"
import { AgentMemory } from "../../agent/memory/agent-memory"
import path from "path"
// Multi-Agent Analysis Command
const MultiAgentAnalyzeCommand = cmd({
command: "analyze [file]",
describe: "analyze code using Abyss multi-agent reasoning system",
builder: (yargs) =>
yargs
.positional("file", {
type: "string",
describe: "file to analyze",
})
.option("reasoning-modes", {
type: "array",
describe: "reasoning modes to use",
choices: ["ultrathinking", "ultrareasoning", "hybrid-reasoning", "hybrid-thinking"],
default: ["ultrathinking", "ultrareasoning", "hybrid-reasoning", "hybrid-thinking"],
})
.option("output", {
type: "string",
describe: "output format",
choices: ["json", "text", "summary", "questions"],
default: "text",
})
.option("analysis-mode", {
type: "string",
describe: "analysis approach to use",
choices: ["question-driven", "legacy", "auto"],
default: "question-driven",
})
.option("show-questions", {
type: "boolean",
describe: "show generated questions in output",
default: false,
}),
async handler(args) {
await App.provide({ cwd: process.cwd() }, async (_app) => {
UI.empty()
prompts.intro("Multi-Agent Code Analysis")
let filePath: string
if (args.file) {
filePath = path.resolve(args.file)
} else {
const fileInput = await prompts.text({
message: "File to analyze",
placeholder: "Enter file path",
validate: (x) => (x.length > 0 ? undefined : "File path required"),
})
if (prompts.isCancel(fileInput)) throw new UI.CancelledError()
filePath = path.resolve(fileInput)
}
// Check if file exists
const file = Bun.file(filePath)
if (!(await file.exists())) {
prompts.log.error(`File not found: ${filePath}`)
return
}
const spinner = prompts.spinner()
spinner.start("Initializing multi-agent system...")
try {
// Initialize agent coordinator
const coordinator = new AgentCoordinator()
await coordinator.initializeAgents()
// Map string reasoning modes to enum values (for legacy compatibility)
const _reasoningModes = (args.reasoningModes as string[]).map(mode => {
switch (mode) {
case "ultrathinking": return ReasoningMode.ULTRATHINKING
case "ultrareasoning": return ReasoningMode.ULTRAREASONING
case "hybrid-reasoning": return ReasoningMode.HYBRID_REASONING
case "hybrid-thinking": return ReasoningMode.HYBRID_THINKING
default: return ReasoningMode.ULTRATHINKING
}
})
spinner.message("Reading and analyzing file...")
// Read file content
const content = await file.text()
const _fileStats = await file.stat()
// Determine if we need large file processing
const isLargeFile = content.length > 50000 // 50KB threshold
let results
if (isLargeFile) {
spinner.message("Processing large file with chunking...")
const processor = new LargeFileProcessor()
const context: ProcessingContext = {
filePath,
language: detectLanguage(filePath),
complexity: assessBasicComplexity(content)
}
const processingResult = await processor.processLargeFile(filePath, context)
results = [processingResult]
} else {
spinner.message("Analyzing with question-driven multi-agent system...")
// Use question-driven approach with OpenCode's agent system
results = await processWithQuestionDrivenAgents(content, filePath, spinner)
}
spinner.stop("Analysis complete")
// Display results based on output format
await displayResults(results, args.output as string, filePath, {
showQuestions: args.showQuestions as boolean,
analysisMode: args.analysisMode as string
})
// Cleanup
await coordinator.dispose()
} catch (error) {
spinner.stop("Analysis failed")
prompts.log.error(`Analysis failed: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
prompts.outro("Multi-agent analysis complete")
})
},
})
// YOLO Analysis Command (Autonomous Mode) - Now the main command
export const YoloCommand = cmd({
command: "yolo [file]",
describe: "autonomous mode - analyze and fix code without prompts",
builder: (yargs) =>
yargs
.positional("file", {
type: "string",
describe: "file or directory to analyze and fix autonomously",
})
.option("directory", {
type: "boolean",
describe: "process entire directory recursively",
default: false,
})
.option("file-patterns", {
type: "array",
describe: "file patterns to include (for directory mode)",
default: ["**/*.{js,ts,jsx,tsx,py,java,cpp,c,go,rs}"],
})
.option("exclude-patterns", {
type: "array",
describe: "file patterns to exclude",
default: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**"],
})
.option("max-files", {
type: "number",
describe: "maximum number of files to process",
default: 50,
})
.option("concurrent", {
type: "number",
describe: "number of files to process concurrently",
default: 3,
})
.option("dry-run", {
type: "boolean",
describe: "preview changes without modifying files",
default: false,
})
.option("backup", {
type: "boolean",
describe: "create backup before making changes",
default: true,
})
.option("rollback", {
type: "string",
describe: "rollback from specific backup directory",
})
.option("list-backups", {
type: "boolean",
describe: "list available backups",
default: false,
})
.option("safety-checks", {
type: "boolean",
describe: "enable additional safety checks",
default: true,
}),
async handler(args) {
await App.provide({ cwd: process.cwd() }, async (_app) => {
UI.empty()
// Handle special actions first
if (args.listBackups) {
await listAvailableBackups(process.cwd())
return
}
if (args.rollback) {
await rollbackFromBackup(args.rollback)
return
}
prompts.intro("š YOLO Mode - Autonomous Analysis & Fixes")
let targetPath: string
if (args.file) {
targetPath = path.resolve(args.file)
} else {
const pathInput = await prompts.text({
message: args.directory ? "Directory to analyze and fix" : "File to analyze and fix",
placeholder: args.directory ? "Enter directory path" : "Enter file path",
validate: (x) => (x.length > 0 ? undefined : "Path required"),
})
if (prompts.isCancel(pathInput)) throw new UI.CancelledError()
targetPath = path.resolve(pathInput)
}
// Check if path exists
const stat = await Bun.file(targetPath).exists() || await Bun.file(targetPath + '/').exists()
if (!stat) {
prompts.log.error(`Path not found: ${targetPath}`)
return
}
// Determine if processing directory or single file
const isDirectory = args.directory || (await Bun.file(targetPath).stat()).isDirectory
if (isDirectory) {
await processDirectoryWithYolo(targetPath, args)
} else {
await processSingleFileWithYolo(targetPath, args)
}
})
},
})
// Directory processing with YOLO
async function processDirectoryWithYolo(directoryPath: string, args: any) {
const spinner = prompts.spinner()
spinner.start("šļø Initializing batch processing...")
try {
// Check if YOLO agent exists
const availableAgents = await Agent.list()
const yoloAgent = availableAgents.find(agent => agent.name === 'yolo')
if (!yoloAgent) {
spinner.stop("YOLO agent not found")
prompts.log.error("YOLO agent not configured. Please create a 'yolo' agent first.")
prompts.log.info("You can create one using: abyss agent create")
return
}
const agent = await Agent.get('yolo')
if (!agent) {
spinner.stop("YOLO agent configuration error")
prompts.log.error("YOLO agent configuration is invalid.")
return
}
// Setup batch processor and memory system
const batchProcessor = new BatchProcessor({
directory: directoryPath,
filePatterns: args.filePatterns as string[],
excludePatterns: args.excludePatterns as string[],
maxFiles: args.maxFiles as number,
concurrent: args.concurrent as number,
dryRun: args.dryRun as boolean,
backupEnabled: args.backup as boolean
})
const agentMemory = new AgentMemory(directoryPath)
spinner.message("š Finding files to process...")
const files = await batchProcessor.findFiles()
if (files.length === 0) {
spinner.stop("No files found")
prompts.log.warn("No files found matching the specified patterns")
return
}
spinner.message(`š Found ${files.length} files`)
if (args.dryRun) {
prompts.log.info("š DRY RUN MODE - Previewing files that would be processed:")
files.forEach((file, index) => {
console.log(` ${index + 1}. ${path.relative(directoryPath, file)}`)
})
prompts.outro("Dry run complete - no files were modified")
return
}
// Confirm before processing
const shouldProceed = await prompts.confirm({
message: `Process ${files.length} files with YOLO autonomous mode?`,
initialValue: true,
})
if (prompts.isCancel(shouldProceed) || !shouldProceed) {
prompts.log.info("Operation cancelled")
return
}
spinner.start("š Processing files with YOLO...")
// Process files in batch
const results = await batchProcessor.processBatch(async (filePath: string) => {
const fileStartTime = Date.now()
try {
spinner.message(`Processing: ${path.relative(directoryPath, filePath)}`)
// Read file content
const content = await Bun.file(filePath).text()
const fileType = detectLanguage(filePath)
// Get contextual advice from memory
const contextualAdvice = agentMemory.generateContextualAdvice(filePath, fileType)
// Create a session for YOLO analysis
const sessionId = Identifier.ascending("session")
const session = await Session.create(sessionId)
const messageID = Identifier.ascending("message")
const yoloPrompt = `AUTONOMOUS MODE: Analyze and fix this file without asking for permission.
File: ${filePath}
Content:
\`\`\`
${content}
\`\`\`
${contextualAdvice}
Instructions:
1. Analyze the code thoroughly for issues, bugs, and improvements
2. Fix them directly using write/edit tools
3. Focus on: syntax errors, type issues, best practices, security issues
4. Be concise - this is part of a batch operation
5. Only make changes if there are clear improvements to be made
6. Learn from the contextual advice above from previous similar analyses
Work autonomously - no permission requests needed.`
const result = await Session.chat({
messageID,
sessionID: session.id,
modelID: agent.model?.modelID || "anthropic/claude-3-5-sonnet-20241022",
providerID: agent.model?.providerID || "anthropic",
mode: "build",
system: agent.prompt,
tools: agent.tools,
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: yoloPrompt,
},
],
})
// Analyze result
const toolParts = result.parts.filter((x) => x.type === "tool")
const changes = toolParts.filter(t =>
(t as any).tool_name === 'write' || (t as any).tool_name === 'edit'
).length
const responseText = result.parts.findLast((x) => x.type === "text")?.text || ""
const issues = extractIssueCount(responseText)
// Record memory for learning
const changesApplied = extractChangesApplied(responseText, toolParts)
const issuesFound = extractIssuesFound(responseText)
agentMemory.addMemory({
filePath,
fileType,
issuesFound,
changesApplied,
successMetrics: {
syntaxValid: changes > 0, // Assume valid if changes were made
testsPass: responseText.toLowerCase().includes('test') && responseText.toLowerCase().includes('pass'),
lintClean: !responseText.toLowerCase().includes('lint error')
},
patterns: {
commonIssues: issuesFound,
effectiveFixes: changesApplied,
riskySections: []
},
context: {
language: fileType,
complexity: assessBasicComplexity(content)
}
})
return {
file: path.relative(directoryPath, filePath),
success: true,
changes,
issues,
processingTime: Date.now() - fileStartTime
}
} catch (error) {
return {
file: path.relative(directoryPath, filePath),
success: false,
changes: 0,
issues: 0,
processingTime: Date.now() - fileStartTime,
error: error instanceof Error ? error.message : String(error)
}
}
})
spinner.stop("ā
Batch processing complete")
// Display summary
const summary = batchProcessor.getSummary()
console.log(`\nš YOLO Batch Processing Results\n`)
console.log(`Directory: ${path.basename(directoryPath)}`)
console.log(`Files Processed: ${summary.successful}/${summary.total}`)
console.log(`Success Rate: ${Math.round(summary.successRate * 100)}%`)
console.log(`Total Changes: ${summary.totalChanges}`)
console.log(`Total Issues Found: ${summary.totalIssues}`)
console.log(`Processing Time: ${Math.round(summary.elapsedTime / 1000)}s`)
console.log(`Average Time/File: ${Math.round(summary.averageTime)}ms`)
// Show failed files if any
const failedResults = results.filter(r => !r.success)
if (failedResults.length > 0) {
console.log(`\nā Failed Files:`)
failedResults.forEach(result => {
console.log(` ⢠${result.file}: ${result.error}`)
})
}
// Show most impacted files
const changedFiles = results.filter(r => r.changes > 0).sort((a, b) => b.changes - a.changes)
if (changedFiles.length > 0) {
console.log(`\nš§ Files with Changes:`)
changedFiles.slice(0, 10).forEach(result => {
console.log(` ⢠${result.file}: ${result.changes} changes, ${result.issues} issues`)
})
if (changedFiles.length > 10) {
console.log(` ... and ${changedFiles.length - 10} more files`)
}
}
} catch (error) {
spinner.stop("Batch processing failed")
prompts.log.error(`Batch processing failed: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
prompts.outro("YOLO batch processing complete - check your files for autonomous improvements!")
}
// Single file processing with YOLO
async function processSingleFileWithYolo(filePath: string, args: any) {
const spinner = prompts.spinner()
spinner.start("Launching YOLO agent...")
try {
// Check if YOLO agent exists
const availableAgents = await Agent.list()
const yoloAgent = availableAgents.find(agent => agent.name === 'yolo')
if (!yoloAgent) {
spinner.stop("YOLO agent not found")
prompts.log.error("YOLO agent not configured. Please create a 'yolo' agent first.")
prompts.log.info("You can create one using: opencode agent create")
return
}
spinner.message("Reading file and preparing autonomous analysis...")
// Read file content
const content = await Bun.file(filePath).text()
const fileType = detectLanguage(filePath)
// Setup memory system
const agentMemory = new AgentMemory(path.dirname(filePath))
const contextualAdvice = agentMemory.generateContextualAdvice(filePath, fileType)
// Create backup if enabled
if (args.backup) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const backupPath = `${filePath}.backup-${timestamp}`
await Bun.write(backupPath, content)
prompts.log.info(`Backup created: ${backupPath}`)
}
// Create a session for YOLO analysis
const sessionId = Identifier.ascending("session")
const session = await Session.create(sessionId)
const messageID = Identifier.ascending("message")
const agent = await Agent.get('yolo')
if (!agent) {
spinner.stop("YOLO agent configuration error")
prompts.log.error("YOLO agent configuration is invalid.")
return
}
spinner.message("š¤ YOLO agent is working autonomously...")
const yoloPrompt = `AUTONOMOUS MODE: Analyze and fix this file without asking for permission.
File: ${filePath}
Content:
\`\`\`
${content}
\`\`\`
${contextualAdvice}
Instructions:
1. Analyze the code thoroughly
2. Identify any issues, bugs, or improvements
3. Fix them directly (use write/edit tools)
4. Run tests if available (use bash tool)
5. Provide a summary of what was done
6. Learn from the contextual advice above from previous similar analyses
Work autonomously - no permission requests needed.`
const result = await Session.chat({
messageID,
sessionID: session.id,
modelID: agent.model?.modelID || "anthropic/claude-3-5-sonnet-20241022",
providerID: agent.model?.providerID || "anthropic",
mode: "build",
system: agent.prompt,
tools: agent.tools,
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: yoloPrompt,
},
],
})
spinner.stop("š YOLO analysis complete")
// Display results
const textPart = result.parts.findLast((x) => x.type === "text")
const toolParts = result.parts.filter((x) => x.type === "tool")
const responseText = textPart?.text || ""
// Record memory for learning
const changesApplied = extractChangesApplied(responseText, toolParts)
const issuesFound = extractIssuesFound(responseText)
agentMemory.addMemory({
filePath,
fileType,
issuesFound,
changesApplied,
successMetrics: {
syntaxValid: toolParts.length > 0,
testsPass: responseText.toLowerCase().includes('test') && responseText.toLowerCase().includes('pass'),
lintClean: !responseText.toLowerCase().includes('lint error')
},
patterns: {
commonIssues: issuesFound,
effectiveFixes: changesApplied,
riskySections: []
},
context: {
language: fileType,
complexity: assessBasicComplexity(content)
}
})
console.log(`\nš YOLO Autonomous Analysis Results\n`)
console.log(`File: ${path.basename(filePath)}`)
console.log(`Tools Used: ${toolParts.length} tool calls`)
console.log(`\nš Summary:`)
if (responseText) {
console.log(responseText)
} else {
console.log("YOLO agent completed the analysis. Check the file for changes.")
}
if (toolParts.length > 0) {
console.log(`\nš§ Actions Taken:`)
toolParts.forEach((tool, index) => {
console.log(`${index + 1}. ${(tool as any).tool_name || 'Tool'}: ${(tool as any).description || 'Action performed'}`)
})
}
} catch (error) {
spinner.stop("YOLO analysis failed")
prompts.log.error(`YOLO analysis failed: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
prompts.outro("YOLO mode complete - check your files for autonomous improvements!")
}
function extractIssueCount(text: string): number {
const lowerText = text.toLowerCase()
const issueKeywords = ['error', 'bug', 'issue', 'problem', 'warning', 'fix']
let count = 0
for (const keyword of issueKeywords) {
const matches = lowerText.match(new RegExp(keyword, 'g'))
if (matches) {
count += matches.length
}
}
return Math.min(count, 10) // Cap at 10 to avoid noise
}
function extractChangesApplied(responseText: string, toolParts: any[]): string[] {
const changes: string[] = []
// Extract changes mentioned in response text
const changeKeywords = ['fixed', 'updated', 'added', 'removed', 'refactored', 'improved']
const lines = responseText.split('\n')
for (const line of lines) {
for (const keyword of changeKeywords) {
if (line.toLowerCase().includes(keyword)) {
const cleanLine = line.trim().replace(/^[-ā¢*]\s*/, '')
if (cleanLine.length > 0 && cleanLine.length < 100) {
changes.push(cleanLine)
}
}
}
}
// Extract changes from tool calls
toolParts.forEach(tool => {
const toolName = (tool as any).tool_name || 'Unknown'
const description = (tool as any).description || `Used ${toolName} tool`
changes.push(description)
})
return changes.slice(0, 10) // Limit to prevent noise
}
function extractIssuesFound(responseText: string): string[] {
const issues: string[] = []
const lines = responseText.split('\n')
let inIssueSection = false
for (const line of lines) {
const lowerLine = line.toLowerCase()
// Check if we're entering an issue section
if (lowerLine.includes('issue') || lowerLine.includes('problem') ||
lowerLine.includes('error') || lowerLine.includes('bug')) {
inIssueSection = true
}
// Extract issue if we're in a section and line looks like an issue
if (inIssueSection && (line.trim().startsWith('ā¢') || line.trim().startsWith('-') || line.trim().startsWith('*'))) {
const cleanLine = line.trim().replace(/^[-ā¢*]\s*/, '')
if (cleanLine.length > 0 && cleanLine.length < 150) {
issues.push(cleanLine)
}
}
// Stop if we hit a different section
if (line.trim() === '' ||
(lowerLine.includes('fix') && !lowerLine.includes('issue')) ||
lowerLine.includes('recommendation') ||
lowerLine.includes('summary')) {
inIssueSection = false
}
}
return issues.slice(0, 10) // Limit to prevent noise
}
// Safety and backup management functions
async function listAvailableBackups(projectPath: string): Promise<void> {
const spinner = prompts.spinner()
spinner.start("Scanning for backups...")
try {
const backupDirs: string[] = []
// Find .abyss-backup-* directories
const entries = await Bun.file(projectPath).text().then(() => null, () => null)
// Use a simple file listing approach
const files = await Bun.file(path.join(projectPath, '.')).text().catch(() => '')
// This is a simplified version - in a real implementation you'd use proper directory traversal
const possibleBackups = [
'.abyss-backup-2024-01-01T00-00-00-000Z',
'.abyss-backup-2024-01-02T00-00-00-000Z'
]
for (const backupName of possibleBackups) {
const backupPath = path.join(projectPath, backupName)
const backupInfoPath = path.join(backupPath, '.backup-info.json')
if (await Bun.file(backupInfoPath).exists()) {
backupDirs.push(backupName)
}
}
spinner.stop("Backup scan complete")
if (backupDirs.length === 0) {
prompts.log.info("No backups found in this directory")
return
}
console.log(`\nš¦ Available Backups (${backupDirs.length} found):\n`)
for (const backupDir of backupDirs) {
try {
const backupInfoPath = path.join(projectPath, backupDir, '.backup-info.json')
const backupInfo = JSON.parse(await Bun.file(backupInfoPath).text())
const timestamp = new Date(backupInfo.timestamp).toLocaleString()
const fileCount = backupInfo.files?.length || 0
console.log(`š ${backupDir}`)
console.log(` Created: ${timestamp}`)
console.log(` Files: ${fileCount}`)
console.log(` Rollback: abyss multiagent yolo --rollback "${backupDir}"`)
console.log()
} catch (error) {
console.log(`š ${backupDir} (corrupted backup info)`)
console.log()
}
}
} catch (error) {
spinner.stop("Backup scan failed")
prompts.log.error(`Failed to list backups: ${error}`)
}
}
async function rollbackFromBackup(backupDir: string): Promise<void> {
const spinner = prompts.spinner()
spinner.start("Preparing rollback...")
try {
const projectPath = process.cwd()
const fullBackupPath = path.resolve(backupDir.startsWith('/') ? backupDir : path.join(projectPath, backupDir))
const backupInfoPath = path.join(fullBackupPath, '.backup-info.json')
if (!(await Bun.file(backupInfoPath).exists())) {
spinner.stop("Backup not found")
prompts.log.error(`Backup not found: ${fullBackupPath}`)
prompts.log.info("Use --list-backups to see available backups")
return
}
const backupInfo = JSON.parse(await Bun.file(backupInfoPath).text())
spinner.message(`Found backup from ${new Date(backupInfo.timestamp).toLocaleString()}`)
console.log(`\nš Rollback Preview:`)
console.log(` Backup: ${path.basename(fullBackupPath)}`)
console.log(` Created: ${new Date(backupInfo.timestamp).toLocaleString()}`)
console.log(` Files to restore: ${backupInfo.files?.length || 0}`)
console.log()
const shouldProceed = await prompts.confirm({
message: `Proceed with rollback? This will overwrite current files.`,
initialValue: false,
})
if (prompts.isCancel(shouldProceed) || !shouldProceed) {
prompts.log.info("Rollback cancelled")
return
}
spinner.start("Rolling back files...")
let restoredCount = 0
let failedCount = 0
for (const relativePath of backupInfo.files || []) {
try {
const backupFilePath = path.join(fullBackupPath, relativePath)
const originalFilePath = path.join(backupInfo.originalDirectory || projectPath, relativePath)
if (await Bun.file(backupFilePath).exists()) {
const content = await Bun.file(backupFilePath).text()
await Bun.write(originalFilePath, content)
restoredCount++
} else {
failedCount++
}
spinner.message(`Restored ${restoredCount} files...`)
} catch (error) {
failedCount++
prompts.log.warn(`Failed to restore ${relativePath}: ${error}`)
}
}
spinner.stop("Rollback complete")
console.log(`\nā
Rollback Results:`)
console.log(` Files restored: ${restoredCount}`)
if (failedCount > 0) {
console.log(` Files failed: ${failedCount}`)
}
prompts.outro("Rollback completed successfully")
} catch (error) {
spinner.stop("Rollback failed")
prompts.log.error(`Rollback failed: ${error instanceof Error ? error.message : String(error)}`)
}
}
// Memory Command - simplified
export const MemoryCommand = cmd({
command: "memory",
describe: "show learning and memory statistics",
builder: (yargs) =>
yargs
.option("clear-old", {
type: "number",
describe: "clear memories older than N days",
default: 0,
})
.option("project", {
type: "string",
describe: "project path for memory analysis",
default: process.cwd(),
}),
async handler(args) {
UI.empty()
prompts.intro("š§ YOLO Agent Memory & Learning")
const spinner = prompts.spinner()
spinner.start("Loading agent memory...")
try {
const agentMemory = new AgentMemory(args.project)
if (args.clearOld > 0) {
const removedCount = agentMemory.clearOldMemories(args.clearOld)
prompts.log.info(`Cleared ${removedCount} memories older than ${args.clearOld} days`)
}
const stats = agentMemory.getMemoryStats()
spinner.stop("Memory loaded")
console.log(`\nš Memory Statistics:\n`)
console.log(`Total Memories: ${stats.totalMemories}`)
console.log(`Success Rate: ${Math.round(stats.successRate * 100)}%`)
console.log(`Average Complexity: ${stats.averageComplexity.toFixed(2)}`)
console.log(`\nš¤ Language Breakdown:`)
Object.entries(stats.languageBreakdown)
.sort((a, b) => b[1] - a[1])
.forEach(([language, count]) => {
console.log(` ${language}: ${count} files`)
})
// Show learning patterns for top languages
const topLanguages = Object.entries(stats.languageBreakdown)
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([lang]) => lang)
for (const language of topLanguages) {
const patterns = agentMemory.getLearningPatterns(language)
if (patterns.length > 0) {
console.log(`\nšÆ ${language.toUpperCase()} Learning Patterns:`)
patterns.slice(0, 5).forEach(pattern => {
console.log(` ⢠${pattern.pattern}`)
console.log(` Frequency: ${pattern.frequency}, Success: ${Math.round(pattern.successRate * 100)}%`)
console.log(` ${pattern.recommendation}`)
})
}
}
} catch (error) {
spinner.stop("Memory analysis failed")
prompts.log.error(`Memory analysis failed: ${error instanceof Error ? error.message : String(error)}`)
}
prompts.outro("Memory analysis complete")
},
})
// Multi-Agent Status Command
const MultiAgentStatusCommand = cmd({
command: "status",
describe: "show multi-agent system status",
async handler() {
UI.empty()
prompts.intro("Multi-Agent System Status")
const spinner = prompts.spinner()
spinner.start("Checking system status...")
try {
const coordinator = new AgentCoordinator()
await coordinator.initializeAgents()
const status = coordinator.getStatus()
const healthCheck = await coordinator.healthCheck()
spinner.stop("Status retrieved")
// Display status
prompts.log.info(`Registered Agents: ${status.registeredAgents}`)
prompts.log.info(`Active Tasks: ${status.activeTasks}`)
prompts.log.info(`Queued Tasks: ${status.queuedTasks}`)
prompts.log.info(`Total Processed: ${status.totalTasksProcessed}`)
prompts.log.info(`Average Processing Time: ${Math.round(status.averageProcessingTime)}ms`)
prompts.log.info(`Success Rate: ${Math.round(status.successRate * 100)}%`)
prompts.log.info(`System Health: ${healthCheck ? "ā
Healthy" : "ā Issues detected"}`)
if (status.agentUtilization && Object.keys(status.agentUtilization).length > 0) {
prompts.log.info("Agent Utilization:")
Object.entries(status.agentUtilization).forEach(([agentId, utilization]) => {
prompts.log.info(` ${agentId}: ${Math.round(Number(utilization) * 100)}%`)
})
}
await coordinator.dispose()
} catch (error) {
spinner.stop("Status check failed")
prompts.log.error(`Status check failed: ${error instanceof Error ? error.message : String(error)}`)
}
prompts.outro("Status check complete")
},
})
// Main Multi-Agent Command
export const MultiAgentCommand = cmd({
command: "multiagent",
describe: "Abyss advanced multi-agent analysis system",
builder: (yargs) =>
yargs
.command(MultiAgentAnalyzeCommand)
.command(MultiAgentAnalyzeCommand)
.command(MemoryCommand)
.command(MultiAgentStatusCommand)
.demandCommand()
.help(),
async handler() {
// This will show help when no subcommand is provided
},
})
// Question-driven analysis using OpenCode's agent system
async function processWithQuestionDrivenAgents(content: string, filePath: string, spinner: any) {
const results = []
try {
// Generate questions based on content and context
const questionGenerator = new QuestionGenerator()
const task: AgentTask = {
id: `analysis-${Date.now()}`,
type: 'code-analysis',
data: content,
context: {
sessionId: `session-${Date.now()}`,
filePath,
language: detectLanguage(filePath),
fileSize: content.length,
lineCount: content.split('\n').length
},
reasoningModes: [ReasoningMode.ULTRATHINKING, ReasoningMode.HYBRID_REASONING],
priority: 1,
timeout: 30000
}
const questionResult = await questionGenerator.generateQuestions(task)
spinner.message(`Generated ${questionResult.questions.length} specialized analysis questions...`)
// Get available agents
const availableAgents = await Agent.list()
const agentMap = new Map(availableAgents.map(agent => [agent.name, agent]))
// Process each question with the appropriate agent
for (const question of questionResult.questions) {
const agentName = question.targetAgentName
if (!agentMap.has(agentName)) {
prompts.log.warn(`Agent '${agentName}' not found, skipping question: ${question.perspective}`)
continue
}
spinner.message(`${question.perspective}: Analyzing with ${agentName}...`)
try {
const agent = await Agent.get(agentName)
if (!agent) {
prompts.log.warn(`Agent '${agentName}' configuration is invalid`)
continue
}
// Create a session for this analysis
const sessionId = Identifier.ascending("session")
const session = await Session.create(sessionId)
const messageID = Identifier.ascending("message")
// Run the agent with the specific question
const analysisPrompt = `${question.question}
File: ${filePath}
Content:
\`\`\`
${content}
\`\`\`
Please provide a detailed analysis focusing on: ${question.perspective}`
const result = await Session.chat({
messageID,
sessionID: session.id,
modelID: agent.model?.modelID || "anthropic/claude-3-5-sonnet-20241022",
providerID: agent.model?.providerID || "anthropic",
mode: "build",
system: agent.prompt,
tools: agent.tools,
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: analysisPrompt,
},
],
})
// Extract the response
const textPart = result.parts.findLast((x) => x.type === "text")
const analysisResult = {
agentId: agentName,
agentType: agentName,
taskId: question.id,
reasoningMode: question.reasoningMode,
result: {
perspective: question.perspective,
question: question.question,
analysis: textPart?.text || "No analysis provided",
insights: extractInsights(textPart?.text || ""),
issues: extractIssues(textPart?.text || ""),
recommendations: extractRecommendations(textPart?.text || "")
},
confidence: calculateConfidence(textPart?.text || ""),
processingTime: 0, // TODO: Add timing
metadata: {
questionId: question.id,
questionPerspective: question.perspective,
originalQuestion: question.question,
agentUsed: agentName
}
}
results.push(analysisResult)
} catch (error) {
prompts.log.warn(`Failed to run ${agentName}: ${error instanceof Error ? error.message : String(error)}`)
}
}
return results
} catch (error) {
prompts.log.error(`Question-driven analysis failed: ${error instanceof Error ? error.message : String(error)}`)
return []
}
}
// Helper functions for result extraction
function extractInsights(text: string): string[] {
const insights = []
const lines = text.split('\n')
let inInsightSection = false
for (const line of lines) {
if (line.toLowerCase().includes('insight') && line.includes(':')) {
inInsightSection = true
continue
}
if (inInsightSection && line.trim().startsWith('ā¢') || line.trim().startsWith('-')) {
insights.push(line.trim().substring(1).trim())
} else if (inInsightSection && line.trim() === '') {
continue
} else if (inInsightSection && !line.trim().startsWith('ā¢') && !line.trim().startsWith('-')) {
inInsightSection = false
}
}
return insights
}
function extractIssues(text: string): string[] {
const issues = []
const lines = text.split('\n')
let inIssueSection = false
for (const line of lines) {
if ((line.toLowerCase().includes('issue') || line.toLowerCase().includes('problem') || line.toLowerCase().includes('error')) && line.includes(':')) {
inIssueSection = true
continue
}
if (inIssueSection && (line.trim().startsWith('ā¢') || line.trim().startsWith('-'))) {
issues.push(line.trim().substring(1).trim())
} else if (inIssueSection && line.trim() === '') {
continue
} else if (inIssueSection && !line.trim().startsWith('ā¢') && !line.trim().startsWith('-')) {
inIssueSection = false
}
}
return issues
}
function extractRecommendations(text: string): string[] {
const recommendations = []
const lines = text.split('\n')
let inRecommendationSection = false
for (const line of lines) {
if ((line.toLowerCase().includes('recommend') || line.toLowerCase().includes('suggest')) && line.includes(':')) {
inRecommendationSection = true
continue
}
if (inRecommendationSection && (line.trim().startsWith('ā¢') || line.trim().startsWith('-'))) {
recommendations.push(line.trim().substring(1).trim())
} else if (inRecommendationSection && line.trim() === '') {
continue
} else if (inRecommendationSection && !line.trim().startsWith('ā¢') && !line.trim().startsWith('-')) {
inRecommendationSection = false
}
}
return recommendations
}
function calculateConfidence(text: string): number {
// Simple confidence calculation based on text length and detail
const length = text.length
if (length > 1000) return 0.9
if (length > 500) return 0.7
if (length > 200) return 0.5
return 0.3
}
// Helper functions
function detectLanguage(filePath: string): string {
const ext = path.extname(filePath).toLowerCase()
const languageMap: { [key: string]: string } = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.java': 'java',
'.cpp': 'cpp',
'.c': 'c',
'.cs': 'csharp',
'.rb': 'ruby',
'.php': 'php',
'.go': 'go',
'.rs': 'rust',
'.swift': 'swift',
'.kt': 'kotlin',
'.scala': 'scala',
'.sh': 'bash',
'.sql': 'sql',
'.html': 'html',
'.css': 'css',
'.scss': 'scss',
'.less': 'less',
'.json': 'json',
'.xml': 'xml',
'.yaml': 'yaml',
'.yml': 'yaml',
'.md': 'markdown',
'.vue': 'vue',
'.svelte': 'svelte'
}
return languageMap[ext] || 'unknown'
}
function assessBasicComplexity(content: string): number {
const lines = content.split('\n').length
const functions = (content.match(/function\s+\w+|def\s+\w+|fn\s+\w+/g) || []).length
const classes = (content.match(/class\s+\w+|struct\s+\w+/g) || []).length
const conditions = (content.match(/\b(if|while|for|switch)\b/g) || []).length
// Normalize to 0-1 scale
return Math.min(1, (lines / 1000 + functions / 20 + classes / 10 + conditions / 50))
}
interface DisplayOptions {
showQuestions?: boolean
analysisMode?: string
}
async function displayResults(results: any[], outputFormat: string, filePath: string, options: DisplayOptions = {}) {
switch (outputFormat) {
case 'json':
await displayJsonResults(results)
break
case 'summary':
await displaySummaryResults(results, filePath, options)
break
case 'questions':
await displayQuestionResults(results, filePath, options)
break
case 'text':
default:
await displayTextResults(results, filePath, options)
break
}
}
async function displayJsonResults(results: any[]) {
console.log(JSON.stringify(results, null, 2))
}
async function displaySummaryResults(results: any[], filePath: string, options: DisplayOptions = {}) {
prompts.log.info(`Analysis Summary for: ${path.basename(filePath)}`)
if (options.analysisMode) {
prompts.log.info(`Analysis Mode: ${options.analysisMode === 'question-driven' ? 'š Question-Driven' : 'š§ Legacy'}`)
}
let totalConfidence = 0
let issueCount = 0
let insightCount = 0
let questionCount = 0
let perspectiveCount = 0
for (const result of results) {
if (result.confidence !== undefined) {
totalConfidence += result.confidence
}
if (result.result) {
if (result.result.issues) {
issueCount += Array.isArray(result.result.issues) ? result.result.issues.length : 1
}
if (result.result.insights) {
insightCount += Array.isArray(result.result.insights) ? result.result.insights.length : 1
}
}
// Count question-driven specific metrics
if (result.metadata?.questionPerspective) {
perspectiveCount++
}
if (result.metadata?.originalQuestion) {
questionCount++
}
}
const avgConfidence = results.length > 0 ? totalConfidence / results.length : 0
prompts.log.info(`Average Confidence: ${Math.round(avgConfidence * 100)}%`)
prompts.log.info(`Issues Found: ${issueCount}`)
prompts.log.info(`Insights Generated: ${insightCount}`)
prompts.log.info(`Analysis Methods: ${results.length}`)
if (questionCount > 0) {
prompts.log.info(`Questions Processed: ${questionCount}`)
prompts.log.info(`Perspectives Analyzed: ${perspectiveCount}`)
}
}
async function displayTextResults(results: any[], filePath: string, options: DisplayOptions = {}) {
console.log(`\nš Multi-Agent Analysis Results for: ${path.basename(filePath)}`)
if (options.analysisMode) {
console.log(`Analysis Mode: ${options.analysisMode === 'question-driven' ? 'š Question-Driven Analysis' : 'š§ Legacy Analysis'}`)
}
console.log()
for (let i = 0; i < results.length; i++) {
const result = results[i]
console.log(`${'='.repeat(50)}`)
console.log(`Analysis ${i + 1}: ${result.type || 'Unknown Type'}`)
console.log(`${'='.repeat(50)}`)
if (result.confidence !== undefined) {
const confidencePercent = Math.round(result.confidence * 100)
const confidenceBar = 'ā'.repeat(Math.floor(confidencePercent / 10)) + 'ā'.repeat(10 - Math.floor(confidencePercent / 10))
console.log(`Confidence: ${confidencePercent}% [${confidenceBar}]`)
}
if (result.processingTime) {
console.log(`Processing Time: ${result.processingTime}ms`)
}
if (result.reasoningMode) {
console.log(`Reasoning Mode: ${result.reasoningMode}`)
}
if (result.agentType) {
console.log(`Agent Type: ${result.agentType}`)
}
// Display question-driven specific information
if (result.metadata?.questionPerspective) {
console.log(`š Perspective: ${result.metadata.questionPerspective}`)
}
if (options.showQuestions && result.metadata?.originalQuestion) {
console.log(`ā Question: ${result.metadata.originalQuestion}`)
}
console.log()
// Display result content
if (result.result) {
displayResultContent(result.result, ' ')
}
// Display errors and warnings
if (result.errors && result.errors.length > 0) {
console.log(`ā Errors:`)
result.errors.forEach((error: string) => {
console.log(` ⢠${error}`)
})
console.log()
}
if (result.warnings && result.warnings.length > 0) {
console.log(`ā ļø Warnings:`)
result.warnings.forEach((warning: string) => {
console.log(` ⢠${warning}`)
})
console.log()
}
console.log()
}
}
async function displayQuestionResults(results: any[], filePath: string, _options: DisplayOptions = {}) {
console.log(`\nš Question-Driven Analysis for: ${path.basename(filePath)}\n`)
// Group results by perspective
const resultsByPerspective: { [perspective: string]: any[] } = {}
results.forEach(result => {
const perspective = result.metadata?.questionPerspective || 'General Analysis'
if (!resultsByPerspective[perspective]) {
resultsByPerspective[perspective] = []
}
resultsByPerspective[perspective].push(result)
})
// Display results grouped by perspective
for (const [perspective, perspectiveResults] of Object.entries(resultsByPerspective)) {
console.log(`${'ā'.repeat(60)}`)
console.log(`šÆ ${perspective}`)
console.log(`${'ā'.repeat(60)}`)
// Show the original question if available
const sampleResult = perspectiveResults[0]
if (sampleResult?.metadata?.originalQuestion) {
console.log(`ā Question: ${sampleResult.metadata.originalQuestion}`)
console.log()
}
// Calculate perspective metrics
const avgConfidence = perspectiveResults.reduce((sum, r) => sum + (r.confidence || 0), 0) / perspectiveResults.length
const totalProcessingTime = perspectiveResults.reduce((sum, r) => sum + (r.processingTime || 0), 0)
console.log(`š Perspective Metrics:`)
console.log(` ⢠Results: ${perspectiveResults.length}`)
console.log(` ⢠Average Confidence: ${Math.round(avgConfidence * 100)}%`)
console.log(` ⢠Total Processing Time: ${totalProcessingTime}ms`)
console.log()
// Display insights from this perspective
const allInsights: string[] = []
const allIssues: any[] = []
const allRecommendations: string[] = []
perspectiveResults.forEach(result => {
if (result.result) {
if (result.result.insights) {
allInsights.push(...Array.isArray(result.result.insights) ? result.result.insights : [result.result.insights])
}
if (result.result.issues) {
allIssues.push(...Array.isArray(result.result.issues) ? result.result.issues : [result.result.issues])
}
if (result.result.recommendations) {
allRecommendations.push(...Array.isArray(result.result.recommendations) ? result.result.recommendations : [result.result.recommendations])
}
}
})
if (allInsights.length > 0) {
console.log(`š” Key Insights:`)
allInsights.forEach(insight => {
console.log(` ⢠${insight}`)
})
console.log()
}
if (allIssues.length > 0) {
console.log(`š Issues Found:`)
allIssues.forEach(issue => {
if (typeof issue === 'string') {
console.log(` ⢠${issue}`)
} else if (issue.message) {
console.log(` ⢠${issue.severity || 'INFO'}: ${issue.message}`)
}
})
console.log()
}
if (allRecommendations.length > 0) {
console.log(`š Recommendations:`)
allRecommendations.forEach(rec => {
console.log(` ⢠${rec}`)
})
console.log()
}
console.log()
}
// Summary across all perspectives
console.log(`${'ā'.repeat(60)}`)
console.log(`š Overall Analysis Summary`)
console.log(`${'ā'.repeat(60)}`)
const totalConfidence = results.reduce((sum, r) => sum + (r.confidence || 0), 0) / results.length
const totalProcessingTime = results.reduce((sum, r) => sum + (r.processingTime || 0), 0)
const perspectiveCount = Object.keys(resultsByPerspective).length
console.log(`⢠Perspectives Analyzed: ${perspectiveCount}`)
console.log(`⢠Total Results: ${results.length}`)
console.log(`⢠Average Confidence: ${Math.round(totalConfidence * 100)}%`)
console.log(`⢠Total Processing Time: ${totalProcessingTime}ms`)
console.log()
}
function displayResultContent(content: any, indent: string = '') {
if (typeof content === 'string') {
console.log(`${indent}${content}`)
return
}
if (typeof content !== 'object' || content === null) {
console.log(`${indent}${String(content)}`)
return
}
// Handle specific result structures
if (content.insights && Array.isArray(content.insights)) {
console.log(`${indent}š” Insights:`)
content.insights.forEach((insight: string) => {
console.log(`${indent} ⢠${insight}`)
})
console.log()
}
if (content.issues && Array.isArray(content.issues)) {
console.log(`${indent}š Issues Found:`)
content.issues.forEach((issue: any) => {
if (typeof issue === 'string') {
console.log(`${indent} ⢠${issue}`)
} else if (issue.message) {
console.log(`${indent} ⢠${issue.severity || 'INFO'}: ${issue.message