taskforce-aiagent
Version:
TaskForce is a modular, open-source, production-ready TypeScript agent framework for orchestrating AI agents, LLM-powered autonomous agents, task pipelines, dynamic toolchains, RAG workflows and memory/retrieval systems.
366 lines (348 loc) • 15.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildUserMessage = buildUserMessage;
exports.buildSystemMessage = buildSystemMessage;
exports.toolWorker = toolWorker;
exports.recursiveToolWorker = recursiveToolWorker;
exports.delegateTo = delegateTo;
exports.truncateMessagesToFitModelLimit = truncateMessagesToFitModelLimit;
exports.generateDynamicPlan = generateDynamicPlan;
const chalk_1 = __importDefault(require("chalk"));
const helper_js_1 = require("../helpers/helper.js");
const aiClient_js_1 = require("../llm/aiClient.js");
const agentRegistry_js_1 = require("./agentRegistry.js");
const log_helper_js_1 = require("../helpers/log.helper.js");
const enum_js_1 = require("../configs/enum.js");
const aiConfig_js_1 = require("../configs/aiConfig.js");
const summarize_helper_js_1 = require("../helpers/summarize.helper.js");
const task_js_1 = require("../tasks/task.js");
function interpolate(template, inputs) {
return template.replace(/\{(\w+)\}/g, (_, key) => inputs[key] || `{${key}}`);
}
function buildAgentDirectory(currentAgent) {
const allAgents = agentRegistry_js_1.AgentRegistry.getAll();
return allAgents
.filter((a) => a.name !== currentAgent.name)
.map((a) => `- ${a.name}: ${a.role} — ${a.goal}`)
.join("\n");
}
function buildUserMessage(inputs, agent, taskDescription, inputFromPrevious, retryReason, delegateReason, outputFormat, rePlanReason) {
const format = (outputFormat || enum_js_1.OutputFormat.text).toLowerCase();
const parts = [];
parts.push(`Background:\n${interpolate(agent.backstory, inputs)}`);
parts.push(`Task:\n${interpolate(taskDescription, inputs)}`);
if (inputFromPrevious?.trim()) {
parts.push(`Previous output:\n${inputFromPrevious.trim()}`);
}
if (retryReason?.trim()) {
parts.push(`This task is being re-executed because: ${retryReason.trim()}`);
}
if (delegateReason?.trim()) {
parts.push(`This task was delegated to you because: ${delegateReason.trim()}`);
}
if (rePlanReason?.trim()) {
parts.push(`This task is being re-executed because:\n${rePlanReason.trim()}`);
}
// Output format block
if (format === enum_js_1.OutputFormat.text) {
if (outputFormat) {
parts.push(`Expected output format:\nReturn the result in ${outputFormat} format.`);
}
}
else {
parts.push(`Expected output format:\nReturn ONLY the result in ${outputFormat} format.\nDo NOT include any explanations, comments, or additional text.`);
}
return parts.join("\n\n");
}
// SYSTEM MESSAGE START
function buildIdentityBlock(agent, inputs) {
const interpolatedGoal = interpolate(agent.goal, inputs);
const interpolatedPrompt = agent.systemPrompt
? interpolate(agent.systemPrompt, inputs)
: null;
return (interpolatedPrompt ??
`You are a ${agent.role}.\n\nGoal: ${interpolatedGoal}`);
}
function buildToolInstructions(agent) {
const tools = agent.toolExecutor.getTools();
const toolInfoWithExamples = agent.toolExecutor.buildToolUsageExamples(tools);
return `You should use tools whenever the topic includes current, missing, or external information. When in doubt, prefer tool usage. If the user prompt includes phrases like "latest", "recent", "trending", or mentions specific time frames (e.g. "last 5 years"), always consider using a tool before answering.
Before using any tool, ask yourself:
1. What exactly is my task?
2. Is there any missing information or uncertainty?
3. Would using a tool help me complete the task more accurately?
Available Tools:
${toolInfoWithExamples}
Tool Usage Instructions:
- Use: TOOL(toolName, {"query": "your search query"})
- Tools may be used more than once.
- Do not continue the main task until tool results are returned.
- Tool names are case-sensitive. Use them exactly as listed.
Do NOT:
- Modify the tool name
- Write your own function-like formats (e.g., toolName(...))
- Add explanations or reasoning inside the tool call`;
}
function buildDelegationInstructions(agent) {
return `Delegation Instructions:
You may delegate work to another agent ONLY IF:
- You cannot complete the task properly.
- The previous step was incomplete or unclear.
- Another agent is more suitable.
Use the format:
DELEGATE(agentName, "task to delegate")
Examples:
- Input lacks structure → delegate to someone who can outline better
- Writing is unclear → delegate to an editor
- Content is missing → delegate to someone who can fill the gaps
Before completing:
- Is the input ready, or should it go back?
- Are you fixing something that should have been handled earlier?
If yes, delegate instead of fixing.
Rules:
- Do not continue after delegating.
- Do not summarize previous work.
- Delegation is optional. Use only if necessary.
Available agents for delegation:
${buildAgentDirectory(agent)}`;
}
function buildGuardrails(agent) {
return `Rules you must follow:
${agent.guardrails
? agent.guardrails.map((r, i) => `${i + 1}. ${r}`).join("\n")
: ""}`;
}
function buildTrainingInsights(agent) {
const summary = agent.trained?.final_summary?.trim();
if (!summary || summary.length <= 5 || !summary.includes(" "))
return;
return `Training Insights:\n- ${summary}`;
}
function buildSystemMessage(inputs, agent, isTraining = false) {
const today = new Date().toLocaleDateString("tr-TR", {
year: "numeric",
month: "long",
day: "numeric",
});
const parts = [];
parts.push(buildIdentityBlock(agent, inputs));
if (agent.canUseTools()) {
if (!agent.llmModel.supportsTools) {
parts.push(buildToolInstructions(agent));
}
}
if (agent.allowDelegation &&
!agent.getDelegationDisallowed() &&
!isTraining) {
parts.push(buildDelegationInstructions(agent));
}
if (agent.guardrails && agent.guardrails.length > 0) {
parts.push(buildGuardrails(agent));
}
const trainingBlock = buildTrainingInsights(agent);
if (!isTraining && trainingBlock) {
parts.push(trainingBlock);
}
parts.push(`Date: ${today}`);
return parts.join("\n\n");
}
// SYSTEM MESSAGE END
async function toolWorker(output, agent, taskDescription, outputFormat, context) {
if (agent.toolExecutor && output.includes("TOOL(")) {
const toolCalls = Array.from(output.matchAll(/TOOL\(([^,]+),\s*(\{.*?\})\)/g));
if (!toolCalls.length)
return;
let enrichedOutput = output;
const toolResponses = [];
for (const match of toolCalls) {
const [toolCall, toolId, toolArg] = match;
const toolName = agent.toolExecutor.getToolNameById(toolId);
let parsedArgs;
try {
parsedArgs = JSON.parse(toolArg);
}
catch (e) {
return [
{
role: "assistant",
content: `❌ Failed to parse tool args for ${toolName}: ${e.message}`,
},
];
}
const toolResult = await agent.toolExecutor.executeToolById(toolId, parsedArgs);
const normalized = (0, helper_js_1.normalizeOutput)(toolResult);
toolResponses.push(`Tool Response for ${toolName}("${toolArg}"):\n${normalized}`);
enrichedOutput = enrichedOutput.replace(toolCall, toolResponses[toolResponses.length - 1]);
if (agent.getVerbose()) {
(0, log_helper_js_1.TFLog)(`🔧 [Tool Used] ${toolName}("${toolArg}") by '${agent.name}'`, chalk_1.default.white);
(0, log_helper_js_1.TFLog)(`🧾 Output:\n${toolResult}\n`, chalk_1.default.gray);
}
}
return [
{ role: "system", content: buildSystemMessage(context, agent) },
{
role: "user",
content: `Tool results:\n\n${toolResponses.join("\n\n")}` +
`\n\nNow complete this task:\n\n${taskDescription}` +
(outputFormat ? `\n\nFormat: ${outputFormat}` : ""),
},
];
}
else {
return undefined;
}
}
async function recursiveToolWorker(output, agent, taskDescription, outputFormat, context, depth = 0, maxDepth = 3) {
if (depth > maxDepth)
return;
const toolMessages = await toolWorker(output, agent, taskDescription, outputFormat, context);
if (!toolMessages)
return;
const newOutput = await (0, aiClient_js_1.callAIModel)(agent.name, agent.model || process.env.DEFAULT_MODEL, toolMessages, agent.getVerbose());
if (agent.getVerbose()) {
(0, log_helper_js_1.TFLog)(`📤 [Agent Output] for '${agent.name}' after tool execution of task "${taskDescription}"`, chalk_1.default.blue);
(0, log_helper_js_1.TFLog)(`${newOutput}\n`, chalk_1.default.gray);
}
if (newOutput.includes("TOOL(")) {
return await recursiveToolWorker(newOutput, agent, taskDescription, outputFormat, context, depth + 1, maxDepth);
}
return [{ role: "assistant", content: newOutput }];
}
async function delegateTo(agent, output, inputs, currentDepth = 0, maxDepth = 3) {
if (!agent.canDelegate())
return null;
if (currentDepth >= maxDepth) {
console.warn(`🚫 Delegation depth limit (${maxDepth}) reached for ${agent.name}`);
return null;
}
const match = output.match(/DELEGATE\(([^,]+),\s*"([^"]+)"\)/);
if (!match)
return null;
const [, delegateName, delegateTask] = match;
agent.incrementDelegation();
if (agent["verbose"]) {
(0, log_helper_js_1.TFLog)(`🔁 [Delegation] ${agent.name} → ${delegateName.trim()} | Task: "${delegateTask}"`, chalk_1.default.red);
}
return {
delegateTo: delegateName.trim(),
task: delegateTask,
};
}
async function truncateMessagesToFitModelLimit(modelName, messages) {
const modelConfig = (0, aiConfig_js_1.getLLMRouteByModel)(modelName);
if (!modelConfig)
throw new Error(`Model not found: ${modelName}`);
const maxTokens = modelConfig.model.maxContextTokens || 16000;
const tokenBuffer = 500;
const estimateTokenCount = (msg) => Math.ceil(msg.content.length / 4); // approx
const systemMessage = messages.find((m) => m.role === "system");
const otherMessages = messages.filter((m) => m.role !== "system");
const finalMessages = [];
const includedMessages = [];
const truncatedMessages = [];
let totalTokens = systemMessage ? estimateTokenCount(systemMessage) : 0;
if (systemMessage)
finalMessages.push(systemMessage);
for (let i = otherMessages.length - 1; i >= 0; i--) {
const msg = otherMessages[i];
const tokenCount = estimateTokenCount(msg);
if (totalTokens + tokenCount <= maxTokens - tokenBuffer) {
includedMessages.unshift(msg);
totalTokens += tokenCount;
}
else {
truncatedMessages.unshift(msg);
}
}
// Only summarize if meaningful chunk was excluded
if (truncatedMessages.length > 2) {
const summaryMessage = await (0, summarize_helper_js_1.summarizeOldMessages)(truncatedMessages);
const summaryTokenCount = estimateTokenCount(summaryMessage);
if (totalTokens + summaryTokenCount <= maxTokens - tokenBuffer) {
finalMessages.push(summaryMessage);
totalTokens += summaryTokenCount;
}
else {
finalMessages.push({
role: "system",
content: "📘 Context summary omitted due to token limits. Some older dialogue has been truncated.",
});
}
}
// Add preserved messages from newest to oldest
finalMessages.push(...includedMessages);
return finalMessages;
}
async function generateDynamicPlan({ inputs, agents, model = enum_js_1.SupportedModel.GPT_4O_MINI, verbose = false, }) {
const prompt = `
Given the following input:
${JSON.stringify(inputs)}
Available agents (choose agent field EXACTLY as written, case-sensitive):
${agents.map((a) => `- "${a.name}": ${a.role} — ${a.goal}`).join("\n")}
IMPORTANT RULES:
- For each task, the "agent" field MUST be chosen from the above list of agent names, **exactly as shown** (case-sensitive).
- Do NOT invent, translate, or modify agent names.
- If you are unsure, use the closest matching agent in the list.
- If a task does not fit any agent, pick the most appropriate from the list—never make up a new agent.
- AGENT NAMES ARE CASE-SENSITIVE AND MUST MATCH THE LIST EXACTLY.
Your job:
1. Generate an ordered list of tasks, as before.
2. Analyze the task dependencies and recommend the best execution mode for the pipeline:
- "parallel": all tasks are independent and can be run in parallel.
- "hierarchical": tasks have dependencies (use inputFromTask field to indicate dependency).
- "sequential": single step or strictly linear.
Return only a valid JSON object with two fields:
- "executionMode": "parallel" | "hierarchical" | "sequential"
- "tasks": [ ... task array as described above ... ]
Example:
{
"executionMode": "hierarchical",
"tasks": [
{ "id": "plan", "name": "Content Plan", "description": "Plan content", "agent": "Planner" },
{ "id": "write", "name": "Write Article", "description": "Write article based on the plan", "agent": "Writer", "inputFromTask": "plan" }
]
}
`;
// LLM call
const raw = await (0, aiClient_js_1.callAIModel)("Task Planner", model, [
{ role: "system", content: "You are a workflow planner." },
{ role: "user", content: prompt },
], verbose);
let cleaned = raw;
if (typeof cleaned === "string" && cleaned.trim().startsWith("```json"))
cleaned = cleaned.replace(/```json|```/g, "").trim();
let parsed = {};
try {
parsed = JSON.parse(cleaned);
if (!parsed ||
typeof parsed !== "object" ||
!parsed.tasks ||
!Array.isArray(parsed.tasks) ||
!parsed.executionMode) {
throw new Error("Missing executionMode or tasks");
}
}
catch (err) {
throw new Error(`[AI-Driven] Could not parse dynamic plan! Output:\n${raw}\n\nError: ${err}`);
}
// Build tasks
const tasks = parsed.tasks.map((t) => new task_js_1.Task({
id: t.id,
name: t.name,
description: t.description,
agent: t.agent,
outputFormat: t.outputFormat || "text",
inputFromTask: t.inputFromTask,
}));
// Filter agents actually used in the plan
const usedAgentNames = new Set(parsed.tasks.map((t) => t.agent));
const agentsUsed = agents.filter((a) => usedAgentNames.has(a.name));
return {
tasks,
agents: agentsUsed,
executionMode: parsed.executionMode,
};
}