main
  1import { Config, ModelInfo, parseModelPrefix } from "../config.js";
  2import { SessionManager } from "./session.js";
  3import { SessionEntry, ConversationMessage } from "./types.js";
  4import { SYSTEM_PROMPT, HELP_TEXT, MODELS_TEXT } from "./prompts.js";
  5import { ToolRegistry } from "../tools/index.js";
  6import { createProvider, LLMProvider, Message } from "../llm/index.js";
  7import { bareJid } from "../xmpp/types.js";
  8
  9const MAX_TOOL_ITERATIONS = 10;
 10
 11export class AgentRunner {
 12  private config: Config;
 13  private sessionManager: SessionManager;
 14  private toolRegistry: ToolRegistry;
 15  private jid: string;
 16  private processing: boolean = false;
 17  private messageQueue: Array<{ message: string; resolve: (response: string) => void }> = [];
 18
 19  constructor(
 20    jid: string,
 21    config: Config,
 22    sessionManager: SessionManager,
 23    toolRegistry: ToolRegistry
 24  ) {
 25    this.jid = bareJid(jid);
 26    this.config = config;
 27    this.sessionManager = sessionManager;
 28    this.toolRegistry = toolRegistry;
 29  }
 30
 31  async processMessage(message: string): Promise<string> {
 32    return new Promise((resolve) => {
 33      this.messageQueue.push({ message, resolve });
 34      this.processQueue();
 35    });
 36  }
 37
 38  private async processQueue(): Promise<void> {
 39    if (this.processing || this.messageQueue.length === 0) {
 40      return;
 41    }
 42
 43    this.processing = true;
 44
 45    while (this.messageQueue.length > 0) {
 46      const item = this.messageQueue.shift()!;
 47      try {
 48        const response = await this.handleMessage(item.message);
 49        item.resolve(response);
 50      } catch (err) {
 51        item.resolve(`Error: ${(err as Error).message}`);
 52      }
 53    }
 54
 55    this.processing = false;
 56  }
 57
 58  private async handleMessage(message: string): Promise<string> {
 59    // Handle slash commands
 60    if (message.startsWith("/")) {
 61      return this.handleCommand(message);
 62    }
 63
 64    // Parse model prefix
 65    const { model, content } = parseModelPrefix(message);
 66    const modelInfo = model || this.config.llm.defaultModel;
 67
 68    // Log user message
 69    await this.sessionManager.appendEntry(this.jid, {
 70      timestamp: new Date().toISOString(),
 71      type: "user",
 72      content,
 73      model: modelInfo,
 74    });
 75
 76    // Get conversation history
 77    const history = await this.sessionManager.getConversationHistory(this.jid);
 78
 79    // Create provider for this request
 80    const provider = createProvider(modelInfo, this.config.llm.providers);
 81
 82    // Run agent loop
 83    const response = await this.runAgentLoop(provider, history, content);
 84
 85    // Log assistant response
 86    await this.sessionManager.appendEntry(this.jid, {
 87      timestamp: new Date().toISOString(),
 88      type: "assistant",
 89      content: response,
 90      model: modelInfo,
 91    });
 92
 93    return response;
 94  }
 95
 96  private async handleCommand(message: string): Promise<string> {
 97    const parts = message.slice(1).split(/\s+/);
 98    const command = parts[0].toLowerCase();
 99
100    switch (command) {
101      case "ping":
102        return "Pong!";
103
104      case "help":
105        return HELP_TEXT;
106
107      case "models":
108        return MODELS_TEXT;
109
110      case "status": {
111        const statusTool = this.toolRegistry.get("status");
112        if (statusTool) {
113          const result = await statusTool.execute({});
114          return `**System Status:**\n\`\`\`json\n${result}\n\`\`\``;
115        }
116        return "Status tool not available";
117      }
118
119      case "clear":
120        await this.sessionManager.clearSession(this.jid);
121        return "Conversation history cleared.";
122
123      case "stats": {
124        const stats = await this.sessionManager.getSessionStats(this.jid);
125        return `**Session Stats:**\n- Messages: ${stats.messageCount}\n- First: ${stats.firstMessage || "N/A"}\n- Last: ${stats.lastMessage || "N/A"}`;
126      }
127
128      default:
129        return `Unknown command: /${command}\nType /help for available commands.`;
130    }
131  }
132
133  private async runAgentLoop(
134    provider: LLMProvider,
135    history: ConversationMessage[],
136    currentMessage: string
137  ): Promise<string> {
138    const messages: Message[] = [
139      ...history,
140      { role: "user", content: currentMessage },
141    ];
142
143    const tools = this.toolRegistry.getSchemas();
144
145    let iterations = 0;
146    let finalResponse = "";
147
148    while (iterations < MAX_TOOL_ITERATIONS) {
149      iterations++;
150
151      const response = await provider.chat(messages, {
152        systemPrompt: SYSTEM_PROMPT,
153        tools,
154        maxTokens: 4096,
155      });
156
157      // Handle tool calls
158      if (response.stopReason === "tool_use" && response.toolCalls.length > 0) {
159        // Add assistant message with tool calls
160        if (response.content) {
161          messages.push({ role: "assistant", content: response.content });
162        }
163
164        // Execute each tool
165        for (const toolCall of response.toolCalls) {
166          const tool = this.toolRegistry.get(toolCall.name);
167          if (!tool) {
168            messages.push({
169              role: "user",
170              content: `Tool '${toolCall.name}' not found`,
171            });
172            continue;
173          }
174
175          // Log tool call
176          await this.sessionManager.appendEntry(this.jid, {
177            timestamp: new Date().toISOString(),
178            type: "tool_call",
179            content: JSON.stringify({
180              name: toolCall.name,
181              arguments: toolCall.arguments,
182            }),
183            toolName: toolCall.name,
184            toolCallId: toolCall.id,
185          });
186
187          try {
188            const result = await tool.execute(toolCall.arguments);
189
190            // Log tool result
191            await this.sessionManager.appendEntry(this.jid, {
192              timestamp: new Date().toISOString(),
193              type: "tool_result",
194              content: result,
195              toolName: toolCall.name,
196              toolCallId: toolCall.id,
197            });
198
199            messages.push({
200              role: "user",
201              content: `Tool result for ${toolCall.name}:\n${result}`,
202            });
203          } catch (err) {
204            const errorMsg = `Tool error: ${(err as Error).message}`;
205            messages.push({ role: "user", content: errorMsg });
206          }
207        }
208
209        continue;
210      }
211
212      // No more tool calls, return final response
213      finalResponse = response.content || "";
214      break;
215    }
216
217    if (iterations >= MAX_TOOL_ITERATIONS) {
218      finalResponse += "\n\n(Reached maximum tool iterations)";
219    }
220
221    return finalResponse;
222  }
223}