From f46a1c7e8bede146e97803d5712e7162915cc5fa Mon Sep 17 00:00:00 2001 From: Adam Weidman Date: Sun, 15 Mar 2026 15:43:34 -0400 Subject: [PATCH] refactor(core): move background completion consumption from UI to agent loop The agent loop in local-executor now listens via onInjection (all sources) instead of onUserHint (steering only), picking up background completions between turns. This removes the separate bg completion useEffect, refs, state, and callback from AppContainer entirely. --- packages/cli/src/ui/AppContainer.tsx | 60 ++-------------------- packages/core/src/agents/local-executor.ts | 27 +++++++--- 2 files changed, 26 insertions(+), 61 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 241e0f3030..259913f161 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -85,7 +85,6 @@ import { buildUserSteeringHintPrompt, logBillingEvent, ApiKeyUpdatedEvent, - type InjectionSource, } from '@google/gemini-cli-core'; import { validateAuthMethod } from '../config/auth.js'; import process from 'node:process'; @@ -1078,8 +1077,6 @@ Logging in with Google... Restarting Gemini CLI to continue. const pendingHintsRef = useRef([]); const [pendingHintCount, setPendingHintCount] = useState(0); - const pendingBackgroundCompletionsRef = useRef([]); - const [pendingBgCompletionCount, setPendingBgCompletionCount] = useState(0); const consumePendingHints = useCallback(() => { if (pendingHintsRef.current.length === 0) { @@ -1091,29 +1088,14 @@ Logging in with Google... Restarting Gemini CLI to continue. return hint; }, []); - const consumePendingBackgroundCompletions = useCallback(() => { - if (pendingBackgroundCompletionsRef.current.length === 0) { - return null; - } - const output = pendingBackgroundCompletionsRef.current.join('\n'); - pendingBackgroundCompletionsRef.current = []; - setPendingBgCompletionCount(0); - return output; - }, []); - useEffect(() => { - const injectionListener = (text: string, source: InjectionSource) => { - if (source === 'user_steering') { - pendingHintsRef.current.push(text); - setPendingHintCount((prev) => prev + 1); - } else if (source === 'background_completion') { - pendingBackgroundCompletionsRef.current.push(text); - setPendingBgCompletionCount((prev) => prev + 1); - } + const hintListener = (hint: string) => { + pendingHintsRef.current.push(hint); + setPendingHintCount((prev) => prev + 1); }; - config.injectionService.onInjection(injectionListener); + config.injectionService.onUserHint(hintListener); return () => { - config.injectionService.offInjection(injectionListener); + config.injectionService.offUserHint(hintListener); }; }, [config]); @@ -2148,38 +2130,6 @@ Logging in with Google... Restarting Gemini CLI to continue. pendingHintCount, ]); - // Reinject completed background execution output into the model conversation. - // Unlike user steering hints, this is NOT gated on model steering being enabled. - useEffect(() => { - if ( - !isConfigInitialized || - streamingState !== StreamingState.Idle || - !isMcpReady || - isToolAwaitingConfirmation(pendingHistoryItems) - ) { - return; - } - - const bgOutput = consumePendingBackgroundCompletions(); - if (!bgOutput) { - return; - } - - void submitQuery([ - { - text: `Background execution update:\n${bgOutput}\n\nThe above background execution has completed. Review the output and continue your work accordingly.`, - }, - ]); - }, [ - isConfigInitialized, - isMcpReady, - streamingState, - submitQuery, - consumePendingBackgroundCompletions, - pendingHistoryItems, - pendingBgCompletionCount, - ]); - const allToolCalls = useMemo( () => pendingHistoryItems diff --git a/packages/core/src/agents/local-executor.ts b/packages/core/src/agents/local-executor.ts index c761641b55..29c0593184 100644 --- a/packages/core/src/agents/local-executor.ts +++ b/packages/core/src/agents/local-executor.ts @@ -65,6 +65,7 @@ import { getToolCallContext } from '../utils/toolCallContext.js'; import { scheduleAgentTools } from './agent-scheduler.js'; import { DeadlineTimer } from '../utils/deadlineTimer.js'; import { formatUserHintsForModel } from '../utils/fastAckHelper.js'; +import type { InjectionSource } from '../config/injectionService.js'; /** A callback function to report on agent activity. */ export type ActivityCallback = (activity: SubagentActivityEvent) => void; @@ -526,14 +527,19 @@ export class LocalAgentExecutor { : DEFAULT_QUERY_STRING; const pendingHintsQueue: string[] = []; - const hintListener = (hint: string) => { - pendingHintsQueue.push(hint); + const pendingBgCompletionsQueue: string[] = []; + const injectionListener = (text: string, source: InjectionSource) => { + if (source === 'user_steering') { + pendingHintsQueue.push(text); + } else if (source === 'background_completion') { + pendingBgCompletionsQueue.push(text); + } }; // Capture the index of the last hint before starting to avoid re-injecting old hints. // NOTE: Hints added AFTER this point will be broadcast to all currently running // local agents via the listener below. const startIndex = this.config.injectionService.getLatestHintIndex(); - this.config.injectionService.onUserHint(hintListener); + this.config.injectionService.onInjection(injectionListener); try { const initialHints = @@ -585,20 +591,29 @@ export class LocalAgentExecutor { // If status is 'continue', update message for the next loop currentMessage = turnResult.nextMessage; - // Check for new user steering hints collected via subscription + // Inject background completion output into the next turn. + if (pendingBgCompletionsQueue.length > 0) { + const bgText = pendingBgCompletionsQueue.join('\n'); + pendingBgCompletionsQueue.length = 0; + currentMessage.parts ??= []; + currentMessage.parts.unshift({ + text: `Background execution update:\n${bgText}\n\nThe above background execution has completed. Review the output and continue your work accordingly.`, + }); + } + + // Check for new user steering hints collected via subscription. if (pendingHintsQueue.length > 0) { const hintsToProcess = [...pendingHintsQueue]; pendingHintsQueue.length = 0; const formattedHints = formatUserHintsForModel(hintsToProcess); if (formattedHints) { - // Append hints to the current message (next turn) currentMessage.parts ??= []; currentMessage.parts.unshift({ text: formattedHints }); } } } } finally { - this.config.injectionService.offUserHint(hintListener); + this.config.injectionService.offInjection(injectionListener); } // === UNIFIED RECOVERY BLOCK ===