feat: deploy existing agents as subagents via Task tool (#591)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-18 19:12:23 -08:00
committed by GitHub
parent 7905260fc9
commit f30dbf40da
6 changed files with 295 additions and 80 deletions

View File

@@ -25,10 +25,15 @@ interface TaskArgs {
prompt?: string;
description?: string;
model?: string;
agent_id?: string; // Deploy an existing agent instead of creating new
conversation_id?: string; // Resume from an existing conversation
toolCallId?: string; // Injected by executeTool for linking subagent to parent tool call
signal?: AbortSignal; // Injected by executeTool for interruption handling
}
// Valid subagent_types when deploying an existing agent
const VALID_DEPLOY_TYPES = new Set(["explore", "general-purpose"]);
/**
* Task tool - Launch a specialized subagent to handle complex tasks
*/
@@ -61,18 +66,31 @@ export async function task(args: TaskArgs): Promise<string> {
return `Refreshed subagents list: found ${totalCount} total (${customCount} custom)${errorSuffix}`;
}
// For run command, validate required parameters
validateRequiredParams(
args,
["subagent_type", "prompt", "description"],
"Task",
);
// Determine if deploying an existing agent
const isDeployingExisting = Boolean(args.agent_id || args.conversation_id);
// Extract validated params (guaranteed to exist after validateRequiredParams)
const subagent_type = args.subagent_type as string;
// Validate required parameters based on mode
if (isDeployingExisting) {
// Deploying existing agent: prompt and description required, subagent_type optional
validateRequiredParams(args, ["prompt", "description"], "Task");
} else {
// Creating new agent: subagent_type, prompt, and description required
validateRequiredParams(
args,
["subagent_type", "prompt", "description"],
"Task",
);
}
// Extract validated params
const prompt = args.prompt as string;
const description = args.description as string;
// For existing agents, default subagent_type to "general-purpose" for permissions
const subagent_type = isDeployingExisting
? args.subagent_type || "general-purpose"
: (args.subagent_type as string);
// Get all available subagent configs (built-in + custom)
const allConfigs = await getAllSubagentConfigs();
@@ -82,6 +100,11 @@ export async function task(args: TaskArgs): Promise<string> {
return `Error: Invalid subagent type "${subagent_type}". Available types: ${available}`;
}
// For existing agents, only allow explore or general-purpose
if (isDeployingExisting && !VALID_DEPLOY_TYPES.has(subagent_type)) {
return `Error: When deploying an existing agent, subagent_type must be "explore" (read-only) or "general-purpose" (read-write). Got: "${subagent_type}"`;
}
// Register subagent with state store for UI display
const subagentId = generateSubagentId();
registerSubagent(subagentId, subagent_type, description, toolCallId);
@@ -93,6 +116,8 @@ export async function task(args: TaskArgs): Promise<string> {
model,
subagentId,
signal,
args.agent_id,
args.conversation_id,
);
// Mark subagent as completed in state store
@@ -111,6 +136,9 @@ export async function task(args: TaskArgs): Promise<string> {
const header = [
`subagent_type=${subagent_type}`,
result.agentId ? `agent_id=${result.agentId}` : undefined,
result.conversationId
? `conversation_id=${result.conversationId}`
: undefined,
]
.filter(Boolean)
.join(" ");