@hanzo/dev 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,552 @@
1
+ import { EventEmitter } from 'events';
2
+ import chalk from 'chalk';
3
+ import { FunctionCallingSystem, FunctionCall } from './function-calling';
4
+ import { MCPClient, MCPSession } from './mcp-client';
5
+
6
+ export interface LLMProvider {
7
+ name: string;
8
+ type: 'openai' | 'anthropic' | 'local' | 'hanzo-app';
9
+ apiKey?: string;
10
+ baseUrl?: string;
11
+ model: string;
12
+ supportsTools: boolean;
13
+ supportsStreaming: boolean;
14
+ }
15
+
16
+ export interface AgentLoopConfig {
17
+ provider: LLMProvider;
18
+ maxIterations: number;
19
+ enableMCP: boolean;
20
+ enableBrowser: boolean;
21
+ enableSwarm: boolean;
22
+ streamOutput: boolean;
23
+ confirmActions: boolean;
24
+ }
25
+
26
+ export interface AgentMessage {
27
+ role: 'system' | 'user' | 'assistant' | 'tool';
28
+ content: string;
29
+ toolCalls?: FunctionCall[];
30
+ toolResults?: any[];
31
+ }
32
+
33
+ export class ConfigurableAgentLoop extends EventEmitter {
34
+ private config: AgentLoopConfig;
35
+ private functionCalling: FunctionCallingSystem;
36
+ private mcpClient: MCPClient;
37
+ private messages: AgentMessage[] = [];
38
+ private iterations: number = 0;
39
+
40
+ constructor(config: AgentLoopConfig) {
41
+ super();
42
+ this.config = config;
43
+ this.functionCalling = new FunctionCallingSystem();
44
+ this.mcpClient = new MCPClient();
45
+ }
46
+
47
+ // Get available LLM providers
48
+ static getAvailableProviders(): LLMProvider[] {
49
+ const providers: LLMProvider[] = [];
50
+
51
+ // Check for API keys
52
+ if (process.env.ANTHROPIC_API_KEY) {
53
+ providers.push({
54
+ name: 'Claude (Anthropic)',
55
+ type: 'anthropic',
56
+ apiKey: process.env.ANTHROPIC_API_KEY,
57
+ model: 'claude-3-opus-20240229',
58
+ supportsTools: true,
59
+ supportsStreaming: true
60
+ });
61
+ }
62
+
63
+ if (process.env.OPENAI_API_KEY) {
64
+ providers.push({
65
+ name: 'GPT-4 (OpenAI)',
66
+ type: 'openai',
67
+ apiKey: process.env.OPENAI_API_KEY,
68
+ model: 'gpt-4-turbo-preview',
69
+ supportsTools: true,
70
+ supportsStreaming: true
71
+ });
72
+ }
73
+
74
+ // Check for local Hanzo App
75
+ if (process.env.HANZO_APP_URL || this.isHanzoAppRunning()) {
76
+ providers.push({
77
+ name: 'Hanzo Local AI',
78
+ type: 'hanzo-app',
79
+ baseUrl: process.env.HANZO_APP_URL || 'http://localhost:8080',
80
+ model: 'hanzo-zen',
81
+ supportsTools: true,
82
+ supportsStreaming: false
83
+ });
84
+ }
85
+
86
+ // Check for other local models
87
+ if (process.env.LOCAL_LLM_URL) {
88
+ providers.push({
89
+ name: 'Local LLM',
90
+ type: 'local',
91
+ baseUrl: process.env.LOCAL_LLM_URL,
92
+ model: process.env.LOCAL_LLM_MODEL || 'llama2',
93
+ supportsTools: false,
94
+ supportsStreaming: true
95
+ });
96
+ }
97
+
98
+ return providers;
99
+ }
100
+
101
+ private static isHanzoAppRunning(): boolean {
102
+ // Check if Hanzo App is running locally
103
+ try {
104
+ const { execSync } = require('child_process');
105
+ execSync('curl -s http://localhost:8080/health', { stdio: 'ignore' });
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ // Initialize tools and connections
113
+ async initialize(): Promise<void> {
114
+ console.log(chalk.cyan('\n🔧 Initializing agent loop...\n'));
115
+
116
+ // Initialize MCP tools if enabled
117
+ if (this.config.enableMCP) {
118
+ await this.initializeMCPTools();
119
+ }
120
+
121
+ // Initialize browser connection if enabled
122
+ if (this.config.enableBrowser) {
123
+ await this.initializeBrowserTools();
124
+ }
125
+
126
+ // Initialize swarm if enabled
127
+ if (this.config.enableSwarm) {
128
+ await this.initializeSwarmTools();
129
+ }
130
+
131
+ console.log(chalk.green('✓ Agent loop initialized\n'));
132
+ }
133
+
134
+ private async initializeMCPTools(): Promise<void> {
135
+ console.log(chalk.gray('Loading MCP tools...'));
136
+
137
+ // Load configured MCP servers from config file
138
+ const mcpConfig = await this.loadMCPConfig();
139
+
140
+ for (const server of mcpConfig.servers) {
141
+ try {
142
+ console.log(chalk.gray(` Connecting to ${server.name}...`));
143
+ const session = await this.mcpClient.connect(server);
144
+ await this.functionCalling.registerMCPServer(server.name, session);
145
+ console.log(chalk.green(` ✓ Connected to ${server.name} (${session.tools.length} tools)`));
146
+ } catch (error) {
147
+ console.log(chalk.yellow(` ⚠ Failed to connect to ${server.name}`));
148
+ }
149
+ }
150
+ }
151
+
152
+ private async initializeBrowserTools(): Promise<void> {
153
+ console.log(chalk.gray('Connecting to browser...'));
154
+
155
+ // Check for Hanzo Browser Extension
156
+ if (await this.checkBrowserExtension()) {
157
+ console.log(chalk.green(' ✓ Connected to Hanzo Browser Extension'));
158
+ this.registerBrowserTools('extension');
159
+ }
160
+ // Check for Hanzo Browser
161
+ else if (await this.checkHanzoBrowser()) {
162
+ console.log(chalk.green(' ✓ Connected to Hanzo Browser'));
163
+ this.registerBrowserTools('browser');
164
+ } else {
165
+ console.log(chalk.yellow(' ⚠ No browser connection available'));
166
+ }
167
+ }
168
+
169
+ private async checkBrowserExtension(): Promise<boolean> {
170
+ // Check if browser extension is available via WebSocket
171
+ try {
172
+ const ws = new (require('ws'))('ws://localhost:9222/hanzo-extension');
173
+ return new Promise((resolve) => {
174
+ ws.on('open', () => {
175
+ ws.close();
176
+ resolve(true);
177
+ });
178
+ ws.on('error', () => resolve(false));
179
+ setTimeout(() => {
180
+ ws.close();
181
+ resolve(false);
182
+ }, 1000);
183
+ });
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ private async checkHanzoBrowser(): Promise<boolean> {
190
+ // Check if Hanzo Browser is running
191
+ try {
192
+ const response = await fetch('http://localhost:9223/status');
193
+ return response.ok;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ private registerBrowserTools(type: 'extension' | 'browser'): void {
200
+ // Register browser automation tools
201
+ const browserTools = [
202
+ {
203
+ name: 'browser_navigate',
204
+ description: 'Navigate to a URL in the browser',
205
+ parameters: {
206
+ type: 'object',
207
+ properties: {
208
+ url: { type: 'string', description: 'URL to navigate to' }
209
+ },
210
+ required: ['url']
211
+ },
212
+ handler: async (args: any) => this.browserNavigate(args.url)
213
+ },
214
+ {
215
+ name: 'browser_click',
216
+ description: 'Click on an element in the browser',
217
+ parameters: {
218
+ type: 'object',
219
+ properties: {
220
+ selector: { type: 'string', description: 'CSS selector or element ID' }
221
+ },
222
+ required: ['selector']
223
+ },
224
+ handler: async (args: any) => this.browserClick(args.selector)
225
+ },
226
+ {
227
+ name: 'browser_screenshot',
228
+ description: 'Take a screenshot of the current page',
229
+ parameters: {
230
+ type: 'object',
231
+ properties: {
232
+ fullPage: { type: 'boolean', description: 'Capture full page' }
233
+ }
234
+ },
235
+ handler: async (args: any) => this.browserScreenshot(args.fullPage)
236
+ },
237
+ {
238
+ name: 'browser_fill',
239
+ description: 'Fill a form field in the browser',
240
+ parameters: {
241
+ type: 'object',
242
+ properties: {
243
+ selector: { type: 'string', description: 'CSS selector' },
244
+ value: { type: 'string', description: 'Value to fill' }
245
+ },
246
+ required: ['selector', 'value']
247
+ },
248
+ handler: async (args: any) => this.browserFill(args.selector, args.value)
249
+ }
250
+ ];
251
+
252
+ browserTools.forEach(tool => this.functionCalling.registerTool(tool));
253
+ }
254
+
255
+ private async initializeSwarmTools(): Promise<void> {
256
+ console.log(chalk.gray('Initializing swarm tools...'));
257
+
258
+ // Register swarm coordination tools
259
+ this.functionCalling.registerTool({
260
+ name: 'spawn_agent',
261
+ description: 'Spawn a new agent for a specific task',
262
+ parameters: {
263
+ type: 'object',
264
+ properties: {
265
+ task: { type: 'string', description: 'Task for the agent' },
266
+ agentType: { type: 'string', enum: ['claude-code', 'aider', 'openhands'] }
267
+ },
268
+ required: ['task']
269
+ },
270
+ handler: async (args: any) => this.spawnAgent(args.task, args.agentType)
271
+ });
272
+
273
+ this.functionCalling.registerTool({
274
+ name: 'delegate_to_swarm',
275
+ description: 'Delegate multiple tasks to agent swarm',
276
+ parameters: {
277
+ type: 'object',
278
+ properties: {
279
+ tasks: {
280
+ type: 'array',
281
+ items: { type: 'string' },
282
+ description: 'List of tasks to delegate'
283
+ }
284
+ },
285
+ required: ['tasks']
286
+ },
287
+ handler: async (args: any) => this.delegateToSwarm(args.tasks)
288
+ });
289
+ }
290
+
291
+ // Execute agent loop
292
+ async execute(initialPrompt: string): Promise<void> {
293
+ console.log(chalk.cyan(`\n🤖 Starting agent loop with ${this.config.provider.name}\n`));
294
+
295
+ // Add system message
296
+ this.messages.push({
297
+ role: 'system',
298
+ content: this.getSystemPrompt()
299
+ });
300
+
301
+ // Add user message
302
+ this.messages.push({
303
+ role: 'user',
304
+ content: initialPrompt
305
+ });
306
+
307
+ // Main agent loop
308
+ while (this.iterations < this.config.maxIterations) {
309
+ this.iterations++;
310
+ console.log(chalk.blue(`\n▶ Iteration ${this.iterations}`));
311
+
312
+ try {
313
+ // Get LLM response
314
+ const response = await this.callLLM();
315
+
316
+ // Process response
317
+ if (response.toolCalls && response.toolCalls.length > 0) {
318
+ // Execute tool calls
319
+ const results = await this.executeToolCalls(response.toolCalls);
320
+
321
+ // Add tool results to messages
322
+ this.messages.push({
323
+ role: 'tool',
324
+ content: JSON.stringify(results),
325
+ toolResults: results
326
+ });
327
+ } else {
328
+ // No more tool calls, task complete
329
+ console.log(chalk.green('\n✅ Task completed'));
330
+ break;
331
+ }
332
+ } catch (error) {
333
+ console.error(chalk.red(`Error in iteration ${this.iterations}: ${error}`));
334
+ this.emit('error', error);
335
+ break;
336
+ }
337
+ }
338
+
339
+ if (this.iterations >= this.config.maxIterations) {
340
+ console.log(chalk.yellow('\n⚠ Maximum iterations reached'));
341
+ }
342
+ }
343
+
344
+ private getSystemPrompt(): string {
345
+ const tools = this.functionCalling.getAvailableTools();
346
+
347
+ return `You are an AI assistant with access to various tools. You can:
348
+ - Edit files using view_file, create_file, str_replace
349
+ - Run commands using run_command
350
+ - Search files using search_files
351
+ - List directories using list_directory
352
+ ${this.config.enableBrowser ? '- Control the browser using browser_* tools' : ''}
353
+ ${this.config.enableSwarm ? '- Spawn agents and delegate tasks using swarm tools' : ''}
354
+ ${this.config.enableMCP ? '- Use MCP tools for extended functionality' : ''}
355
+
356
+ Available tools: ${tools.map(t => t.name).join(', ')}
357
+
358
+ Always use tools to accomplish tasks. Think step by step.`;
359
+ }
360
+
361
+ private async callLLM(): Promise<AgentMessage> {
362
+ const { provider } = this.config;
363
+
364
+ switch (provider.type) {
365
+ case 'anthropic':
366
+ return this.callAnthropic();
367
+ case 'openai':
368
+ return this.callOpenAI();
369
+ case 'hanzo-app':
370
+ return this.callHanzoApp();
371
+ case 'local':
372
+ return this.callLocalLLM();
373
+ default:
374
+ throw new Error(`Unknown provider type: ${provider.type}`);
375
+ }
376
+ }
377
+
378
+ private async callAnthropic(): Promise<AgentMessage> {
379
+ // Implementation for Anthropic Claude
380
+ console.log(chalk.gray('Calling Claude...'));
381
+
382
+ // This is a simplified version - in production you'd use the actual API
383
+ const tools = this.functionCalling.getAllToolSchemas();
384
+
385
+ // Simulate API call
386
+ const response = {
387
+ role: 'assistant' as const,
388
+ content: 'I will help you with this task.',
389
+ toolCalls: [
390
+ {
391
+ id: 'call_1',
392
+ name: 'view_file',
393
+ arguments: { path: 'package.json' }
394
+ }
395
+ ]
396
+ };
397
+
398
+ this.messages.push(response);
399
+ return response;
400
+ }
401
+
402
+ private async callOpenAI(): Promise<AgentMessage> {
403
+ // Implementation for OpenAI
404
+ console.log(chalk.gray('Calling GPT-4...'));
405
+
406
+ // Similar to Anthropic but with OpenAI API format
407
+ const response = {
408
+ role: 'assistant' as const,
409
+ content: 'I will analyze and complete this task.',
410
+ toolCalls: []
411
+ };
412
+
413
+ this.messages.push(response);
414
+ return response;
415
+ }
416
+
417
+ private async callHanzoApp(): Promise<AgentMessage> {
418
+ // Implementation for local Hanzo App
419
+ console.log(chalk.gray('Calling Hanzo Local AI...'));
420
+
421
+ const response = await fetch(`${this.config.provider.baseUrl}/chat`, {
422
+ method: 'POST',
423
+ headers: { 'Content-Type': 'application/json' },
424
+ body: JSON.stringify({
425
+ messages: this.messages,
426
+ tools: this.functionCalling.getAllToolSchemas()
427
+ })
428
+ });
429
+
430
+ const data = await response.json();
431
+ this.messages.push(data);
432
+ return data;
433
+ }
434
+
435
+ private async callLocalLLM(): Promise<AgentMessage> {
436
+ // Implementation for generic local LLM
437
+ console.log(chalk.gray('Calling Local LLM...'));
438
+
439
+ // Local LLMs might not support tools, so we use a different approach
440
+ const response = {
441
+ role: 'assistant' as const,
442
+ content: 'Processing your request...',
443
+ toolCalls: []
444
+ };
445
+
446
+ this.messages.push(response);
447
+ return response;
448
+ }
449
+
450
+ private async executeToolCalls(calls: FunctionCall[]): Promise<any[]> {
451
+ if (this.config.confirmActions) {
452
+ console.log(chalk.yellow('\n⚠ Tool calls requested:'));
453
+ calls.forEach(call => {
454
+ console.log(` - ${call.name}(${JSON.stringify(call.arguments)})`);
455
+ });
456
+
457
+ const { confirm } = await require('inquirer').prompt([{
458
+ type: 'confirm',
459
+ name: 'confirm',
460
+ message: 'Execute these actions?',
461
+ default: true
462
+ }]);
463
+
464
+ if (!confirm) {
465
+ throw new Error('User cancelled tool execution');
466
+ }
467
+ }
468
+
469
+ return this.functionCalling.callFunctions(calls);
470
+ }
471
+
472
+ // Browser tool implementations
473
+ private async browserNavigate(url: string): Promise<any> {
474
+ console.log(chalk.gray(`Navigating to ${url}...`));
475
+ // Implementation would connect to browser
476
+ return { success: true, url };
477
+ }
478
+
479
+ private async browserClick(selector: string): Promise<any> {
480
+ console.log(chalk.gray(`Clicking ${selector}...`));
481
+ return { success: true, selector };
482
+ }
483
+
484
+ private async browserScreenshot(fullPage: boolean = false): Promise<any> {
485
+ console.log(chalk.gray(`Taking screenshot (fullPage: ${fullPage})...`));
486
+ return { success: true, screenshot: 'base64_image_data' };
487
+ }
488
+
489
+ private async browserFill(selector: string, value: string): Promise<any> {
490
+ console.log(chalk.gray(`Filling ${selector} with "${value}"...`));
491
+ return { success: true, selector, value };
492
+ }
493
+
494
+ // Swarm tool implementations
495
+ private async spawnAgent(task: string, agentType?: string): Promise<any> {
496
+ console.log(chalk.gray(`Spawning ${agentType || 'default'} agent for: ${task}`));
497
+ return { success: true, agentId: `agent-${Date.now()}`, task };
498
+ }
499
+
500
+ private async delegateToSwarm(tasks: string[]): Promise<any> {
501
+ console.log(chalk.gray(`Delegating ${tasks.length} tasks to swarm...`));
502
+ return { success: true, tasks, status: 'delegated' };
503
+ }
504
+
505
+ // Load MCP configuration
506
+ private async loadMCPConfig(): Promise<{ servers: any[] }> {
507
+ const fs = require('fs');
508
+ const path = require('path');
509
+ const os = require('os');
510
+
511
+ // Check multiple locations for MCP config
512
+ const configPaths = [
513
+ path.join(process.cwd(), '.mcp.json'),
514
+ path.join(os.homedir(), '.config', 'hanzo-dev', 'mcp.json'),
515
+ path.join(os.homedir(), '.hanzo', 'mcp.json')
516
+ ];
517
+
518
+ for (const configPath of configPaths) {
519
+ if (fs.existsSync(configPath)) {
520
+ const content = fs.readFileSync(configPath, 'utf-8');
521
+ return JSON.parse(content);
522
+ }
523
+ }
524
+
525
+ // Default MCP servers
526
+ return {
527
+ servers: [
528
+ {
529
+ name: 'filesystem',
530
+ command: 'npx',
531
+ args: ['@modelcontextprotocol/server-filesystem'],
532
+ env: { MCP_ALLOWED_PATHS: process.cwd() }
533
+ },
534
+ {
535
+ name: 'git',
536
+ command: 'npx',
537
+ args: ['@modelcontextprotocol/server-git']
538
+ }
539
+ ]
540
+ };
541
+ }
542
+
543
+ // Get current configuration
544
+ getConfig(): AgentLoopConfig {
545
+ return this.config;
546
+ }
547
+
548
+ // Update configuration
549
+ updateConfig(updates: Partial<AgentLoopConfig>): void {
550
+ this.config = { ...this.config, ...updates };
551
+ }
552
+ }