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:
admin
2026-05-05 16:59:59 +00:00
Unverified
parent e92e9f5b9d
commit 092fefbc52
3 changed files with 176 additions and 10 deletions

134
src/tools/DelegateTool.js Normal file
View 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}`;
}
}
}

View File

@@ -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,
};