diff --git a/src/bot/index.js b/src/bot/index.js index 16462d5b..67326f47 100644 --- a/src/bot/index.js +++ b/src/bot/index.js @@ -198,14 +198,9 @@ export async function initBot(config, api, tools, skills, agents) { toolMap: new Map((tools || []).map(t => [t.name, t])), }; - // ── AI chat with function calling ── - async function chatWithAI(messages, opts = {}) { - const model = opts.model || svc.config?.api?.models?.default || 'glm-5.1'; - const tools = []; - const toolMap = svc.toolMap; - - // ── Tool definitions for the AI API (OpenAI function-calling format) ── - const TOOL_DEFS = { + // ── Tool definitions for the AI API (OpenAI function-calling format) ── + // Defined at startBot scope so delegate handler can access them + const TOOL_DEFS = { bash: { description: 'Execute a shell command', parameters: { type: 'object', properties: { @@ -333,6 +328,15 @@ export async function initBot(config, api, tools, skills, agents) { selector: { type: 'string', description: 'CSS selector for content extraction (optional, auto-detects article/main)' }, }, required: ['url'] }, }, + delegate: { + description: 'Spawn a sub-agent to autonomously complete a complex multi-step task. The sub-agent runs in isolation with its own conversation history and has access to tools. It will use tools, reason through problems, and return a final answer. Use for tasks that require multiple tool calls in sequence.', + parameters: { type: 'object', properties: { + goal: { type: 'string', description: 'The task for the sub-agent to accomplish' }, + context: { type: 'string', description: 'Additional context or background information' }, + tools: { type: 'array', items: { type: 'string' }, description: 'Specific tools to enable (optional, defaults to all available tools)' }, + role: { type: 'string', description: 'Role description for the sub-agent (optional)' }, + }, required: ['goal'] }, + }, delegate_agent: { description: 'Delegate to a specialized agent role', parameters: { type: 'object', properties: { @@ -349,11 +353,18 @@ export async function initBot(config, api, tools, skills, agents) { }, }; + // ── AI chat with function calling ── + async function chatWithAI(messages, opts = {}) { + const model = opts.model || svc.config?.api?.models?.default || 'glm-5.1'; + const tools = []; + const toolMap = svc.toolMap; + // Register all tools that have a matching class loaded for (const [name, def] of Object.entries(TOOL_DEFS)) { if (name === 'delegate_agent' && !svc.agents.length) continue; if (name === 'run_skill' && !svc.skills.length) continue; - if (!toolMap.has(name) && name !== 'delegate_agent' && name !== 'run_skill') continue; + // delegate is special — dynamically created, always available + if (!toolMap.has(name) && name !== 'delegate_agent' && name !== 'run_skill' && name !== 'delegate') continue; tools.push({ type: 'function', function: { name, ...def } }); } @@ -615,6 +626,26 @@ export async function initBot(config, api, tools, skills, agents) { if (!tool) return '❌ Browser tool unavailable.'; try { return await tool.execute(args); } catch (e) { return `❌ ${e.message}`; } }, + delegate: async (args) => { + // Dynamically create a DelegateTool with current context + try { + const { DelegateTool } = await import('../tools/DelegateTool.js'); + // Build tool defs from the currently available toolHandlers + const subToolDefs = {}; + for (const [name, handler] of Object.entries(toolHandlers)) { + subToolDefs[name] = TOOL_DEFS[name] || { description: `Tool: ${name}` }; + } + const subAgent = new DelegateTool({ + apiClient: svc.api.client, + model: svc.config?.api?.models?.default || 'glm-5.1', + toolHandlers, // pass all current tool handlers + toolDefs: subToolDefs, // pass tool definitions + }); + return await subAgent.execute(args); + } catch (e) { + return `❌ Delegate error: ${e.message}`; + } + }, delegate_agent: async (args) => { const agent = svc.agents.find(a => a.id === args.agent_id); if (!agent) return `❌ Agent not found: ${args.agent_id}`; diff --git a/src/tools/DelegateTool.js b/src/tools/DelegateTool.js new file mode 100644 index 00000000..0e0aa59d --- /dev/null +++ b/src/tools/DelegateTool.js @@ -0,0 +1,134 @@ +import { logger } from '../utils/logger.js'; + +const MAX_TURNS = 10; +const MAX_TOKENS_PER_TURN = 4096; + +/** + * DelegateTool — spawns a sub-agent that autonomously executes a task + * using a multi-turn tool-call loop. The sub-agent has its own message + * history (isolated from the parent conversation) and access to a + * configurable subset of tools. + */ +export class DelegateTool { + constructor(config = {}) { + this.name = 'delegate'; + this.description = 'Spawn a sub-agent to autonomously complete a task. The sub-agent runs in an isolated context with its own conversation history and tool access. It will use tools as needed, reason through problems, and return a final answer. Use this for complex multi-step tasks that require tool use.'; + this.apiClient = config.apiClient || null; + this.model = config.model || 'glm-4-flash'; + this.toolHandlers = config.toolHandlers || {}; + this.toolDefs = config.toolDefs || {}; + } + + /** + * Execute a delegated task with multi-turn tool calling. + * @param {Object} args + * @param {string} args.goal - The task to accomplish + * @param {string} [args.context] - Additional context/background + * @param {string[]} [args.tools] - Tool names to enable (default: all) + * @param {string} [args.role] - Role description for the sub-agent + */ + async execute({ goal, context, tools: toolNames, role }) { + if (!goal) return '❌ goal is required.'; + + const rolePrompt = role || 'You are a helpful AI assistant. Complete the assigned task autonomously using the tools available to you. Think step by step. When you have enough information, provide a clear final answer.'; + const contextBlock = context ? `\n\n# Context\n${context}` : ''; + const systemMessage = `${rolePrompt}\n\n# Available Tools\n${Object.values(this.toolDefs).map(d => `- ${d.name || d.description?.substring(0, 60)}`).join('\n')}\n\n# Rules\n- Use tools to gather information before answering\n- If a tool fails, try an alternative approach\n- Keep your reasoning concise\n- When done, provide a clear final answer without calling more tools${contextBlock}`; + + const messages = [ + { role: 'system', content: systemMessage }, + { role: 'user', content: goal }, + ]; + + // Filter tools if specific subset requested + const enabledTools = toolNames + ? Object.entries(this.toolDefs).filter(([name]) => toolNames.includes(name)) + : Object.entries(this.toolDefs); + + const toolSchema = enabledTools.map(([name, def]) => ({ + type: 'function', + function: { name, ...def }, + })); + + logger.info(`🚀 Delegate spawned: "${goal.substring(0, 80)}..." with ${toolSchema.length} tools`); + + try { + let turn = 0; + while (turn < MAX_TURNS) { + turn++; + logger.info(`🔄 Delegate turn ${turn}/${MAX_TURNS}`); + + const body = { + model: this.model, + messages, + temperature: 0.3, + max_tokens: MAX_TOKENS_PER_TURN, + }; + if (toolSchema.length) body.tools = toolSchema; + + const response = await this.apiClient.post('/chat/completions', body); + const choice = response.data.choices?.[0]; + if (!choice) return '❌ Sub-agent: no response from model.'; + + const msg = choice.message; + + // If no tool calls — this is the final answer + if (!msg.tool_calls?.length) { + const answer = msg.content || '✅ Task completed.'; + logger.info(`✅ Delegate finished in ${turn} turns`); + return `🤖 *Sub-agent result* (${turn} turn${turn > 1 ? 's' : ''}):\n\n${answer}`; + } + + // Process tool calls + // Add the assistant message (with tool_calls) to history + messages.push(msg); + + for (const tc of msg.tool_calls) { + const fn = tc.function; + const handler = this.toolHandlers[fn.name]; + let result; + + if (!handler) { + result = `❌ Unknown tool: ${fn.name}`; + } else { + try { + const args = JSON.parse(fn.arguments); + logger.info(`🔧 Delegate tool call: ${fn.name}(${JSON.stringify(args).substring(0, 100)})`); + result = await handler(args); + // Truncate large results to avoid context overflow + if (typeof result === 'string' && result.length > 4000) { + result = result.substring(0, 4000) + '\n... [truncated]'; + } + } catch (e) { + result = `❌ Tool error: ${e.message}`; + } + } + + // Add tool result to message history + messages.push({ + role: 'tool', + tool_call_id: tc.id, + content: typeof result === 'string' ? result : JSON.stringify(result), + }); + } + } + + // Max turns reached — ask for summary + logger.warn(`⚠ Delegate max turns (${MAX_TURNS}) reached`); + messages.push({ role: 'user', content: 'You have reached the maximum number of steps. Please provide your final answer now based on what you have gathered so far.' }); + + const summary = await this.apiClient.post('/chat/completions', { + model: this.model, + messages, + temperature: 0.3, + max_tokens: MAX_TOKENS_PER_TURN, + }); + + const finalAnswer = summary.data.choices?.[0]?.message?.content || '⚠ Max turns reached without final answer.'; + return `🤖 *Sub-agent result* (${MAX_TURNS} turns, max reached):\n\n${finalAnswer}`; + + } catch (error) { + logger.error(`Delegate error: ${error.message}`); + return `❌ Sub-agent error: ${error.message}`; + } + } +} diff --git a/src/tools/index.js b/src/tools/index.js index cfea8a8a..6318983a 100644 --- a/src/tools/index.js +++ b/src/tools/index.js @@ -16,6 +16,7 @@ import { ScheduleCronTool } from './ScheduleCronTool.js'; import { VisionTool } from './VisionTool.js'; import { TTSTool } from './TTSTool.js'; import { BrowserTool } from './BrowserTool.js'; +import { DelegateTool } from './DelegateTool.js'; // Tool definitions: env toggle flag, factory function const TOOL_REGISTRY = [ @@ -65,5 +66,5 @@ export { FileReadTool, FileWriteTool, GlobTool, GrepTool, WebFetchTool, TaskCreateTool, TaskUpdateTool, TaskListTool, SendMessageTool, ScheduleCronTool, - VisionTool, TTSTool, BrowserTool, + VisionTool, TTSTool, BrowserTool, DelegateTool, };