Skip to content

SubagentRunner: Unabhängiger Kontext für Teilaufgaben

源码版本v4.0.10

Verantwortung

SubagentRunner ist der von Cline gekapselte Executor für «ein LLM eine Teilaufgabe in seinem eigenen Kontextfenster ausführen zu lassen». Wenn der Haupt-Agent auf einen Werkzeugaufruf (tool_use) wie subagent trifft (new SubagentRunner:215), wird für jeden Prompt ein Runner gestartet. Der Runner führt intern eine Mini-Agent-Loop aus — Stream ziehen, tool_calls parsen, ausführen, Ergebnisse zurück in die Konversation pushen — aber mit einem vollständig unabhängigen conversation-Array und einem unabhängigen ContextManager, sodass die Nachrichten-History des Haupt-Tasks nicht verschmutzt wird.

Seine Position liegt unter dem Haupt-Task und über den konkreten Werkzeugen. Der Haupt-Task übergibt Prompt und TaskConfig, der Runner berechnet über SubagentBuilder die Werkzeug-Teilmenge, die System-Prompts und den API-Handler, die dieser Subagent nutzen darf, und iteriert dann in seiner eigenen run-Methode. Wenn der Subagent fertig ist, ruft er attempt_completion auf; der Ergebnis-String geht unverändert an den Haupt-Agent zurück, der aus Sicht des Haupt-Agent nur ein normales Werkzeugergebnis ist. Dieses Design erlaubt es dem Haupt-Task, teure Aktionen wie «Code erkunden, mehrere Dateien lesen, eine Antwort synthetisieren» in einem isolierten Fenster einzusperren, ohne dass die zwischenzeitlichen Token den Hauptkontext belasten.

Entwurfsmotivation

  • Unabhängiges Kontextfenster: Der Subagent nutzt sein eigenes ClineStorageMessage[]-Array; der Haupt-Task sieht die zwischenzeitlichen tool-Aufrufe des Subagenten nicht (conversation init:463).
  • Werkzeug-Whitelist: Der Subagent ist standardmäßig nur lesend, SUBAGENT_DEFAULT_ALLOWED_TOOLS erlaubt nur file_read / list_files / search / list_code_def / bash / use_skill / attempt_completion (default allowed tools:14).
  • attempt_completion erzwingen: Unabhängig davon, welche Werkzeuge der Benutzer konfiguriert, wird ATTEMPT immer hinzugefügt (force ATTEMPT:96), da der Subagent sonst nicht abschließen kann.
  • Modell-Override pro Agent: AgentConfigLoader liest die modelId eines einzelnen Agenten, SubagentBuilder.applyModelOverride ersetzt den apiHandler (applyModelOverride:57), sodass verschiedene Subagenten verschiedene Modelle nutzen.
  • Automatisches Kontext-Komprimieren: Der Runner prüft vor jeder Anfrage die Token-Zahl der letzten Anfrage; bei Überschreitung der Schwelle wird compactConversationForContextWindow aufgerufen, das zuerst file read-Optimierung durchführt und dann abschneidet (shouldCompactBeforeNextRequest:488).
  • Leer-Antwort-Wiederholung: Wenn das Modell in dieser Runde kein tool_use ausgibt, gilt dies als Leerantwort; es wird ein noToolsUsed-Prompt angehängt und erneut gefragt, bei mehr als MAX_EMPTY_ASSISTANT_RETRIES (3) Versuchen schlägt der Lauf direkt fehl (empty response retry:662).
  • Parallele Runner-Abbruch-Synchronisation: Ein subagent-Werkzeugaufruf kann mehrere Prompts enthalten; mehrere Runner laufen parallel, abort wird alle 100 ms einmal gepollt (abort poll:216), um alle Runner gemeinsam abzubrechen.

Schlüsseldateien

Datenfluss

Beim Start bereitet der Runner den Systemprompt und den anfänglichen user-Content vor. Die Anfangskonversation enthält nur zwei Einträge: den Prompt des Benutzers + einen Workspace-Metadaten-Block, der für die Validierung des serverseitigen Task-Loops benötigt wird:

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;
    // ...
  }
  // ...
}

Dieses Stück liegt nahe conversation + while:463. Nach Eintritt in die Schleife zieht jede Runde createMessageWithInitialChunkRetry den Stream; Stream-Chunks werden in usage/text/tool_calls/reasoning klassifiziert. Nach Stream-Ende werden die finalisierten Tool-Calls einzeln ausgeführt; ein Call, der attempt_completion trifft, gibt das Ergebnis direkt an den Haupt-Agent zurück (return on attempt:723); andere Werkzeuge werden über coordinator.getHandler(toolName).execute ausgeführt, das Ergebnis als user-Content zurückgespült und die Schleife fortgesetzt.

Grenzen und Fehler

  • Werkzeug nicht in Whitelist gibt direkt toolError: Wenn der Subagent z. B. write_to_file aufrufen will, wird es blockiert; die Fehler-Zeichenkette wird in den user-Content gesteckt (whitelist check:726), die Schleife läuft weiter und bricht die gesamte Aufgabe nicht ab.
  • attempt_completion ohne result: Wenn result leer ist, wird missingToolParameterError eingefügt statt abzuschließen (missing result:705).
  • Erster Chunk mit context window exceeded: Wiederholung: Wenn der erste Chunk fehlschlägt und der Fehler mit dem Kontextfenster zusammenhängt, wird sofort komprimiert und erneut versucht (context window retry:1036), maximal MAX_INITIAL_STREAM_ATTEMPTS Mal.
  • Laufendes Kommando bei abort: abort bricht nicht nur den API-Stream ab, sondern delegiert auch an cancelRunningCommandTool, um den aktuellen bash zu stoppen (cancel running command:281).
  • native vs non-native tool calls fallback: Im non-native-Modus werden beim Empfang strukturierter tool_calls-Chunks diese dennoch ausgeführt, aber das Ergebnis als Plain-Text serialisiert zurückgegeben, um eine tool_result-Paarungs-Verschiebung zu vermeiden (non-native fallback:631).
  • stats-Akkumulation: Bei jedem Chunk werden inputTokens/outputTokens/cacheWrite/cacheRead/totalCost aktualisiert und über onProgress in Echtzeit ans Frontend gemeldet (stats accumulation:534).
  • Leere assistant-Antwort zählt als Runde: Wenn der assistant-Content vollständig leer ist, wird aktiv ein «Failure: I did not provide a response.» eingefügt und dann noToolsUsed angehängt (empty assistant:671).

Zusammenfassung

SubagentRunner ermöglicht es dem Haupt-Agent, «große Erkundungen» an einen isolierten Mini-Agenten auszulagern. Um die Kontext-Komprimierungsstrategie zu sehen, von der er abhängt, lies context-manager; um zu sehen, wie der Haupt-Task Werkzeugergebnisse zurückführt und rekursiert, lies agent-loop/task-class; um zu sehen, wie MCP-Werkzeuge vom Haupt-Agent aufgerufen werden, lies mcp-hub.

Siehe offizielle Dokumentation: Cline-Dokumentation · README