import { feature } from 'bun:bundle';
import * as React from 'react';
import { buildTool, type ToolDef, toolMatchesName } from 'src/Tool.js';
import type { AssistantMessage, Message as MessageType, NormalizedUserMessage } from 'src/types/message.js';
import { getQuerySourceForAgent } from 'src/utils/promptCategory.js';
import { z } from 'zod/v4';
import { clearInvokedSkillsForAgent, getSdkAgentProgressSummariesEnabled } from 'src/bootstrap/state.js';
import { enhanceSystemPromptWithEnvDetails, getSystemPrompt } from 'src/constants/prompts.js';
import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js';
import { startAgentSummarization } from 'src/services/AgentSummary/agentSummary.js';
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
import {
  type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  logEvent,
} from 'src/services/analytics/index.js';
import { clearDumpState } from 'src/services/api/dumpPrompts.js';
import {
  completeAgentTask as completeAsyncAgent,
  createActivityDescriptionResolver,
  createProgressTracker,
  enqueueAgentNotification,
  failAgentTask as failAsyncAgent,
  getProgressUpdate,
  getTokenCountFromTracker,
  isLocalAgentTask,
  killAsyncAgent,
  registerAgentForeground,
  registerAsyncAgent,
  unregisterAgentForeground,
  updateAgentProgress as updateAsyncAgentProgress,
  updateProgressFromMessage,
} from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
import {
  checkRemoteAgentEligibility,
  formatPreconditionError,
  getRemoteTaskSessionUrl,
  registerRemoteAgentTask,
  type BackgroundRemoteSessionPrecondition,
} from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js';
import { assembleToolPool } from 'src/tools.js';
import { filterParentToolsForFork } from 'src/utils/agentToolFilter.js';
import { asAgentId } from 'src/types/ids.js';
import { runWithAgentContext, type SubagentContext } from 'src/utils/agentContext.js';
import { isAgentSwarmsEnabled } from 'src/utils/agentSwarmsEnabled.js';
import { getCwd, runWithCwdOverride } from 'src/utils/cwd.js';
import { logForDebugging } from 'src/utils/debug.js';
import { isEnvTruthy } from 'src/utils/envUtils.js';
import { AbortError, errorMessage, toError } from 'src/utils/errors.js';
import type { CacheSafeParams } from 'src/utils/forkedAgent.js';
import { lazySchema } from 'src/utils/lazySchema.js';
import { createUserMessage, extractTextContent, isSyntheticMessage, normalizeMessages } from 'src/utils/messages.js';
import { getAgentModel } from 'src/utils/model/agent.js';
import { permissionModeSchema } from 'src/utils/permissions/PermissionMode.js';
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js';
import { filterDeniedAgents, getDenyRuleForAgent } from 'src/utils/permissions/permissions.js';
import { enqueueSdkEvent } from 'src/utils/sdkEventQueue.js';
import { writeAgentMetadata } from 'src/utils/sessionStorage.js';
import { sleep } from 'src/utils/sleep.js';
import { buildEffectiveSystemPrompt } from 'src/utils/systemPrompt.js';
import { asSystemPrompt } from 'src/utils/systemPromptType.js';
import { getTaskOutputPath } from 'src/utils/task/diskOutput.js';
import { getParentSessionId, isTeammate } from 'src/utils/teammate.js';
import { isInProcessTeammate } from 'src/utils/teammateContext.js';
import { teleportToRemote } from 'src/utils/teleport.js';
import { getAssistantMessageContentLength } from 'src/utils/tokens.js';
import { createAgentId } from 'src/utils/uuid.js';
import { createAgentWorktree, hasWorktreeChanges, removeAgentWorktree } from 'src/utils/worktree.js';
import { BASH_TOOL_NAME } from '../BashTool/toolName.js';
import { BackgroundHint } from '../BashTool/UI.js';
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js';
import { spawnTeammate } from '../shared/spawnMultiAgent.js';
import { setAgentColor } from './agentColorManager.js';
import {
  agentToolResultSchema,
  classifyHandoffIfNeeded,
  emitTaskProgress,
  extractPartialResult,
  finalizeAgentTool,
  getLastToolUseName,
  runAsyncAgentLifecycle,
} from './agentToolUtils.js';
import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js';
import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME, ONE_SHOT_BUILTIN_AGENT_TYPES } from './constants.js';
import {
  buildForkedMessages,
  buildWorktreeNotice,
  FORK_AGENT,
  isForkSubagentEnabled,
  isInForkChild,
} from './forkSubagent.js';
import type { AgentDefinition } from './loadAgentsDir.js';
import { filterAgentsByMcpRequirements, hasRequiredMcpServers, isBuiltInAgent } from './loadAgentsDir.js';
import { getPrompt } from './prompt.js';
import { runAgent } from './runAgent.js';
import {
  renderGroupedAgentToolUse,
  renderToolResultMessage,
  renderToolUseErrorMessage,
  renderToolUseMessage,
  renderToolUseProgressMessage,
  renderToolUseRejectedMessage,
  renderToolUseTag,
  userFacingName,
  userFacingNameBackgroundColor,
} from './UI.js';

/* eslint-disable @typescript-eslint/no-require-imports */
const proactiveModule =
  feature('PROACTIVE') || feature('KAIROS')
    ? (require('src/proactive/index.js') as typeof import('src/proactive/index.js'))
    : null;
/* eslint-enable @typescript-eslint/no-require-imports */

// Progress display constants (for showing background hint)
const PROGRESS_THRESHOLD_MS = 2000; // Show background hint after 2 seconds

// Check if background tasks are disabled at module load time
const isBackgroundTasksDisabled =
  // eslint-disable-next-line custom-rules/no-process-env-top-level -- Intentional: schema must be defined at module load
  isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS);

// Auto-background agent tasks after this many ms (0 = disabled)
// Enabled by env var OR GrowthBook gate (checked lazily since GB may not be ready at module load)
function getAutoBackgroundMs(): number {
  if (
    isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS) ||
    getFeatureValue_CACHED_MAY_BE_STALE('tengu_auto_background_agents', false)
  ) {
    return 120_000;
  }
  return 0;
}

// Multi-agent type constants are defined inline inside gated blocks to enable dead code elimination

// Base input schema without multi-agent parameters
const baseInputSchema = lazySchema(() =>
  z.object({
    description: z.string().describe('A short (3-5 word) description of the task'),
    prompt: z.string().describe('The task for the agent to perform'),
    subagent_type: z.string().optional().describe('The type of specialized agent to use for this task'),
    model: z
      .enum(['sonnet', 'opus', 'haiku'])
      .optional()
      .describe(
        "Optional model override for this agent. Takes precedence over the agent definition's model frontmatter. If omitted, uses the agent definition's model, or inherits from the parent.",
      ),
    run_in_background: z
      .boolean()
      .optional()
      .describe('Set to true to run this agent in the background. You will be notified when it completes.'),
  }),
);

// Full schema combining base + multi-agent params + isolation
const fullInputSchema = lazySchema(() => {
  // Multi-agent parameters
  const multiAgentInputSchema = z.object({
    name: z
      .string()
      .optional()
      .describe('Name for the spawned agent. Makes it addressable via SendMessage({to: name}) while running.'),
    team_name: z.string().optional().describe('Team name for spawning. Uses current team context if omitted.'),
    mode: permissionModeSchema()
      .optional()
      .describe('Permission mode for spawned teammate (e.g., "plan" to require plan approval).'),
  });

  return baseInputSchema()
    .merge(multiAgentInputSchema)
    .extend({
      isolation: (process.env.USER_TYPE === 'ant' ? z.enum(['worktree', 'remote']) : z.enum(['worktree']))
        .optional()
        .describe(
          process.env.USER_TYPE === 'ant'
            ? 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo. "remote" launches the agent in a remote CCR environment (always runs in background).'
            : 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo.',
        ),
      cwd: z
        .string()
        .optional()
        .describe(
          'Absolute path to run the agent in. Overrides the working directory for all filesystem and shell operations within this agent. Mutually exclusive with isolation: "worktree".',
        ),
    });
});

// Strip optional fields from the schema when the backing feature is off so
// the model never sees them. Done via .omit() rather than conditional spread
// inside .extend() because the spread-ternary breaks Zod's type inference
// (field type collapses to `unknown`). The ternary return produces a union
// type, but call() destructures via the explicit AgentToolInput type below
// which always includes all optional fields.
export const inputSchema = lazySchema(() => {
  const schema = feature('KAIROS') ? fullInputSchema() : fullInputSchema().omit({ cwd: true });

  // GrowthBook-in-lazySchema is acceptable here (unlike subagent_type, which
  // was removed in 906da6c723): the divergence window is one-session-per-
  // gate-flip via _CACHED_MAY_BE_STALE disk read, and worst case is either
  // "schema shows a no-op param" (gate flips on mid-session: param ignored
  // by forceAsync) or "schema hides a param that would've worked" (gate
  // flips off mid-session: everything still runs async via memoized
  // forceAsync). No Zod rejection, no crash — unlike required→optional.
  return isBackgroundTasksDisabled || isForkSubagentEnabled() ? schema.omit({ run_in_background: true }) : schema;
});
type InputSchema = ReturnType<typeof inputSchema>;

// Explicit type widens the schema inference to always include all optional
// fields even when .omit() strips them for gating (cwd, run_in_background).
// subagent_type is optional; call() defaults it to general-purpose when the
// fork gate is off, or routes to the fork path when the gate is on.
type AgentToolInput = z.infer<ReturnType<typeof baseInputSchema>> & {
  name?: string;
  team_name?: string;
  mode?: z.infer<ReturnType<typeof permissionModeSchema>>;
  isolation?: 'worktree' | 'remote';
  cwd?: string;
};

// Output schema - multi-agent spawned schema added dynamically at runtime when enabled
export const outputSchema = lazySchema(() => {
  const syncOutputSchema = agentToolResultSchema().extend({
    status: z.literal('completed'),
    prompt: z.string(),
  });

  const asyncOutputSchema = z.object({
    status: z.literal('async_launched'),
    agentId: z.string().describe('The ID of the async agent'),
    description: z.string().describe('The description of the task'),
    prompt: z.string().describe('The prompt for the agent'),
    outputFile: z.string().describe('Path to the output file for checking agent progress'),
    canReadOutputFile: z
      .boolean()
      .optional()
      .describe('Whether the calling agent has Read/Bash tools to check progress'),
  });

  return z.union([syncOutputSchema, asyncOutputSchema]);
});
type OutputSchema = ReturnType<typeof outputSchema>;
type Output = z.input<OutputSchema>;

// Private type for teammate spawn results - excluded from exported schema for dead code elimination
// The 'teammate_spawned' status string is only included when ENABLE_AGENT_SWARMS is true
type TeammateSpawnedOutput = {
  status: 'teammate_spawned';
  prompt: string;
  teammate_id: string;
  agent_id: string;
  agent_type?: string;
  model?: string;
  name: string;
  color?: string;
  tmux_session_name: string;
  tmux_window_name: string;
  tmux_pane_id: string;
  team_name?: string;
  is_splitpane?: boolean;
  plan_mode_required?: boolean;
};

// Combined output type including both public and internal types
// Note: TeammateSpawnedOutput type is fine - TypeScript types are erased at compile time
// Private type for remote-launched results — excluded from exported schema
// like TeammateSpawnedOutput for dead code elimination purposes. Exported
// for UI.tsx to do proper discriminated-union narrowing instead of ad-hoc casts.
export type RemoteLaunchedOutput = {
  status: 'remote_launched';
  taskId: string;
  sessionUrl: string;
  description: string;
  prompt: string;
  outputFile: string;
};

type InternalOutput = Output | TeammateSpawnedOutput | RemoteLaunchedOutput;

import type { AgentToolProgress, ShellProgress } from 'src/types/tools.js';
// AgentTool forwards both its own progress events and shell progress
// events from the sub-agent so the SDK receives tool_progress updates during bash/powershell runs.
export type Progress = AgentToolProgress | ShellProgress;

export const AgentTool = buildTool({
  async prompt({ agents, tools, getToolPermissionContext, allowedAgentTypes }) {
    const toolPermissionContext = await getToolPermissionContext();

    // Get MCP servers that have tools available
    const mcpServersWithTools: string[] = [];
    for (const tool of tools) {
      if (tool.name?.startsWith('mcp__')) {
        const parts = tool.name.split('__');
        const serverName = parts[1];
        if (serverName && !mcpServersWithTools.includes(serverName)) {
          mcpServersWithTools.push(serverName);
        }
      }
    }

    // Filter agents: first by MCP requirements, then by permission rules
    const agentsWithMcpRequirementsMet = filterAgentsByMcpRequirements(agents, mcpServersWithTools);
    const filteredAgents = filterDeniedAgents(agentsWithMcpRequirementsMet, toolPermissionContext, AGENT_TOOL_NAME);

    // Use inline env check instead of coordinatorModule to avoid circular
    // dependency issues during test module loading.
    const isCoordinator = feature('COORDINATOR_MODE') ? isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) : false;
    return await getPrompt(filteredAgents, isCoordinator, allowedAgentTypes);
  },
  name: AGENT_TOOL_NAME,
  searchHint: 'delegate work to a subagent',
  aliases: [LEGACY_AGENT_TOOL_NAME],
  maxResultSizeChars: 100_000,
  async description() {
    return 'Launch a new agent';
  },
  get inputSchema(): InputSchema {
    return inputSchema();
  },
  get outputSchema(): OutputSchema {
    return outputSchema();
  },
  async call(
    {
      prompt,
      subagent_type,
      description,
      model: modelParam,
      run_in_background,
      name,
      team_name,
      mode: spawnMode,
      isolation,
      cwd,
    }: AgentToolInput,
    toolUseContext,
    canUseTool,
    assistantMessage,
    onProgress?,
  ) {
    const startTime = Date.now();
    const model = isCoordinatorMode() ? undefined : modelParam;

    // Get app state for permission mode and agent filtering
    const appState = toolUseContext.getAppState();
    const permissionMode = appState.toolPermissionContext.mode;
    // In-process teammates get a no-op setAppState; setAppStateForTasks
    // reaches the root store so task registration/progress/kill stay visible.
    const rootSetAppState = toolUseContext.setAppStateForTasks ?? toolUseContext.setAppState;

    // Check if user is trying to use agent teams without access
    if (team_name && !isAgentSwarmsEnabled()) {
      throw new Error('Agent Teams is not yet available on your plan.');
    }

    // Teammates (in-process or tmux) passing `name` would trigger spawnTeammate()
    // below, but TeamFile.members is a flat array with one leadAgentId — nested
    // teammates land in the roster with no provenance and confuse the lead.
    const teamName = resolveTeamName({ team_name }, appState);
    if (isTeammate() && teamName && name) {
      throw new Error(
        'Teammates cannot spawn other teammates — the team roster is flat. To spawn a subagent instead, omit the `name` parameter.',
      );
    }
    // In-process teammates cannot spawn background agents (their lifecycle is
    // tied to the leader's process). Tmux teammates are separate processes and
    // can manage their own background agents.
    if (isInProcessTeammate() && teamName && run_in_background === true) {
      throw new Error(
        'In-process teammates cannot spawn background agents. Use run_in_background=false for synchronous subagents.',
      );
    }

    // Check if this is a multi-agent spawn request
    // Spawn is triggered when team_name is set (from param or context) and name is provided
    if (teamName && name) {
      // Set agent definition color for grouped UI display before spawning
      const agentDef = subagent_type
        ? toolUseContext.options.agentDefinitions.activeAgents.find(a => a.agentType === subagent_type)
        : undefined;
      if (agentDef?.color) {
        setAgentColor(subagent_type!, agentDef.color);
      }
      const result = await spawnTeammate(
        {
          name,
          prompt,
          description,
          team_name: teamName,
          use_splitpane: true,
          plan_mode_required: spawnMode === 'plan',
          model: model ?? agentDef?.model,
          agent_type: subagent_type,
          invokingRequestId: assistantMessage?.requestId as string | undefined,
        },
        toolUseContext,
      );

      // Type assertion uses TeammateSpawnedOutput (defined above) instead of any.
      // This type is excluded from the exported outputSchema for dead code elimination.
      // Cast through unknown because TeammateSpawnedOutput is intentionally
      // not part of the exported Output union (for dead code elimination purposes).
      const spawnResult: TeammateSpawnedOutput = {
        status: 'teammate_spawned' as const,
        prompt,
        ...result.data,
      };
      return { data: spawnResult } as unknown as { data: Output };
    }

    // Fork subagent experiment routing:
    // - subagent_type set: use it (explicit wins)
    // - subagent_type omitted, gate on: fork path (undefined)
    // - subagent_type omitted, gate off: default general-purpose
    const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType);
    const isForkPath = effectiveType === undefined;

    let selectedAgent: AgentDefinition;
    if (isForkPath) {
      // Recursive fork guard: fork children keep the Agent tool in their
      // pool for cache-identical tool defs, so reject fork attempts at call
      // time. Primary check is querySource (compaction-resistant — set on
      // context.options at spawn time, survives autocompact's message
      // rewrite). Message-scan fallback catches any path where querySource
      // wasn't threaded.
      if (
        toolUseContext.options.querySource === `agent:builtin:${FORK_AGENT.agentType}` ||
        isInForkChild(toolUseContext.messages)
      ) {
        throw new Error('Fork is not available inside a forked worker. Complete your task directly using your tools.');
      }
      selectedAgent = FORK_AGENT;
    } else {
      // Filter agents to exclude those denied via Agent(AgentName) syntax
      const allAgents = toolUseContext.options.agentDefinitions.activeAgents;
      const { allowedAgentTypes } = toolUseContext.options.agentDefinitions;
      const agents = filterDeniedAgents(
        // When allowedAgentTypes is set (from Agent(x,y) tool spec), restrict to those types
        allowedAgentTypes ? allAgents.filter(a => allowedAgentTypes.includes(a.agentType)) : allAgents,
        appState.toolPermissionContext,
        AGENT_TOOL_NAME,
      );

      const found = agents.find(agent => agent.agentType === effectiveType);
      if (!found) {
        // Check if the agent exists but is denied by permission rules
        const agentExistsButDenied = allAgents.find(agent => agent.agentType === effectiveType);
        if (agentExistsButDenied) {
          const denyRule = getDenyRuleForAgent(appState.toolPermissionContext, AGENT_TOOL_NAME, effectiveType);
          throw new Error(
            `Agent type '${effectiveType}' has been denied by permission rule '${AGENT_TOOL_NAME}(${effectiveType})' from ${denyRule?.source ?? 'settings'}.`,
          );
        }
        throw new Error(
          `Agent type '${effectiveType}' not found. Available agents: ${agents.map(a => a.agentType).join(', ')}`,
        );
      }
      selectedAgent = found;
    }

    // Same lifecycle constraint as the run_in_background guard above, but for
    // agent definitions that force background via `background: true`. Checked
    // here because selectedAgent is only now resolved.
    if (isInProcessTeammate() && teamName && selectedAgent.background === true) {
      throw new Error(
        `In-process teammates cannot spawn background agents. Agent '${selectedAgent.agentType}' has background: true in its definition.`,
      );
    }

    // Capture for type narrowing — `let selectedAgent` prevents TS from
    // narrowing property types across the if-else assignment above.
    const requiredMcpServers = selectedAgent.requiredMcpServers;

    // Check if required MCP servers have tools available
    // A server that's connected but not authenticated won't have any tools
    if (requiredMcpServers?.length) {
      // If any required servers are still pending (connecting), wait for them
      // before checking tool availability. This avoids a race condition where
      // the agent is invoked before MCP servers finish connecting.
      const hasPendingRequiredServers = appState.mcp.clients.some(
        c =>
          c.type === 'pending' &&
          requiredMcpServers.some(pattern => c.name.toLowerCase().includes(pattern.toLowerCase())),
      );

      let currentAppState = appState;
      if (hasPendingRequiredServers) {
        const MAX_WAIT_MS = 30_000;
        const POLL_INTERVAL_MS = 500;
        const deadline = Date.now() + MAX_WAIT_MS;

        while (Date.now() < deadline) {
          await sleep(POLL_INTERVAL_MS);
          currentAppState = toolUseContext.getAppState();

          // Early exit: if any required server has already failed, no point
          // waiting for other pending servers — the check will fail regardless.
          const hasFailedRequiredServer = currentAppState.mcp.clients.some(
            c =>
              c.type === 'failed' &&
              requiredMcpServers.some(pattern => c.name.toLowerCase().includes(pattern.toLowerCase())),
          );
          if (hasFailedRequiredServer) break;

          const stillPending = currentAppState.mcp.clients.some(
            c =>
              c.type === 'pending' &&
              requiredMcpServers.some(pattern => c.name.toLowerCase().includes(pattern.toLowerCase())),
          );
          if (!stillPending) break;
        }
      }

      // Get servers that actually have tools (meaning they're connected AND authenticated)
      const serversWithTools: string[] = [];
      for (const tool of currentAppState.mcp.tools) {
        if (tool.name?.startsWith('mcp__')) {
          // Extract server name from tool name (format: mcp__serverName__toolName)
          const parts = tool.name.split('__');
          const serverName = parts[1];
          if (serverName && !serversWithTools.includes(serverName)) {
            serversWithTools.push(serverName);
          }
        }
      }

      if (!hasRequiredMcpServers(selectedAgent, serversWithTools)) {
        const missing = requiredMcpServers.filter(
          pattern => !serversWithTools.some(server => server.toLowerCase().includes(pattern.toLowerCase())),
        );
        throw new Error(
          `Agent '${selectedAgent.agentType}' requires MCP servers matching: ${missing.join(', ')}. ` +
            `MCP servers with tools: ${serversWithTools.length > 0 ? serversWithTools.join(', ') : 'none'}. ` +
            `Use /mcp to configure and authenticate the required MCP servers.`,
        );
      }
    }

    // Initialize the color for this agent if it has a predefined one
    if (selectedAgent.color) {
      setAgentColor(selectedAgent.agentType, selectedAgent.color);
    }

    // Resolve agent params for logging (these are already resolved in runAgent)
    const resolvedAgentModel = getAgentModel(
      selectedAgent.model,
      toolUseContext.options.mainLoopModel,
      isForkPath ? undefined : model,
      permissionMode,
    );

    logEvent('tengu_agent_tool_selected', {
      agent_type: selectedAgent.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
      model: resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
      source: selectedAgent.source as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
      color: selectedAgent.color as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
      is_built_in_agent: isBuiltInAgent(selectedAgent),
      is_resume: false,
      is_async: (run_in_background === true || selectedAgent.background === true) && !isBackgroundTasksDisabled,
      is_fork: isForkPath,
    });

    // Resolve effective isolation mode (explicit param overrides agent def)
    const effectiveIsolation = isolation ?? selectedAgent.isolation;

    // Remote isolation: delegate to CCR. Gated ant-only — the guard enables
    // dead code elimination of the entire block for external builds.
    if (process.env.USER_TYPE === 'ant' && effectiveIsolation === 'remote') {
      const eligibility = await checkRemoteAgentEligibility();
      if (!eligibility.eligible) {
        const reasons = (eligibility as { eligible: false; errors: BackgroundRemoteSessionPrecondition[] }).errors
          .map(formatPreconditionError)
          .join('\n');
        throw new Error(`Cannot launch remote agent:\n${reasons}`);
      }

      let bundleFailHint: string | undefined;
      const session = await teleportToRemote({
        initialMessage: prompt,
        description,
        signal: toolUseContext.abortController.signal,
        onBundleFail: msg => {
          bundleFailHint = msg;
        },
      });
      if (!session) {
        throw new Error(bundleFailHint ?? 'Failed to create remote session');
      }

      const { taskId, sessionId } = registerRemoteAgentTask({
        remoteTaskType: 'remote-agent',
        session: { id: session.id, title: session.title || description },
        command: prompt,
        context: toolUseContext,
        toolUseId: toolUseContext.toolUseId,
      });

      logEvent('tengu_agent_tool_remote_launched', {
        agent_type: selectedAgent.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
      });

      const remoteResult: RemoteLaunchedOutput = {
        status: 'remote_launched',
        taskId,
        sessionUrl: getRemoteTaskSessionUrl(sessionId),
        description,
        prompt,
        outputFile: getTaskOutputPath(taskId),
      };
      return { data: remoteResult } as unknown as { data: Output };
    }
    // System prompt + prompt messages: branch on fork path.
    //
    // Fork path: child inherits the PARENT's system prompt (not FORK_AGENT's)
    // for cache-identical API request prefixes. Prompt messages are built via
    // buildForkedMessages() which clones the parent's full assistant message
    // (all tool_use blocks) + placeholder tool_results + per-child directive.
    //
    // Normal path: build the selected agent's own system prompt with env
    // details, and use a simple user message for the prompt.
    let enhancedSystemPrompt: string[] | undefined;
    let forkParentSystemPrompt: ReturnType<typeof buildEffectiveSystemPrompt> | undefined;
    let promptMessages: MessageType[];

    if (isForkPath) {
      if (toolUseContext.renderedSystemPrompt) {
        forkParentSystemPrompt = toolUseContext.renderedSystemPrompt;
      } else {
        // Fallback: recompute. May diverge from parent's cached bytes if
        // GrowthBook state changed between parent turn-start and fork spawn.
        const mainThreadAgentDefinition = appState.agent
          ? appState.agentDefinitions.activeAgents.find(a => a.agentType === appState.agent)
          : undefined;
        const additionalWorkingDirectories = Array.from(
          appState.toolPermissionContext.additionalWorkingDirectories.keys(),
        );
        const defaultSystemPrompt = await getSystemPrompt(
          toolUseContext.options.tools,
          toolUseContext.options.mainLoopModel,
          additionalWorkingDirectories,
          toolUseContext.options.mcpClients,
        );
        forkParentSystemPrompt = buildEffectiveSystemPrompt({
          mainThreadAgentDefinition,
          toolUseContext,
          customSystemPrompt: toolUseContext.options.customSystemPrompt,
          defaultSystemPrompt,
          appendSystemPrompt: toolUseContext.options.appendSystemPrompt,
        });
      }
      promptMessages = buildForkedMessages(prompt, assistantMessage);
    } else {
      try {
        const additionalWorkingDirectories = Array.from(
          appState.toolPermissionContext.additionalWorkingDirectories.keys(),
        );

        // All agents have getSystemPrompt - pass toolUseContext to all
        const agentPrompt = selectedAgent.getSystemPrompt({ toolUseContext });

        // Log agent memory loaded event for subagents
        if (selectedAgent.memory) {
          logEvent('tengu_agent_memory_loaded', {
            ...(process.env.USER_TYPE === 'ant' && {
              agent_type: selectedAgent.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
            }),
            scope: selectedAgent.memory as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
            source: 'subagent' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
          });
        }

        // Apply environment details enhancement
        enhancedSystemPrompt = await enhanceSystemPromptWithEnvDetails(
          [agentPrompt],
          resolvedAgentModel,
          additionalWorkingDirectories,
        );
      } catch (error) {
        logForDebugging(`Failed to get system prompt for agent ${selectedAgent.agentType}: ${errorMessage(error)}`);
      }
      promptMessages = [createUserMessage({ content: prompt })];
    }

    const metadata = {
      prompt,
      resolvedAgentModel,
      isBuiltInAgent: isBuiltInAgent(selectedAgent),
      startTime,
      agentType: selectedAgent.agentType,
      isAsync: (run_in_background === true || selectedAgent.background === true) && !isBackgroundTasksDisabled,
    };

    // Use inline env check instead of coordinatorModule to avoid circular
    // dependency issues during test module loading.
    const isCoordinator = feature('COORDINATOR_MODE') ? isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) : false;

    // Fork subagent experiment: force ALL spawns async for a unified
    // <task-notification> interaction model (not just fork spawns — all of them).
    const forceAsync = isForkSubagentEnabled();

    // Assistant mode: force all agents async. Synchronous subagents hold the
    // main loop's turn open until they complete — the daemon's inputQueue
    // backs up, and the first overdue cron catch-up on spawn becomes N
    // serial subagent turns blocking all user input. Same gate as
    // executeForkedSlashCommand's fire-and-forget path; the
    // <task-notification> re-entry there is handled by the else branch
    // below (registerAsyncAgentTask + notifyOnCompletion).
    const assistantForceAsync = feature('KAIROS') ? appState.kairosEnabled : false;

    const shouldRunAsync =
      (run_in_background === true ||
        selectedAgent.background === true ||
        isCoordinator ||
        forceAsync ||
        assistantForceAsync ||
        (proactiveModule?.isProactiveActive() ?? false)) &&
      !isBackgroundTasksDisabled;
    // Assemble the worker's tool pool independently of the parent's.
    // Workers always get their tools from assembleToolPool with their own
    // permission mode, so they aren't affected by the parent's tool
    // restrictions. This is computed here so that runAgent doesn't need to
    // import from tools.ts (which would create a circular dependency).
    const workerPermissionContext = {
      ...appState.toolPermissionContext,
      mode: selectedAgent.permissionMode ?? 'acceptEdits',
    };
    const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools);

    // Create a stable agent ID early so it can be used for worktree slug
    const earlyAgentId = createAgentId();

    // Set up worktree isolation if requested
    let worktreeInfo: {
      worktreePath: string;
      worktreeBranch?: string;
      headCommit?: string;
      gitRoot?: string;
      hookBased?: boolean;
    } | null = null;

    if (effectiveIsolation === 'worktree') {
      const slug = `agent-${earlyAgentId.slice(0, 8)}`;
      worktreeInfo = await createAgentWorktree(slug);
    }

    // Fork + worktree: inject a notice telling the child to translate paths
    // and re-read potentially stale files. Appended after the fork directive
    // so it appears as the most recent guidance the child sees.
    if (isForkPath && worktreeInfo) {
      promptMessages.push(
        createUserMessage({
          content: buildWorktreeNotice(getCwd(), worktreeInfo.worktreePath),
        }),
      );
    }

    const runAgentParams: Parameters<typeof runAgent>[0] = {
      agentDefinition: selectedAgent,
      promptMessages,
      toolUseContext,
      canUseTool,
      isAsync: shouldRunAsync,
      querySource:
        toolUseContext.options.querySource ??
        getQuerySourceForAgent(selectedAgent.agentType, isBuiltInAgent(selectedAgent)),
      model: isForkPath ? undefined : model,
      // Fork path: pass parent's system prompt AND parent's exact tool
      // array (cache-identical prefix). workerTools is rebuilt under
      // permissionMode 'bubble' which differs from the parent's mode, so
      // its tool-def serialization diverges and breaks cache at the first
      // differing tool. useExactTools also inherits the parent's
      // thinkingConfig and isNonInteractiveSession (see runAgent.ts).
      //
      // Normal path: when a cwd override is in effect (worktree isolation
      // or explicit cwd), skip the pre-built system prompt so runAgent's
      // buildAgentSystemPrompt() runs inside wrapWithCwd where getCwd()
      // returns the override path.
      override: isForkPath
        ? { systemPrompt: forkParentSystemPrompt }
        : enhancedSystemPrompt && !worktreeInfo && !cwd
          ? { systemPrompt: asSystemPrompt(enhancedSystemPrompt) }
          : undefined,
      availableTools: isForkPath ? filterParentToolsForFork(toolUseContext.options.tools) : workerTools,
      // Pass parent conversation when the fork-subagent path needs full
      // context. useExactTools inherits thinkingConfig (runAgent.ts:624).
      forkContextMessages: isForkPath ? toolUseContext.messages : undefined,
      ...(isForkPath && { useExactTools: true }),
      worktreePath: worktreeInfo?.worktreePath,
      description,
    };

    // Helper to wrap execution with a cwd override: explicit cwd arg (KAIROS)
    // takes precedence over worktree isolation path.
    const cwdOverridePath = cwd ?? worktreeInfo?.worktreePath;
    const wrapWithCwd = <T,>(fn: () => T): T => (cwdOverridePath ? runWithCwdOverride(cwdOverridePath, fn) : fn());

    // Helper to clean up worktree after agent completes
    const cleanupWorktreeIfNeeded = async (): Promise<{
      worktreePath?: string;
      worktreeBranch?: string;
    }> => {
      if (!worktreeInfo) return {};
      const { worktreePath, worktreeBranch, headCommit, gitRoot, hookBased } = worktreeInfo;
      // Null out to make idempotent — guards against double-call if code
      // between cleanup and end of try throws into catch
      worktreeInfo = null;
      if (hookBased) {
        // Hook-based worktrees are always kept since we can't detect VCS changes
        logForDebugging(`Hook-based agent worktree kept at: ${worktreePath}`);
        return { worktreePath };
      }
      if (headCommit) {
        const changed = await hasWorktreeChanges(worktreePath, headCommit);
        if (!changed) {
          await removeAgentWorktree(worktreePath, worktreeBranch, gitRoot);
          // Clear worktreePath from metadata so resume doesn't try to use
          // a deleted directory. Fire-and-forget to match runAgent's
          // writeAgentMetadata handling.
          void writeAgentMetadata(asAgentId(earlyAgentId), {
            agentType: selectedAgent.agentType,
            description,
          }).catch(_err => logForDebugging(`Failed to clear worktree metadata: ${_err}`));
          return {};
        }
      }
      logForDebugging(`Agent worktree has changes, keeping: ${worktreePath}`);
      return { worktreePath, worktreeBranch };
    };

    if (shouldRunAsync) {
      const asyncAgentId = earlyAgentId;
      const agentBackgroundTask = registerAsyncAgent({
        agentId: asyncAgentId,
        description,
        prompt,
        selectedAgent,
        setAppState: rootSetAppState,
        // Don't link to parent's abort controller -- background agents should
        // survive when the user presses ESC to cancel the main thread.
        // They are killed explicitly via chat:killAgents.
        toolUseId: toolUseContext.toolUseId,
      });

      // Register name → agentId for SendMessage routing. Post-registerAsyncAgent
      // so we don't leave a stale entry if spawn fails. Sync agents skipped —
      // coordinator is blocked, so SendMessage routing doesn't apply.
      if (name) {
        rootSetAppState(prev => {
          const next = new Map(prev.agentNameRegistry);
          next.set(name, asAgentId(asyncAgentId));
          return { ...prev, agentNameRegistry: next };
        });
      }

      // Wrap async agent execution in agent context for analytics attribution
      const asyncAgentContext: SubagentContext = {
        agentId: asyncAgentId,
        // For subagents from teammates: use team lead's session
        // For subagents from main REPL: undefined (no parent session)
        parentSessionId: getParentSessionId(),
        agentType: 'subagent' as const,
        subagentName: selectedAgent.agentType,
        isBuiltIn: isBuiltInAgent(selectedAgent),
        invokingRequestId: assistantMessage?.requestId as string | undefined,
        invocationKind: 'spawn' as const,
        invocationEmitted: false,
      };

      // Workload propagation: handlePromptSubmit wraps the entire turn in
      // runWithWorkload (AsyncLocalStorage). ALS context is captured at
      // invocation time — when this `void` fires — and survives every await
      // inside. No capture/restore needed; the detached closure sees the
      // parent turn's workload automatically, isolated from its finally.
      void runWithAgentContext(asyncAgentContext, () =>
        wrapWithCwd(() =>
          runAsyncAgentLifecycle({
            taskId: agentBackgroundTask.agentId,
            abortController: agentBackgroundTask.abortController!,
            makeStream: onCacheSafeParams =>
              runAgent({
                ...runAgentParams,
                override: {
                  ...runAgentParams.override,
                  agentId: asAgentId(agentBackgroundTask.agentId),
                  abortController: agentBackgroundTask.abortController!,
                },
                onCacheSafeParams,
              }),
            metadata,
            description,
            toolUseContext,
            rootSetAppState,
            agentIdForCleanup: asyncAgentId,
            enableSummarization: isCoordinator || isForkSubagentEnabled() || getSdkAgentProgressSummariesEnabled(),
            getWorktreeResult: cleanupWorktreeIfNeeded,
          }),
        ),
      );

      const canReadOutputFile = toolUseContext.options.tools.some(
        t => toolMatchesName(t, FILE_READ_TOOL_NAME) || toolMatchesName(t, BASH_TOOL_NAME),
      );
      return {
        data: {
          isAsync: true as const,
          status: 'async_launched' as const,
          agentId: agentBackgroundTask.agentId,
          description: description,
          prompt: prompt,
          outputFile: getTaskOutputPath(agentBackgroundTask.agentId),
          canReadOutputFile,
        },
      };
    } else {
      // Create an explicit agentId for sync agents
      const syncAgentId = asAgentId(earlyAgentId);

      // Set up agent context for sync execution (for analytics attribution)
      const syncAgentContext: SubagentContext = {
        agentId: syncAgentId,
        // For subagents from teammates: use team lead's session
        // For subagents from main REPL: undefined (no parent session)
        parentSessionId: getParentSessionId(),
        agentType: 'subagent' as const,
        subagentName: selectedAgent.agentType,
        isBuiltIn: isBuiltInAgent(selectedAgent),
        invokingRequestId: assistantMessage?.requestId as string | undefined,
        invocationKind: 'spawn' as const,
        invocationEmitted: false,
      };

      // Wrap entire sync agent execution in context for analytics attribution
      // and optionally in a worktree cwd override for filesystem isolation
      return runWithAgentContext(syncAgentContext, () =>
        wrapWithCwd(async () => {
          const agentMessages: MessageType[] = [];
          const agentStartTime = Date.now();
          const syncTracker = createProgressTracker();
          const syncResolveActivity = createActivityDescriptionResolver(toolUseContext.options.tools);

          // Yield initial progress message to carry metadata (prompt)
          if (promptMessages.length > 0) {
            const normalizedPromptMessages = normalizeMessages(promptMessages);
            const normalizedFirstMessage = normalizedPromptMessages.find(
              (m): m is NormalizedUserMessage => m.type === 'user',
            );
            if (normalizedFirstMessage && normalizedFirstMessage.type === 'user' && onProgress) {
              onProgress({
                toolUseID: `agent_${assistantMessage.message.id}`,
                data: {
                  message: normalizedFirstMessage,
                  type: 'agent_progress',
                  prompt,
                  agentId: syncAgentId,
                },
              });
            }
          }

          // Register as foreground task immediately so it can be backgrounded at any time
          // Skip registration if background tasks are disabled
          let foregroundTaskId: string | undefined;
          // Create the background race promise once outside the loop — otherwise
          // each iteration adds a new .then() reaction to the same pending
          // promise, accumulating callbacks for the lifetime of the agent.
          let backgroundPromise: Promise<{ type: 'background' }> | undefined;
          let cancelAutoBackground: (() => void) | undefined;
          if (!isBackgroundTasksDisabled) {
            const registration = registerAgentForeground({
              agentId: syncAgentId,
              description,
              prompt,
              selectedAgent,
              setAppState: rootSetAppState,
              toolUseId: toolUseContext.toolUseId,
              autoBackgroundMs: getAutoBackgroundMs() || undefined,
            });
            foregroundTaskId = registration.taskId;
            backgroundPromise = registration.backgroundSignal.then(() => ({
              type: 'background' as const,
            }));
            cancelAutoBackground = registration.cancelAutoBackground;
          }

          // Track if we've shown the background hint UI
          let backgroundHintShown = false;
          // Track if the agent was backgrounded (cleanup handled by backgrounded finally)
          let wasBackgrounded = false;
          // Per-scope stop function — NOT shared with the backgrounded closure.
          // idempotent: startAgentSummarization's stop() checks `stopped` flag.
          let stopForegroundSummarization: (() => void) | undefined;
          // const capture for sound type narrowing inside the callback below
          const summaryTaskId = foregroundTaskId;

          // Get async iterator for the agent
          const agentIterator = runAgent({
            ...runAgentParams,
            override: {
              ...runAgentParams.override,
              agentId: syncAgentId,
            },
            onCacheSafeParams:
              summaryTaskId && getSdkAgentProgressSummariesEnabled()
                ? (params: CacheSafeParams) => {
                    const { stop } = startAgentSummarization(summaryTaskId, syncAgentId, params, rootSetAppState);
                    stopForegroundSummarization = stop;
                  }
                : undefined,
          })[Symbol.asyncIterator]();

          // Track if an error occurred during iteration
          let syncAgentError: Error | undefined;
          let wasAborted = false;
          let worktreeResult: {
            worktreePath?: string;
            worktreeBranch?: string;
          } = {};

          try {
            while (true) {
              const elapsed = Date.now() - agentStartTime;

              // Show background hint after threshold (but task is already registered)
              // Skip if background tasks are disabled
              if (
                !isBackgroundTasksDisabled &&
                !backgroundHintShown &&
                elapsed >= PROGRESS_THRESHOLD_MS &&
                toolUseContext.setToolJSX
              ) {
                backgroundHintShown = true;
                toolUseContext.setToolJSX({
                  jsx: <BackgroundHint />,
                  shouldHidePromptInput: false,
                  shouldContinueAnimation: true,
                  showSpinner: true,
                });
              }

              // Race between next message and background signal
              // If background tasks are disabled, just await the next message directly
              const nextMessagePromise = agentIterator.next();
              const raceResult = backgroundPromise
                ? await Promise.race([
                    nextMessagePromise.then(r => ({
                      type: 'message' as const,
                      result: r,
                    })),
                    backgroundPromise,
                  ])
                : {
                    type: 'message' as const,
                    result: await nextMessagePromise,
                  };

              // Check if we were backgrounded via backgroundAll()
              // foregroundTaskId is guaranteed to be defined if raceResult.type is 'background'
              // because backgroundPromise is only defined when foregroundTaskId is defined
              if (raceResult.type === 'background' && foregroundTaskId) {
                const appState = toolUseContext.getAppState();
                const task = appState.tasks[foregroundTaskId];
                if (isLocalAgentTask(task) && task.isBackgrounded) {
                  // Capture the taskId for use in the async callback
                  const backgroundedTaskId = foregroundTaskId;
                  wasBackgrounded = true;
                  // Stop foreground summarization; the backgrounded closure
                  // below owns its own independent stop function.
                  stopForegroundSummarization?.();

                  // Workload: inherited via ALS at `void` invocation time,
                  // same as the async-from-start path above.
                  // Continue agent in background and return async result
                  void runWithAgentContext(syncAgentContext, async () => {
                    let stopBackgroundedSummarization: (() => void) | undefined;
                    try {
                      // Clean up the foreground iterator so its finally block runs
                      // (releases MCP connections, session hooks, prompt cache tracking, etc.)
                      // Timeout prevents blocking if MCP server cleanup hangs.
                      // .catch() prevents unhandled rejection if timeout wins the race.
                      await Promise.race([agentIterator.return(undefined).catch(() => {}), sleep(1000)]);
                      // Initialize progress tracking from existing messages
                      const tracker = createProgressTracker();
                      const resolveActivity2 = createActivityDescriptionResolver(toolUseContext.options.tools);
                      for (const existingMsg of agentMessages) {
                        updateProgressFromMessage(tracker, existingMsg, resolveActivity2, toolUseContext.options.tools);
                      }
                      for await (const msg of runAgent({
                        ...runAgentParams,
                        isAsync: true, // Agent is now running in background
                        override: {
                          ...runAgentParams.override,
                          agentId: asAgentId(backgroundedTaskId),
                          abortController: task.abortController,
                        },
                        onCacheSafeParams: getSdkAgentProgressSummariesEnabled()
                          ? (params: CacheSafeParams) => {
                              const { stop } = startAgentSummarization(
                                backgroundedTaskId,
                                asAgentId(backgroundedTaskId),
                                params,
                                rootSetAppState,
                              );
                              stopBackgroundedSummarization = stop;
                            }
                          : undefined,
                      })) {
                        agentMessages.push(msg);

                        // Track progress for backgrounded agents
                        updateProgressFromMessage(tracker, msg, resolveActivity2, toolUseContext.options.tools);
                        updateAsyncAgentProgress(backgroundedTaskId, getProgressUpdate(tracker), rootSetAppState);

                        const lastToolName = getLastToolUseName(msg);
                        if (lastToolName) {
                          emitTaskProgress(
                            tracker,
                            backgroundedTaskId,
                            toolUseContext.toolUseId,
                            description,
                            startTime,
                            lastToolName,
                          );
                        }
                      }
                      const agentResult = finalizeAgentTool(agentMessages, backgroundedTaskId, metadata);

                      // Mark task completed FIRST so TaskOutput(block=true)
                      // unblocks immediately. classifyHandoffIfNeeded and
                      // cleanupWorktreeIfNeeded can hang — they must not gate
                      // the status transition (gh-20236).
                      completeAsyncAgent(agentResult, rootSetAppState);

                      // Extract text from agent result content for the notification
                      let finalMessage = extractTextContent(agentResult.content, '\n');

                      if (feature('TRANSCRIPT_CLASSIFIER')) {
                        const backgroundedAppState = toolUseContext.getAppState();
                        const handoffWarning = await classifyHandoffIfNeeded({
                          agentMessages,
                          tools: toolUseContext.options.tools,
                          toolPermissionContext: backgroundedAppState.toolPermissionContext,
                          abortSignal: task.abortController!.signal,
                          subagentType: selectedAgent.agentType,
                          totalToolUseCount: agentResult.totalToolUseCount,
                        });
                        if (handoffWarning) {
                          finalMessage = `${handoffWarning}\n\n${finalMessage}`;
                        }
                      }

                      // Clean up worktree before notification so we can include it
                      const worktreeResult = await cleanupWorktreeIfNeeded();

                      enqueueAgentNotification({
                        taskId: backgroundedTaskId,
                        description,
                        status: 'completed',
                        setAppState: rootSetAppState,
                        finalMessage,
                        usage: {
                          totalTokens: getTokenCountFromTracker(tracker),
                          toolUses: agentResult.totalToolUseCount,
                          durationMs: agentResult.totalDurationMs,
                        },
                        toolUseId: toolUseContext.toolUseId,
                        ...worktreeResult,
                      });
                    } catch (error) {
                      if (error instanceof AbortError) {
                        // Transition status BEFORE worktree cleanup so
                        // TaskOutput unblocks even if git hangs (gh-20236).
                        killAsyncAgent(backgroundedTaskId, rootSetAppState);
                        logEvent('tengu_agent_tool_terminated', {
                          agent_type: metadata.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
                          model:
                            metadata.resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
                          duration_ms: Date.now() - metadata.startTime,
                          is_async: true,
                          is_built_in_agent: metadata.isBuiltInAgent,
                          reason:
                            'user_cancel_background' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
                        });
                        const worktreeResult = await cleanupWorktreeIfNeeded();
                        const partialResult = extractPartialResult(agentMessages);
                        enqueueAgentNotification({
                          taskId: backgroundedTaskId,
                          description,
                          status: 'killed',
                          setAppState: rootSetAppState,
                          toolUseId: toolUseContext.toolUseId,
                          finalMessage: partialResult,
                          ...worktreeResult,
                        });
                        return;
                      }
                      const errMsg = errorMessage(error);
                      failAsyncAgent(backgroundedTaskId, errMsg, rootSetAppState);
                      const worktreeResult = await cleanupWorktreeIfNeeded();
                      enqueueAgentNotification({
                        taskId: backgroundedTaskId,
                        description,
                        status: 'failed',
                        error: errMsg,
                        setAppState: rootSetAppState,
                        toolUseId: toolUseContext.toolUseId,
                        ...worktreeResult,
                      });
                    } finally {
                      stopBackgroundedSummarization?.();
                      clearInvokedSkillsForAgent(syncAgentId);
                      clearDumpState(syncAgentId);
                      // Note: worktree cleanup is done before enqueueAgentNotification
                      // in both try and catch paths so we can include worktree info
                    }
                  });

                  // Return async_launched result immediately
                  const canReadOutputFile = toolUseContext.options.tools.some(
                    t => toolMatchesName(t, FILE_READ_TOOL_NAME) || toolMatchesName(t, BASH_TOOL_NAME),
                  );
                  return {
                    data: {
                      isAsync: true as const,
                      status: 'async_launched' as const,
                      agentId: backgroundedTaskId,
                      description: description,
                      prompt: prompt,
                      outputFile: getTaskOutputPath(backgroundedTaskId),
                      canReadOutputFile,
                    },
                  };
                }
              }

              // Process the message from the race result
              if (raceResult.type !== 'message') {
                // This shouldn't happen - background case handled above
                continue;
              }
              const { result } = raceResult;
              if (result.done) break;
              const message = result.value as MessageType;

              agentMessages.push(message);

              // Emit task_progress for the VS Code subagent panel
              updateProgressFromMessage(syncTracker, message, syncResolveActivity, toolUseContext.options.tools);
              if (foregroundTaskId) {
                const lastToolName = getLastToolUseName(message);
                if (lastToolName) {
                  emitTaskProgress(
                    syncTracker,
                    foregroundTaskId,
                    toolUseContext.toolUseId,
                    description,
                    agentStartTime,
                    lastToolName,
                  );
                  // Keep AppState task.progress in sync when SDK summaries are
                  // enabled, so updateAgentSummary reads correct token/tool counts
                  // instead of zeros.
                  if (getSdkAgentProgressSummariesEnabled()) {
                    updateAsyncAgentProgress(foregroundTaskId, getProgressUpdate(syncTracker), rootSetAppState);
                  }
                }
              }

              // Forward bash_progress events from sub-agent to parent so the SDK
              // receives tool_progress events just as it does for the main agent.
              if (
                message.type === 'progress' &&
                ((message.data as { type: string })?.type === 'bash_progress' ||
                  (message.data as { type: string })?.type === 'powershell_progress') &&
                onProgress
              ) {
                onProgress({
                  toolUseID: message.toolUseID as string,
                  data: message.data,
                });
              }

              if (message.type !== 'assistant' && message.type !== 'user') {
                continue;
              }

              // Increment token count in spinner for assistant messages
              // Subagent streaming events are filtered out in runAgent.ts, so we
              // need to count tokens from completed messages here
              if (message.type === 'assistant') {
                const contentLength = getAssistantMessageContentLength(message as AssistantMessage);
                if (contentLength > 0) {
                  toolUseContext.setResponseLength(len => len + contentLength);
                }
              }

              const normalizedNew = normalizeMessages([message]);
              for (const m of normalizedNew) {
                for (const content of (m.message?.content ?? []) as readonly { readonly type: string }[]) {
                  if (content.type !== 'tool_use' && content.type !== 'tool_result') {
                    continue;
                  }

                  // Forward progress updates
                  if (onProgress) {
                    onProgress({
                      toolUseID: `agent_${assistantMessage.message.id}`,
                      data: {
                        message: m,
                        type: 'agent_progress',
                        // prompt only needed on first progress message (UI.tsx:624
                        // reads progressMessages[0]). Omit here to avoid duplication.
                        prompt: '',
                        agentId: syncAgentId,
                      },
                    });
                  }
                }
              }
            }
          } catch (error) {
            // Handle errors from the sync agent loop
            // AbortError should be re-thrown for proper interruption handling
            if (error instanceof AbortError) {
              wasAborted = true;
              logEvent('tengu_agent_tool_terminated', {
                agent_type: metadata.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
                model: metadata.resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
                duration_ms: Date.now() - metadata.startTime,
                is_async: false,
                is_built_in_agent: metadata.isBuiltInAgent,
                reason: 'user_cancel_sync' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
              });
              throw error;
            }

            // Log the error for debugging
            logForDebugging(`Sync agent error: ${errorMessage(error)}`, {
              level: 'error',
            });

            // Store the error to handle after cleanup
            syncAgentError = toError(error);
          } finally {
            // Clear the background hint UI
            if (toolUseContext.setToolJSX) {
              toolUseContext.setToolJSX(null);
            }

            // Stop foreground summarization. Idempotent — if already stopped at
            // the backgrounding transition, this is a no-op. The backgrounded
            // closure owns a separate stop function (stopBackgroundedSummarization).
            stopForegroundSummarization?.();

            // Unregister foreground task if agent completed without being backgrounded
            if (foregroundTaskId) {
              unregisterAgentForeground(foregroundTaskId, rootSetAppState);
              // Notify SDK consumers (e.g. VS Code subagent panel) that this
              // foreground agent is done. Goes through drainSdkEvents() — does
              // NOT trigger the print.ts XML task_notification parser or the LLM loop.
              if (!wasBackgrounded) {
                const progress = getProgressUpdate(syncTracker);
                enqueueSdkEvent({
                  type: 'system',
                  subtype: 'task_notification',
                  task_id: foregroundTaskId,
                  tool_use_id: toolUseContext.toolUseId,
                  status: syncAgentError ? 'failed' : wasAborted ? 'stopped' : 'completed',
                  output_file: '',
                  summary: description,
                  usage: {
                    total_tokens: progress.tokenCount,
                    tool_uses: progress.toolUseCount,
                    duration_ms: Date.now() - agentStartTime,
                  },
                });
              }
            }

            // Clean up scoped skills so they don't accumulate in the global map
            clearInvokedSkillsForAgent(syncAgentId);

            // Clean up dumpState entry for this agent to prevent unbounded growth
            // Skip if backgrounded — the backgrounded agent's finally handles cleanup
            if (!wasBackgrounded) {
              clearDumpState(syncAgentId);
            }

            // Cancel auto-background timer if agent completed before it fired
            cancelAutoBackground?.();

            // Clean up worktree if applicable (in finally to handle abort/error paths)
            // Skip if backgrounded — the background continuation is still running in it
            if (!wasBackgrounded) {
              worktreeResult = await cleanupWorktreeIfNeeded();
            }
          }

          // Re-throw abort errors
          // TODO: Find a cleaner way to express this
          const lastMessage = agentMessages.findLast(_ => _.type !== 'system' && _.type !== 'progress');
          if (lastMessage && isSyntheticMessage(lastMessage)) {
            logEvent('tengu_agent_tool_terminated', {
              agent_type: metadata.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
              model: metadata.resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
              duration_ms: Date.now() - metadata.startTime,
              is_async: false,
              is_built_in_agent: metadata.isBuiltInAgent,
              reason: 'user_cancel_sync' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
            });
            throw new AbortError();
          }

          // If an error occurred during iteration, try to return a result with
          // whatever messages we have. If we have no assistant messages,
          // re-throw the error so it's properly handled by the tool framework.
          if (syncAgentError) {
            // Check if we have any assistant messages to return
            const hasAssistantMessages = agentMessages.some(msg => msg.type === 'assistant');

            if (!hasAssistantMessages) {
              // No messages collected, re-throw the error
              throw syncAgentError;
            }

            // We have some messages, try to finalize and return them
            // This allows the parent agent to see partial progress even after an error
            logForDebugging(`Sync agent recovering from error with ${agentMessages.length} messages`);
          }

          const agentResult = finalizeAgentTool(agentMessages, syncAgentId, metadata);

          if (feature('TRANSCRIPT_CLASSIFIER')) {
            const currentAppState = toolUseContext.getAppState();
            const handoffWarning = await classifyHandoffIfNeeded({
              agentMessages,
              tools: toolUseContext.options.tools,
              toolPermissionContext: currentAppState.toolPermissionContext,
              abortSignal: toolUseContext.abortController.signal,
              subagentType: selectedAgent.agentType,
              totalToolUseCount: agentResult.totalToolUseCount,
            });
            if (handoffWarning) {
              agentResult.content = [{ type: 'text' as const, text: handoffWarning }, ...agentResult.content];
            }
          }

          return {
            data: {
              status: 'completed' as const,
              prompt,
              ...agentResult,
              ...worktreeResult,
            },
          };
        }),
      );
    }
  },
  isReadOnly() {
    return true; // delegates permission checks to its underlying tools
  },
  toAutoClassifierInput(input) {
    const i = input as AgentToolInput;
    const tags = [i.subagent_type, i.mode ? `mode=${i.mode}` : undefined].filter((t): t is string => t !== undefined);
    const prefix = tags.length > 0 ? `(${tags.join(', ')}): ` : ': ';
    return `${prefix}${i.prompt}`;
  },
  isConcurrencySafe() {
    return true;
  },
  userFacingName,
  userFacingNameBackgroundColor,
  getActivityDescription(input) {
    return input?.description ?? 'Running task';
  },
  async checkPermissions(input, context): Promise<PermissionResult> {
    const appState = context.getAppState();

    // Only route through auto mode classifier when in auto mode
    // In all other modes, auto-approve sub-agent generation
    // Note: process.env.USER_TYPE === 'ant' guard enables dead code elimination for external builds
    if (process.env.USER_TYPE === 'ant' && appState.toolPermissionContext.mode === 'auto') {
      return {
        behavior: 'passthrough',
        message: 'Agent tool requires permission to spawn sub-agents.',
      };
    }

    return { behavior: 'allow', updatedInput: input };
  },
  mapToolResultToToolResultBlockParam(data, toolUseID) {
    // Multi-agent spawn result
    const internalData = data as InternalOutput;
    if (
      typeof internalData === 'object' &&
      internalData !== null &&
      'status' in internalData &&
      internalData.status === 'teammate_spawned'
    ) {
      const spawnData = internalData as TeammateSpawnedOutput;
      return {
        tool_use_id: toolUseID,
        type: 'tool_result',
        content: [
          {
            type: 'text',
            text: `Spawned successfully.
agent_id: ${spawnData.teammate_id}
name: ${spawnData.name}
team_name: ${spawnData.team_name}
The agent is now running and will receive instructions via mailbox.`,
          },
        ],
      };
    }
    if ('status' in internalData && internalData.status === 'remote_launched') {
      const r = internalData;
      return {
        tool_use_id: toolUseID,
        type: 'tool_result',
        content: [
          {
            type: 'text',
            text: `Remote agent launched in CCR.\ntaskId: ${r.taskId}\nsession_url: ${r.sessionUrl}\noutput_file: ${r.outputFile}\nThe agent is running remotely. You will be notified automatically when it completes.\nBriefly tell the user what you launched and end your response.`,
          },
        ],
      };
    }
    if (data.status === 'async_launched') {
      const prefix = `Async agent launched successfully.\nagentId: ${data.agentId} (internal ID - do not mention to user. Use SendMessage with to: '${data.agentId}' to continue this agent.)\nThe agent is working in the background. You will be notified automatically when it completes.`;
      const instructions = data.canReadOutputFile
        ? `Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\noutput_file: ${data.outputFile}\nIf asked, you can check progress before completion by using ${FILE_READ_TOOL_NAME} or ${BASH_TOOL_NAME} tail on the output file.`
        : `Briefly tell the user what you launched and end your response. Do not generate any other text — agent results will arrive in a subsequent message.`;
      const text = `${prefix}\n${instructions}`;
      return {
        tool_use_id: toolUseID,
        type: 'tool_result',
        content: [
          {
            type: 'text',
            text,
          },
        ],
      };
    }
    if (data.status === 'completed') {
      const worktreeData = data as Record<string, unknown>;
      const worktreeInfoText = worktreeData.worktreePath
        ? `\nworktreePath: ${worktreeData.worktreePath}\nworktreeBranch: ${worktreeData.worktreeBranch}`
        : '';
      // If the subagent completes with no content, the tool_result is just the
      // agentId/usage trailer below — a metadata-only block at the prompt tail.
      // Some models read that as "nothing to act on" and end their turn
      // immediately. Say so explicitly so the parent has something to react to.
      const contentOrMarker =
        data.content.length > 0
          ? data.content
          : [
              {
                type: 'text' as const,
                text: '(Subagent completed but returned no output.)',
              },
            ];
      // One-shot built-ins (Explore, Plan) are never continued via SendMessage
      // — the agentId hint and <usage> block are dead weight (~135 chars ×
      // 34M Explore runs/week ≈ 1-2 Gtok/week). Telemetry doesn't parse this
      // block (it uses logEvent in finalizeAgentTool), so dropping is safe.
      // agentType is optional for resume compat — missing means show trailer.
      if (data.agentType && ONE_SHOT_BUILTIN_AGENT_TYPES.has(data.agentType) && !worktreeInfoText) {
        return {
          tool_use_id: toolUseID,
          type: 'tool_result',
          content: contentOrMarker,
        };
      }
      return {
        tool_use_id: toolUseID,
        type: 'tool_result',
        content: [
          ...contentOrMarker,
          {
            type: 'text',
            text: `agentId: ${data.agentId} (use SendMessage with to: '${data.agentId}' to continue this agent)${worktreeInfoText}
<usage>total_tokens: ${data.totalTokens}
tool_uses: ${data.totalToolUseCount}
duration_ms: ${data.totalDurationMs}</usage>`,
          },
        ],
      };
    }
    data satisfies never;
    throw new Error(`Unexpected agent tool result status: ${(data as { status: string }).status}`);
  },
  renderToolResultMessage,
  renderToolUseMessage,
  renderToolUseTag,
  renderToolUseProgressMessage,
  renderToolUseRejectedMessage,
  renderToolUseErrorMessage,
  renderGroupedToolUse: renderGroupedAgentToolUse,
} satisfies ToolDef<InputSchema, Output, Progress>);

function resolveTeamName(
  input: { team_name?: string },
  appState: { teamContext?: { teamName: string } },
): string | undefined {
  if (!isAgentSwarmsEnabled()) return undefined;
  return input.team_name || appState.teamContext?.teamName;
}
