Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/cli/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
} from '../../agents/spawner.js';
import { listAgentTypes, getAgentDefinition } from '../../agents/registry.js';
import { getConfig } from '../../utils/config.js';
import { getSmartDispatcher } from '../../tasks/smart-dispatcher.js';
import { readFileSync, existsSync } from 'node:fs';
import { runAgentWatch, type AgentWatchOptions } from './agent-watch.js';
import * as readline from 'node:readline';

export function createAgentCommand(): Command {
const command = new Command('agent')
Expand Down Expand Up @@ -375,5 +377,114 @@ export function createAgentCommand(): Command {
}
});

// auto subcommand - automatically select agent type and run
command
.command('auto')
.description('Automatically select the best agent type for a task and run it')
.argument('<description>', 'Task description')
.option('--dry-run', 'Only show agent selection, do not execute')
.option('--confirm', 'Ask for confirmation before executing')
.option('--provider <provider>', 'Provider to use (claude-code, gemini-cli, codex, anthropic, openai, ollama)')
.option('--model <model>', 'Model to use')
.option('--context <context>', 'Additional context or @file to read from file')
.action(async (description, options) => {
const {
dryRun,
confirm,
provider,
model,
context: rawContext,
} = options as {
dryRun?: boolean;
confirm?: boolean;
provider?: string;
model?: string;
context?: string;
};

try {
const config = getConfig();
const dispatcher = getSmartDispatcher(config);

if (!dispatcher.isEnabled()) {
console.error('Error: Smart dispatcher is not enabled or no provider configured');
process.exit(1);
}

console.log('Analyzing task description...\n');

const result = await dispatcher.dispatch(description);

if (!result.success || !result.decision) {
console.error(`Error: ${result.error ?? 'Failed to dispatch task'}`);
process.exit(1);
}

const { agentType, confidence, reasoning, cached, latencyMs } = result.decision;

console.log(`Selected agent: ${agentType}`);
console.log(`Confidence: ${Math.round(confidence * 100)}%`);
console.log(`Reasoning: ${reasoning}`);
if (cached) {
console.log(`(Cached result)`);
}
console.log(`Dispatch latency: ${latencyMs}ms\n`);

if (dryRun) {
console.log('Dry run - not executing agent.');
return;
}

// Ask for confirmation if requested
if (confirm) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const answer = await new Promise<string>((resolve) => {
rl.question('Proceed with this agent? (y/N): ', resolve);
});
rl.close();

if (answer.toLowerCase() !== 'y') {
console.log('Aborted.');
return;
}
}

// Read context from file if it starts with @
let context = rawContext;
if (rawContext?.startsWith('@')) {
const filePath = rawContext.slice(1);
if (!existsSync(filePath)) {
console.error(`Error: Context file not found: ${filePath}`);
process.exit(1);
}
context = readFileSync(filePath, 'utf-8');
}

console.log(`Running ${agentType} agent...\n`);

const agentResult = await runAgent(agentType, description, config, {
provider,
model,
context,
});

console.log('─'.repeat(60));
console.log('Response:');
console.log('─'.repeat(60));
console.log(agentResult.response);
console.log('─'.repeat(60));
console.log(`\nAgent: ${agentResult.agentId}`);
console.log(`Model: ${agentResult.model}`);
console.log(`Duration: ${(agentResult.duration / 1000).toFixed(2)}s`);
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});

return command;
}
5 changes: 4 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from './tools/index.js';
import { DriftDetectionService } from '../tasks/drift-detection-service.js';
import { ConsensusService } from '../tasks/consensus-service.js';
import { SmartDispatcher } from '../tasks/smart-dispatcher.js';

const log = logger.child('mcp');

Expand All @@ -39,12 +40,14 @@ export class MCPServer {
private config: AgentStackConfig;
private driftService: DriftDetectionService;
private consensusService: ConsensusService;
private smartDispatcher: SmartDispatcher;

constructor(config: AgentStackConfig) {
this.config = config;
this.memory = new MemoryManager(config);
this.driftService = new DriftDetectionService(this.memory.getStore(), config);
this.consensusService = new ConsensusService(this.memory.getStore(), config);
this.smartDispatcher = new SmartDispatcher(config);

this.server = new Server(
{
Expand All @@ -68,7 +71,7 @@ export class MCPServer {
createAgentTools(this.config),
createIdentityTools(this.config),
createMemoryTools(this.memory),
createTaskTools(this.memory, this.driftService, this.consensusService),
createTaskTools(this.memory, this.driftService, this.consensusService, this.smartDispatcher, this.config),
createSessionTools(this.memory),
createSystemTools(this.memory, this.config),
createGitHubTools(this.config),
Expand Down
48 changes: 37 additions & 11 deletions src/mcp/tools/task-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { randomUUID } from 'node:crypto';
import type { MemoryManager } from '../../memory/index.js';
import type { DriftDetectionService } from '../../tasks/drift-detection-service.js';
import type { ConsensusService } from '../../tasks/consensus-service.js';
import type { TaskRiskLevel, ProposedSubtask } from '../../types.js';
import type { SmartDispatcher } from '../../tasks/smart-dispatcher.js';
import type { TaskRiskLevel, ProposedSubtask, AgentStackConfig } from '../../types.js';

// Input schemas
const CreateInputSchema = z.object({
agentType: z.string().min(1).describe('Agent type for this task'),
agentType: z.string().min(1).optional().describe('Agent type for this task (optional - will auto-dispatch if not provided)'),
input: z.string().optional().describe('Task input/description'),
sessionId: z.string().uuid().optional().describe('Session to associate with'),
parentTaskId: z.string().uuid().optional().describe('Parent task ID for drift detection'),
Expand Down Expand Up @@ -52,33 +53,57 @@ const GetRelationshipsInputSchema = z.object({
export function createTaskTools(
memory: MemoryManager,
driftService?: DriftDetectionService,
consensusService?: ConsensusService
consensusService?: ConsensusService,
smartDispatcher?: SmartDispatcher,
config?: AgentStackConfig
) {
return {
task_create: {
name: 'task_create',
description: 'Create a new task with optional drift detection and consensus checking',
description: 'Create a new task with optional drift detection and consensus checking. Agent type is auto-detected if not specified.',
inputSchema: {
type: 'object',
properties: {
agentType: { type: 'string', description: 'Agent type for this task' },
agentType: { type: 'string', description: 'Agent type for this task (optional - auto-dispatched if not provided)' },
input: { type: 'string', description: 'Task input/description' },
sessionId: { type: 'string', description: 'Session to associate with' },
parentTaskId: { type: 'string', description: 'Parent task ID for drift detection' },
riskLevel: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Risk level for consensus checking' },
},
required: ['agentType'],
required: [],
},
handler: async (params: Record<string, unknown>) => {
const input = CreateInputSchema.parse(params);

try {
// Auto-dispatch agent type if not specified
let agentType = input.agentType;
let dispatchInfo: { agentType: string; confidence: number; reasoning: string; cached: boolean } | undefined;

if (!agentType && input.input && smartDispatcher?.isEnabled()) {
const dispatchResult = await smartDispatcher.dispatch(input.input);
if (dispatchResult.success && dispatchResult.decision) {
agentType = dispatchResult.decision.agentType;
dispatchInfo = {
agentType: dispatchResult.decision.agentType,
confidence: dispatchResult.decision.confidence,
reasoning: dispatchResult.decision.reasoning,
cached: dispatchResult.decision.cached,
};
}
}

// Fallback to config default if still no agent type
if (!agentType) {
agentType = config?.smartDispatcher?.fallbackAgentType ?? 'coder';
}

// Check for drift if service is available and input is provided
let driftResult = null;
if (driftService && input.input && input.parentTaskId) {
driftResult = await driftService.checkDrift(
input.input,
input.agentType,
agentType,
input.parentTaskId
);

Expand All @@ -102,7 +127,7 @@ export function createTaskTools(
if (consensusService && consensusService.isEnabled()) {
// Estimate or use provided risk level
const riskLevel: TaskRiskLevel = input.riskLevel ||
consensusService.estimateRiskLevel(input.agentType, input.input);
consensusService.estimateRiskLevel(agentType, input.input);

// Calculate depth
const depth = consensusService.calculateTaskDepth(input.parentTaskId);
Expand All @@ -119,7 +144,7 @@ export function createTaskTools(
const subtaskId = randomUUID();
const proposedSubtask: ProposedSubtask = {
id: subtaskId,
agentType: input.agentType,
agentType,
input: input.input || '',
estimatedRiskLevel: riskLevel,
parentTaskId: input.parentTaskId || '',
Expand Down Expand Up @@ -154,11 +179,11 @@ export function createTaskTools(

// Get risk level and depth for task creation
const riskLevel: TaskRiskLevel | undefined = input.riskLevel ||
(consensusService ? consensusService.estimateRiskLevel(input.agentType, input.input) : undefined);
(consensusService ? consensusService.estimateRiskLevel(agentType, input.input) : undefined);
const depth = consensusService?.calculateTaskDepth(input.parentTaskId);

const task = memory.createTask(
input.agentType,
agentType,
input.input,
input.sessionId,
{
Expand Down Expand Up @@ -194,6 +219,7 @@ export function createTaskTools(
riskLevel: task.riskLevel,
depth: task.depth,
},
dispatch: dispatchInfo,
drift: driftResult ? {
isDrift: driftResult.isDrift,
highestSimilarity: driftResult.highestSimilarity,
Expand Down
6 changes: 6 additions & 0 deletions src/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ export {
getDriftDetectionService,
resetDriftDetectionService,
} from './drift-detection-service.js';

export {
SmartDispatcher,
getSmartDispatcher,
resetSmartDispatcher,
} from './smart-dispatcher.js';
Loading
Loading