Skip to content

SubagentRunner: contexto independiente para subtareas

源码版本v4.0.10

Responsabilidades

SubagentRunner es el ejecutor con el que Cline encapsula la acción de «dejar que un LLM ejecute una subtarea en su propia ventana de contexto». Cuando el agente principal se encuentra con una llamada a la herramienta subagent (new SubagentRunner:215), lanza un runner por cada prompt. El runner ejecuta internamente un agent loop en miniatura —recibe el stream, parsea tool_calls, los ejecuta y vuelve a inyectar los resultados en la conversación— pero con un arreglo conversation propio y un ContextManager independiente, sin contaminar el historial de mensajes del Task principal.

Su posición está por debajo del Task principal y por encima de las herramientas concretas. El Task principal pasa el prompt y un TaskConfig; el runner usa SubagentBuilder para calcular el subconjunto de herramientas permitidas, el system prompt y el API handler del subagente, y luego entra en su bucle run. Al terminar, el subagente invoca attempt_completion y la cadena resultante se devuelve tal cual al agente principal, que la ve como un resultado de herramienta normal. Este diseño permite al Task principal confinar acciones costosas como «explorar código, leer varios archivos y sintetizar una respuesta» dentro de una ventana aislada, de modo que sus tokens intermedios no arrastren el contexto principal.

Motivación de diseño

  • Ventana de contexto independiente: el subagente usa su propio arreglo ClineStorageMessage[]; el Task principal no ve las llamadas a herramientas intermedias del subagente (conversation init:463).
  • Lista blanca de herramientas: el subagente por defecto solo lee y no escribe; SUBAGENT_DEFAULT_ALLOWED_TOOLS solo incluye file_read / list_files / search / list_code_def / bash / use_skill / attempt_completion (default allowed tools:14).
  • attempt_completion forzado: independientemente del conjunto de herramientas configurado por el usuario, ATTEMPT siempre se añade (force ATTEMPT:96), de lo contrario el subagente no podría cerrar.
  • Override de modelo por configuración de agente: AgentConfigLoader lee el modelId de cada agente y SubagentBuilder.applyModelOverride reemplaza el apiHandler (applyModelOverride:57), permitiendo distintos modelos para distintos subagentes.
  • Compactación automática de contexto: antes de cada ronda, el runner verifica el conteo de tokens de la petición anterior y, si supera el umbral, llama a compactConversationForContextWindow, que primero aplica la optimización de file read y luego el truncado (shouldCompactBeforeNextRequest:488).
  • Reintento ante respuesta vacía: si en una ronda el modelo no emite tool_use, se trata como respuesta vacía; se inyecta un prompt noToolsUsed y se reinterroga; si se supera MAX_EMPTY_ASSISTANT_RETRIES (3), falla directamente (empty response retry:662).
  • Cancelación sincronizada de runners paralelos: una llamada a la herramienta subagent puede traer varios prompts; los runners se ejecutan en paralelo y se hace polling del abort cada 100 ms (abort poll:216), interrumpiendo todos a la vez.

Archivos clave

Flujo de datos

Al arrancar, el runner prepara el system prompt y el contenido de usuario inicial. La conversación inicial solo contiene dos entradas: el prompt del usuario + un bloque de metadata de workspace, que el task loop del lado servidor exige verificar:

typescript
// apps/vscode/src/core/task/tools/subagent/SubagentRunner.ts
const conversation: ClineStorageMessage[] = [
  {
    role: "user",
    content: [
      { type: "text", text: prompt } as ClineTextContentBlock,
      // Server-side task loop checks require workspace metadata to be present in the
      // initial user message of subagent runs.
      ...(workspaceMetadataEnvironmentBlock
        ? [{ type: "text", text: workspaceMetadataEnvironmentBlock } as ClineTextContentBlock]
        : []),
    ],
  },
];

while (true) {
  if (
    usageState.lastRequest &&
    this.shouldCompactBeforeNextRequest(usageState.lastRequest.totalTokens, api, providerInfo.model.id)
  ) {
    const compactResult = this.compactConversationForContextWindow(
      contextManager,
      conversation,
      contextState.conversationHistoryDeletedRange,
    );
    contextState.conversationHistoryDeletedRange = compactResult.conversationHistoryDeletedRange;
    // ...
  }
  // ...
}

Esto está en conversation + while:463. Entrado el bucle, cada ronda createMessageWithInitialChunkRetry consume el stream y clasifica los chunks en usage/text/tool_calls/reasoning. Al acabar el flujo, ejecuta los finalized tool calls uno a uno; el que toca attempt_completion devuelve el result directamente al agente principal (return on attempt:723); las demás herramientas se ejecutan vía coordinator.getHandler(toolName).execute y el resultado se reinyecta como user content para seguir el bucle.

Límites y fallos

  • Herramienta fuera de la lista blanca = toolError directo: si el subagente intenta invocar write_to_file, se intercepta y se inserta la cadena de error en el user content (whitelist check:726), el bucle continúa y no se aborta toda la tarea.
  • attempt_completion sin result: si result está vacío, se inserta missingToolParameterError en lugar de cerrar (missing result:705).
  • Retry si el primer chunk reporta context window exceeded: si el primer chunk falla con un error relacionado a context window, se compacta la conversación y se reintenta (context window retry:1036), hasta MAX_INITIAL_STREAM_ATTEMPTS veces.
  • Comando en ejecución durante abort: abort no solo cancela el flujo de API, también delega a cancelRunningCommandTool para detener el bash en curso (cancel running command:281).
  • Fallback native vs non-native tool calls: en modo non-native, al recibir chunks estructurados de tool_calls se ejecutan igual, pero el resultado se serializa como texto plano para evitar desalineación en el emparejamiento tool_result (non-native fallback:631).
  • Acumulación de stats: cada chunk actualiza inputTokens/outputTokens/cacheWrite/cacheRead/totalCost y los comunica a la UI en tiempo real vía onProgress (stats accumulation:534).
  • Assistant vacío también cuenta como ronda: si el contenido del assistant viene completamente vacío, se inserta manualmente «Failure: I did not provide a response.» antes de inyectar noToolsUsed (empty assistant:671).

Resumen

SubagentRunner permite al agente principal externalizar la «exploración de gran alcance» a un mini-agent aislado. Para ver la estrategia de compactación de contexto de la que depende, consulta context-manager; para ver cómo el Task principal reinyecta los resultados de herramientas y recursiona, consulta agent-loop/task-class; para ver cómo las herramientas MCP son invocadas por el agente principal, consulta mcp-hub.

Véase la documentación oficial: Cline 文档 · README