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}