feat: deploy existing agents as subagents via Task tool (#591)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import { getAllSubagentConfigs, type SubagentConfig } from ".";
|
||||
*/
|
||||
export interface SubagentResult {
|
||||
agentId: string;
|
||||
conversationId?: string;
|
||||
report: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
@@ -46,6 +47,7 @@ export interface SubagentResult {
|
||||
*/
|
||||
interface ExecutionState {
|
||||
agentId: string | null;
|
||||
conversationId: string | null;
|
||||
finalResult: string | null;
|
||||
finalError: string | null;
|
||||
resultStats: { durationMs: number; totalTokens: number } | null;
|
||||
@@ -106,7 +108,7 @@ function recordToolCall(
|
||||
* Handle an init event from the subagent stream
|
||||
*/
|
||||
function handleInitEvent(
|
||||
event: { agent_id?: string },
|
||||
event: { agent_id?: string; conversation_id?: string },
|
||||
state: ExecutionState,
|
||||
baseURL: string,
|
||||
subagentId: string,
|
||||
@@ -116,6 +118,9 @@ function handleInitEvent(
|
||||
const agentURL = `${baseURL}/agents/${event.agent_id}`;
|
||||
updateSubagent(subagentId, { agentURL });
|
||||
}
|
||||
if (event.conversation_id) {
|
||||
state.conversationId = event.conversation_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,21 +311,40 @@ function parseResultFromStdout(
|
||||
function buildSubagentArgs(
|
||||
type: string,
|
||||
config: SubagentConfig,
|
||||
model: string,
|
||||
model: string | null,
|
||||
userPrompt: string,
|
||||
existingAgentId?: string,
|
||||
existingConversationId?: string,
|
||||
preloadedSkillsContent?: string,
|
||||
): string[] {
|
||||
const args: string[] = [
|
||||
"--new-agent",
|
||||
"--system",
|
||||
type,
|
||||
"--model",
|
||||
model,
|
||||
"-p",
|
||||
userPrompt,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
];
|
||||
const args: string[] = [];
|
||||
const isDeployingExisting = Boolean(
|
||||
existingAgentId || existingConversationId,
|
||||
);
|
||||
|
||||
if (isDeployingExisting) {
|
||||
// Deploy existing agent/conversation
|
||||
if (existingConversationId) {
|
||||
// conversation_id is sufficient (headless derives agent from it)
|
||||
args.push("--conv", existingConversationId);
|
||||
} else if (existingAgentId) {
|
||||
// agent_id only - headless creates new conversation
|
||||
args.push("--agent", existingAgentId);
|
||||
}
|
||||
// Don't pass --system (existing agent keeps its prompt)
|
||||
// Don't pass --model (existing agent keeps its model)
|
||||
// Skip skills block operations (existing agent may not have standard blocks)
|
||||
args.push("--no-skills");
|
||||
} else {
|
||||
// Create new agent (original behavior)
|
||||
args.push("--new-agent", "--system", type);
|
||||
if (model) {
|
||||
args.push("--model", model);
|
||||
}
|
||||
}
|
||||
|
||||
args.push("-p", userPrompt);
|
||||
args.push("--output-format", "stream-json");
|
||||
|
||||
// Use subagent's configured permission mode, or inherit from parent
|
||||
const subagentMode = config.permissionMode;
|
||||
@@ -330,31 +354,40 @@ function buildSubagentArgs(
|
||||
args.push("--permission-mode", modeToUse);
|
||||
}
|
||||
|
||||
// Inherit permission rules from parent (CLI + session rules)
|
||||
// Build list of auto-approved tools:
|
||||
// 1. Inherit from parent (CLI + session rules)
|
||||
// 2. Add subagent's allowed tools (so they don't hang on approvals)
|
||||
const parentAllowedTools = cliPermissions.getAllowedTools();
|
||||
const sessionAllowRules = sessionPermissions.getRules().allow || [];
|
||||
const subagentTools =
|
||||
config.allowedTools !== "all" && Array.isArray(config.allowedTools)
|
||||
? config.allowedTools
|
||||
: [];
|
||||
const combinedAllowedTools = [
|
||||
...new Set([...parentAllowedTools, ...sessionAllowRules]),
|
||||
...new Set([...parentAllowedTools, ...sessionAllowRules, ...subagentTools]),
|
||||
];
|
||||
if (combinedAllowedTools.length > 0) {
|
||||
args.push("--allowedTools", combinedAllowedTools.join(","));
|
||||
}
|
||||
|
||||
const parentDisallowedTools = cliPermissions.getDisallowedTools();
|
||||
if (parentDisallowedTools.length > 0) {
|
||||
args.push("--disallowedTools", parentDisallowedTools.join(","));
|
||||
}
|
||||
|
||||
// Add memory block filtering if specified
|
||||
if (config.memoryBlocks === "none") {
|
||||
args.push("--init-blocks", "none");
|
||||
} else if (
|
||||
Array.isArray(config.memoryBlocks) &&
|
||||
config.memoryBlocks.length > 0
|
||||
) {
|
||||
args.push("--init-blocks", config.memoryBlocks.join(","));
|
||||
// Add memory block filtering if specified (only for new agents)
|
||||
if (!isDeployingExisting) {
|
||||
if (config.memoryBlocks === "none") {
|
||||
args.push("--init-blocks", "none");
|
||||
} else if (
|
||||
Array.isArray(config.memoryBlocks) &&
|
||||
config.memoryBlocks.length > 0
|
||||
) {
|
||||
args.push("--init-blocks", config.memoryBlocks.join(","));
|
||||
}
|
||||
}
|
||||
|
||||
// Add tool filtering if specified
|
||||
// Add tool filtering if specified (applies to both new and existing agents)
|
||||
if (
|
||||
config.allowedTools !== "all" &&
|
||||
Array.isArray(config.allowedTools) &&
|
||||
@@ -363,8 +396,8 @@ function buildSubagentArgs(
|
||||
args.push("--tools", config.allowedTools.join(","));
|
||||
}
|
||||
|
||||
// Add pre-loaded skills content if provided
|
||||
if (preloadedSkillsContent) {
|
||||
// Add pre-loaded skills content if provided (only for new agents)
|
||||
if (!isDeployingExisting && preloadedSkillsContent) {
|
||||
args.push("--block-value", `loaded_skills=${preloadedSkillsContent}`);
|
||||
}
|
||||
|
||||
@@ -377,12 +410,14 @@ function buildSubagentArgs(
|
||||
async function executeSubagent(
|
||||
type: string,
|
||||
config: SubagentConfig,
|
||||
model: string,
|
||||
model: string | null,
|
||||
userPrompt: string,
|
||||
baseURL: string,
|
||||
subagentId: string,
|
||||
isRetry = false,
|
||||
signal?: AbortSignal,
|
||||
existingAgentId?: string,
|
||||
existingConversationId?: string,
|
||||
): Promise<SubagentResult> {
|
||||
// Check if already aborted before starting
|
||||
if (signal?.aborted) {
|
||||
@@ -395,7 +430,9 @@ async function executeSubagent(
|
||||
}
|
||||
|
||||
// Update the state with the model being used (may differ on retry/fallback)
|
||||
updateSubagent(subagentId, { model });
|
||||
if (model) {
|
||||
updateSubagent(subagentId, { model });
|
||||
}
|
||||
|
||||
try {
|
||||
// Pre-load skills if configured
|
||||
@@ -412,12 +449,23 @@ async function executeSubagent(
|
||||
config,
|
||||
model,
|
||||
userPrompt,
|
||||
existingAgentId,
|
||||
existingConversationId,
|
||||
preloadedSkillsContent,
|
||||
);
|
||||
|
||||
// Spawn Letta Code in headless mode.
|
||||
// Some environments may have a different `letta` binary earlier in PATH.
|
||||
const lettaCmd = process.env.LETTA_CODE_BIN || "letta";
|
||||
// Use the same binary as the current process, with fallbacks:
|
||||
// 1. LETTA_CODE_BIN env var (explicit override)
|
||||
// 2. Current process argv[1] if it's a .js file (built letta.js)
|
||||
// 3. ./letta.js if running from dev (src/index.ts)
|
||||
// 4. "letta" (global install)
|
||||
const currentScript = process.argv[1] || "";
|
||||
const lettaCmd =
|
||||
process.env.LETTA_CODE_BIN ||
|
||||
(currentScript.endsWith(".js") ? currentScript : null) ||
|
||||
(currentScript.includes("src/index.ts") ? "./letta.js" : null) ||
|
||||
"letta";
|
||||
// Pass parent agent ID so subagents can access parent's context (e.g., search history)
|
||||
let parentAgentId: string | undefined;
|
||||
try {
|
||||
@@ -450,7 +498,8 @@ async function executeSubagent(
|
||||
|
||||
// Initialize execution state
|
||||
const state: ExecutionState = {
|
||||
agentId: null,
|
||||
agentId: existingAgentId || null,
|
||||
conversationId: existingConversationId || null,
|
||||
finalResult: null,
|
||||
finalError: null,
|
||||
resultStats: null,
|
||||
@@ -486,6 +535,7 @@ async function executeSubagent(
|
||||
if (wasAborted) {
|
||||
return {
|
||||
agentId: state.agentId || "",
|
||||
conversationId: state.conversationId || undefined,
|
||||
report: "",
|
||||
success: false,
|
||||
error: INTERRUPTED_BY_USER,
|
||||
@@ -516,6 +566,7 @@ async function executeSubagent(
|
||||
|
||||
return {
|
||||
agentId: state.agentId || "",
|
||||
conversationId: state.conversationId || undefined,
|
||||
report: "",
|
||||
success: false,
|
||||
error: stderr || `Subagent exited with code ${exitCode}`,
|
||||
@@ -526,6 +577,7 @@ async function executeSubagent(
|
||||
if (state.finalResult !== null) {
|
||||
return {
|
||||
agentId: state.agentId || "",
|
||||
conversationId: state.conversationId || undefined,
|
||||
report: state.finalResult,
|
||||
success: !state.finalError,
|
||||
error: state.finalError || undefined,
|
||||
@@ -537,6 +589,7 @@ async function executeSubagent(
|
||||
if (state.finalError) {
|
||||
return {
|
||||
agentId: state.agentId || "",
|
||||
conversationId: state.conversationId || undefined,
|
||||
report: "",
|
||||
success: false,
|
||||
error: state.finalError,
|
||||
@@ -576,6 +629,28 @@ function getBaseURL(): string {
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a system reminder prefix for deployed agents
|
||||
*/
|
||||
function buildDeploySystemReminder(
|
||||
senderAgentName: string,
|
||||
senderAgentId: string,
|
||||
subagentType: string,
|
||||
): string {
|
||||
const toolDescription =
|
||||
subagentType === "explore"
|
||||
? "read-only tools (Read, Glob, Grep)"
|
||||
: "local tools (Bash, Read, Write, Edit, etc.)";
|
||||
|
||||
return `<system-reminder>
|
||||
This task is from "${senderAgentName}" (agent ID: ${senderAgentId}), which deployed you as a subagent inside the Letta Code CLI (docs.letta.com/letta-code).
|
||||
You have access to ${toolDescription} in their codebase.
|
||||
Your final message will be returned to the caller.
|
||||
</system-reminder>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a subagent and execute it autonomously
|
||||
*
|
||||
@@ -584,6 +659,8 @@ function getBaseURL(): string {
|
||||
* @param userModel - Optional model override from the parent agent
|
||||
* @param subagentId - ID for tracking in the state store (registered by Task tool)
|
||||
* @param signal - Optional abort signal for interruption handling
|
||||
* @param existingAgentId - Optional ID of an existing agent to deploy
|
||||
* @param existingConversationId - Optional conversation ID to resume
|
||||
*/
|
||||
export async function spawnSubagent(
|
||||
type: string,
|
||||
@@ -591,6 +668,8 @@ export async function spawnSubagent(
|
||||
userModel: string | undefined,
|
||||
subagentId: string,
|
||||
signal?: AbortSignal,
|
||||
existingAgentId?: string,
|
||||
existingConversationId?: string,
|
||||
): Promise<SubagentResult> {
|
||||
const allConfigs = await getAllSubagentConfigs();
|
||||
const config = allConfigs[type];
|
||||
@@ -604,19 +683,46 @@ export async function spawnSubagent(
|
||||
};
|
||||
}
|
||||
|
||||
const model = userModel || config.recommendedModel;
|
||||
const isDeployingExisting = Boolean(
|
||||
existingAgentId || existingConversationId,
|
||||
);
|
||||
|
||||
// For existing agents, don't override model; for new agents, use provided or config default
|
||||
const model = isDeployingExisting
|
||||
? null
|
||||
: userModel || config.recommendedModel;
|
||||
const baseURL = getBaseURL();
|
||||
|
||||
// Build the prompt with system reminder for deployed agents
|
||||
let finalPrompt = prompt;
|
||||
if (isDeployingExisting) {
|
||||
try {
|
||||
const parentAgentId = getCurrentAgentId();
|
||||
const client = await getClient();
|
||||
const parentAgent = await client.agents.retrieve(parentAgentId);
|
||||
const systemReminder = buildDeploySystemReminder(
|
||||
parentAgent.name,
|
||||
parentAgentId,
|
||||
type,
|
||||
);
|
||||
finalPrompt = systemReminder + prompt;
|
||||
} catch {
|
||||
// If we can't get parent agent info, proceed without the reminder
|
||||
}
|
||||
}
|
||||
|
||||
// Execute subagent - state updates are handled via the state store
|
||||
const result = await executeSubagent(
|
||||
type,
|
||||
config,
|
||||
model,
|
||||
prompt,
|
||||
finalPrompt,
|
||||
baseURL,
|
||||
subagentId,
|
||||
false,
|
||||
signal,
|
||||
existingAgentId,
|
||||
existingConversationId,
|
||||
);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -104,6 +104,7 @@ export async function handleHeadlessCommand(
|
||||
"init-blocks": { type: "string" },
|
||||
"base-tools": { type: "string" },
|
||||
"from-af": { type: "string" },
|
||||
"no-skills": { type: "boolean" },
|
||||
},
|
||||
strict: false,
|
||||
allowPositionals: true,
|
||||
@@ -554,15 +555,33 @@ export async function handleHeadlessCommand(
|
||||
// Determine which conversation to use
|
||||
let conversationId: string;
|
||||
|
||||
// Only isolate blocks that actually exist on this agent
|
||||
// If initBlocks is undefined, agent has default blocks (all ISOLATED_BLOCK_LABELS exist)
|
||||
// If initBlocks is defined, only isolate blocks that are in both lists
|
||||
const isolatedBlockLabels: string[] =
|
||||
initBlocks === undefined
|
||||
? [...ISOLATED_BLOCK_LABELS]
|
||||
: ISOLATED_BLOCK_LABELS.filter((label) =>
|
||||
initBlocks.includes(label as string),
|
||||
);
|
||||
// Check flags early
|
||||
const noSkillsFlag = values["no-skills"] as boolean | undefined;
|
||||
const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent";
|
||||
|
||||
// Ensure skills blocks exist BEFORE conversation creation (for non-subagent, non-no-skills)
|
||||
// This prevents "block not found" errors when creating conversations with isolated_block_labels
|
||||
// Note: ensureSkillsBlocks already calls blocks.list internally, so no extra API call
|
||||
if (!noSkillsFlag && !isSubagent) {
|
||||
const createdBlocks = await ensureSkillsBlocks(agent.id);
|
||||
if (createdBlocks.length > 0) {
|
||||
console.log("Created missing skills blocks for agent compatibility");
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which blocks to isolate for the conversation
|
||||
let isolatedBlockLabels: string[] = [];
|
||||
if (!noSkillsFlag) {
|
||||
// After ensureSkillsBlocks, we know all standard blocks exist
|
||||
// Use the full list, optionally filtered by initBlocks
|
||||
isolatedBlockLabels =
|
||||
initBlocks === undefined
|
||||
? [...ISOLATED_BLOCK_LABELS]
|
||||
: ISOLATED_BLOCK_LABELS.filter((label) =>
|
||||
initBlocks.includes(label as string),
|
||||
);
|
||||
}
|
||||
// If --no-skills is set, isolatedBlockLabels stays empty (no isolation)
|
||||
|
||||
if (specifiedConversationId) {
|
||||
if (specifiedConversationId === "default") {
|
||||
@@ -627,7 +646,6 @@ export async function handleHeadlessCommand(
|
||||
|
||||
// Save session (agent + conversation) to both project and global settings
|
||||
// Skip for subagents - they shouldn't pollute the LRU settings
|
||||
const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent";
|
||||
if (!isSubagent) {
|
||||
await settingsManager.loadLocalProjectSettings();
|
||||
settingsManager.setLocalLastSession(
|
||||
@@ -640,41 +658,40 @@ export async function handleHeadlessCommand(
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the agent has the required skills blocks (for backwards compatibility)
|
||||
const createdBlocks = await ensureSkillsBlocks(agent.id);
|
||||
if (createdBlocks.length > 0) {
|
||||
console.log("Created missing skills blocks for agent compatibility");
|
||||
}
|
||||
|
||||
// Set agent context for tools that need it (e.g., Skill tool, Task tool)
|
||||
setAgentContext(agent.id, skillsDirectory);
|
||||
|
||||
// Fire-and-forget: Initialize loaded skills flag (LET-7101)
|
||||
// Don't await - this is just for the skill unload reminder
|
||||
initializeLoadedSkillsFlag().catch(() => {
|
||||
// Ignore errors - not critical
|
||||
});
|
||||
// Skills-related fire-and-forget operations (skip for subagents/--no-skills)
|
||||
if (!noSkillsFlag && !isSubagent) {
|
||||
// Fire-and-forget: Initialize loaded skills flag (LET-7101)
|
||||
// Don't await - this is just for the skill unload reminder
|
||||
initializeLoadedSkillsFlag().catch(() => {
|
||||
// Ignore errors - not critical
|
||||
});
|
||||
|
||||
// Fire-and-forget: Sync skills in background (LET-7101)
|
||||
// This ensures new skills added after agent creation are available
|
||||
// Don't await - proceed to message sending immediately
|
||||
(async () => {
|
||||
try {
|
||||
const { syncSkillsToAgent, SKILLS_DIR } = await import("./agent/skills");
|
||||
const { join } = await import("node:path");
|
||||
// Fire-and-forget: Sync skills in background (LET-7101)
|
||||
// This ensures new skills added after agent creation are available
|
||||
// Don't await - proceed to message sending immediately
|
||||
(async () => {
|
||||
try {
|
||||
const { syncSkillsToAgent, SKILLS_DIR } = await import(
|
||||
"./agent/skills"
|
||||
);
|
||||
const { join } = await import("node:path");
|
||||
|
||||
const resolvedSkillsDirectory =
|
||||
skillsDirectory || join(process.cwd(), SKILLS_DIR);
|
||||
const resolvedSkillsDirectory =
|
||||
skillsDirectory || join(process.cwd(), SKILLS_DIR);
|
||||
|
||||
await syncSkillsToAgent(client, agent.id, resolvedSkillsDirectory, {
|
||||
skipIfUnchanged: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[skills] Background sync failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
})();
|
||||
await syncSkillsToAgent(client, agent.id, resolvedSkillsDirectory, {
|
||||
skipIfUnchanged: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[skills] Background sync failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Validate output format
|
||||
const outputFormat =
|
||||
|
||||
@@ -417,6 +417,7 @@ async function main(): Promise<void> {
|
||||
skills: { type: "string" },
|
||||
sleeptime: { type: "boolean" },
|
||||
"from-af": { type: "string" },
|
||||
"no-skills": { type: "boolean" },
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
|
||||
@@ -30,6 +30,16 @@ This skill enables you to send messages to other agents on the same Letta server
|
||||
|
||||
**Important:** This skill is for *communication* with other agents, not *delegation* of local work. The target agent runs in their own environment and cannot interact with your codebase.
|
||||
|
||||
**Need local access?** If you need the target agent to access your local environment (read/write files, run commands), use the Task tool instead to deploy them as a subagent:
|
||||
```typescript
|
||||
Task({
|
||||
agent_id: "agent-xxx", // Deploy this existing agent
|
||||
subagent_type: "explore", // "explore" = read-only, "general-purpose" = read-write
|
||||
prompt: "Look at the code in src/ and tell me about the architecture"
|
||||
})
|
||||
```
|
||||
This gives the agent access to your codebase while running as a subagent.
|
||||
|
||||
## Finding an Agent to Message
|
||||
|
||||
If you don't have a specific agent ID, use these skills to find one:
|
||||
|
||||
@@ -14,6 +14,8 @@ Launch a subagent to perform a task. Parameters:
|
||||
- **prompt**: Detailed, self-contained instructions for the agent (agents cannot ask questions mid-execution)
|
||||
- **description**: Short 3-5 word summary for tracking
|
||||
- **model** (optional): Override the model for this agent
|
||||
- **agent_id** (optional): Deploy an existing agent instead of creating a new one
|
||||
- **conversation_id** (optional): Resume from an existing conversation
|
||||
|
||||
### Refresh
|
||||
Re-scan the `.letta/agents/` directories to discover new or updated custom subagents:
|
||||
@@ -45,6 +47,57 @@ Use this after creating or modifying custom subagent definitions.
|
||||
- **Parallel execution**: Launch multiple agents concurrently by calling Task multiple times in a single response
|
||||
- **Specify return format**: Tell agents exactly what information to include in their report
|
||||
|
||||
## Deploying an Existing Agent
|
||||
|
||||
Instead of spawning a fresh subagent from a template, you can deploy an existing agent to work in your local codebase.
|
||||
|
||||
### Access Levels (subagent_type)
|
||||
Use subagent_type to control what tools the deployed agent can access:
|
||||
- **explore**: Read-only access (Read, Glob, Grep) - safer for exploration tasks
|
||||
- **general-purpose**: Full read-write access (Bash, Edit, Write, etc.) - for implementation tasks
|
||||
|
||||
If subagent_type is not specified when deploying an existing agent, it defaults to "general-purpose".
|
||||
|
||||
### Parameters
|
||||
|
||||
- **agent_id**: The ID of an existing agent to deploy (e.g., "agent-abc123")
|
||||
- Starts a new conversation with that agent
|
||||
- The agent keeps its own system prompt and memory
|
||||
- Tool access is controlled by subagent_type
|
||||
|
||||
- **conversation_id**: Resume from an existing conversation (e.g., "conv-xyz789")
|
||||
- Does NOT require agent_id (conversation IDs are unique and encode the agent)
|
||||
- Continues from the conversation's existing message history
|
||||
- Use this to continue context from:
|
||||
- A prior Task tool invocation that returned a conversation_id
|
||||
- A message thread started via the messaging-agents skill
|
||||
|
||||
### Examples
|
||||
|
||||
```typescript
|
||||
// Deploy agent with read-only access
|
||||
Task({
|
||||
agent_id: "agent-abc123",
|
||||
subagent_type: "explore",
|
||||
description: "Find auth code",
|
||||
prompt: "Find all auth-related code in this codebase"
|
||||
})
|
||||
|
||||
// Deploy agent with full access (default)
|
||||
Task({
|
||||
agent_id: "agent-abc123",
|
||||
description: "Fix auth bug",
|
||||
prompt: "Fix the bug in auth.ts"
|
||||
})
|
||||
|
||||
// Continue an existing conversation
|
||||
Task({
|
||||
conversation_id: "conv-xyz789",
|
||||
description: "Continue implementation",
|
||||
prompt: "Now implement the fix we discussed"
|
||||
})
|
||||
```
|
||||
|
||||
## Examples:
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -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(" ");
|
||||
|
||||
Reference in New Issue
Block a user