feat: add DelegateTool with multi-turn agentic loop (18 tools total)
- DelegateTool.js: multi-turn sub-agent (max 10 turns), feeds tool results back - Moved TOOL_DEFS to startBot scope so delegate handler can access tool schemas - Fixed scoping: delegate handler resolves model from svc.config instead of chatWithAI local - Wired into tools/index.js, TOOL_DEFS, and toolHandlers
This commit is contained in:
@@ -198,13 +198,8 @@ export async function initBot(config, api, tools, skills, agents) {
|
|||||||
toolMap: new Map((tools || []).map(t => [t.name, t])),
|
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) ──
|
// ── Tool definitions for the AI API (OpenAI function-calling format) ──
|
||||||
|
// Defined at startBot scope so delegate handler can access them
|
||||||
const TOOL_DEFS = {
|
const TOOL_DEFS = {
|
||||||
bash: {
|
bash: {
|
||||||
description: 'Execute a shell command',
|
description: 'Execute a shell command',
|
||||||
@@ -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)' },
|
selector: { type: 'string', description: 'CSS selector for content extraction (optional, auto-detects article/main)' },
|
||||||
}, required: ['url'] },
|
}, 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: {
|
delegate_agent: {
|
||||||
description: 'Delegate to a specialized agent role',
|
description: 'Delegate to a specialized agent role',
|
||||||
parameters: { type: 'object', properties: {
|
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
|
// Register all tools that have a matching class loaded
|
||||||
for (const [name, def] of Object.entries(TOOL_DEFS)) {
|
for (const [name, def] of Object.entries(TOOL_DEFS)) {
|
||||||
if (name === 'delegate_agent' && !svc.agents.length) continue;
|
if (name === 'delegate_agent' && !svc.agents.length) continue;
|
||||||
if (name === 'run_skill' && !svc.skills.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 } });
|
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.';
|
if (!tool) return '❌ Browser tool unavailable.';
|
||||||
try { return await tool.execute(args); } catch (e) { return `❌ ${e.message}`; }
|
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) => {
|
delegate_agent: async (args) => {
|
||||||
const agent = svc.agents.find(a => a.id === args.agent_id);
|
const agent = svc.agents.find(a => a.id === args.agent_id);
|
||||||
if (!agent) return `❌ Agent not found: ${args.agent_id}`;
|
if (!agent) return `❌ Agent not found: ${args.agent_id}`;
|
||||||
|
|||||||
134
src/tools/DelegateTool.js
Normal file
134
src/tools/DelegateTool.js
Normal file
@@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { ScheduleCronTool } from './ScheduleCronTool.js';
|
|||||||
import { VisionTool } from './VisionTool.js';
|
import { VisionTool } from './VisionTool.js';
|
||||||
import { TTSTool } from './TTSTool.js';
|
import { TTSTool } from './TTSTool.js';
|
||||||
import { BrowserTool } from './BrowserTool.js';
|
import { BrowserTool } from './BrowserTool.js';
|
||||||
|
import { DelegateTool } from './DelegateTool.js';
|
||||||
|
|
||||||
// Tool definitions: env toggle flag, factory function
|
// Tool definitions: env toggle flag, factory function
|
||||||
const TOOL_REGISTRY = [
|
const TOOL_REGISTRY = [
|
||||||
@@ -65,5 +66,5 @@ export {
|
|||||||
FileReadTool, FileWriteTool, GlobTool, GrepTool, WebFetchTool,
|
FileReadTool, FileWriteTool, GlobTool, GrepTool, WebFetchTool,
|
||||||
TaskCreateTool, TaskUpdateTool, TaskListTool,
|
TaskCreateTool, TaskUpdateTool, TaskListTool,
|
||||||
SendMessageTool, ScheduleCronTool,
|
SendMessageTool, ScheduleCronTool,
|
||||||
VisionTool, TTSTool, BrowserTool,
|
VisionTool, TTSTool, BrowserTool, DelegateTool,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user