feat(core): implement mid-turn queue drain for agent execution (#2854)
Some checks are pending
Qwen Code CI / CodeQL (push) Waiting to run
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run

* feat(core): implement mid-turn queue drain for agent execution

Inject queued user messages between tool execution steps within a single
turn, so the model sees them immediately instead of waiting for the
entire round to complete.

- Add `dequeueAll()` to AsyncMessageQueue
- Add `midTurnDrain` callback to ReasoningLoopOptions
- Drain queue after processFunctionCalls, inject as text parts
- AgentComposer always enqueues directly (no local buffering)
- Add QUEUE_MESSAGES_CONSUMED event for UI sync

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(cli): add mid-turn queue drain to main session

Extend mid-turn queue drain to the main session's tool execution path
(useGeminiStream). Previously only agent tabs had this feature.

- Add midTurnDrainRef parameter to useGeminiStream
- Inject queued messages in handleCompletedTools before submitQuery
- Bridge useMessageQueue to drain ref in AppContainer via ref pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Copilot review feedback on mid-turn drain

- Guard midTurnDrain with abort check to prevent message loss on cancel
- Synchronously clear messageQueueRef to prevent duplicate drains
- Only clear pending display on IDLE status, not all status changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: scope mid-turn drain to main session only

Revert subagent-path changes (AgentCore, AgentInteractive,
AgentComposer, AsyncMessageQueue, agent-events) to keep the PR
focused on the main session, which is easier to test and validate.

Subagent mid-turn drain can be added in a follow-up PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Copilot review on main session mid-turn drain

- Move synchronous queue ref into useMessageQueue itself, expose
  drainQueue() for atomic drain (fixes race between addMessage and drain)
- Record drained messages as USER history items so the transcript
  stays complete
- Simplify AppContainer bridge to just midTurnDrainRef.current = drainQueue

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard mid-turn drain against cancelled turns

- Skip drain when turnCancelledRef or abortController signal is set,
  so queued messages stay for the next turn instead of being lost
- Restore ref-based queue bridge (drainQueue removed from useMessageQueue)
- Keep synchronous ref clear to prevent duplicate drains

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shaojin Wen 2026-04-07 09:14:44 +08:00 committed by GitHub
parent 5df8fa0ff2
commit b6373ac71e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 65 additions and 4 deletions

View file

@ -173,6 +173,7 @@ export const useGeminiStream = (
setShellInputFocused: (value: boolean) => void,
terminalWidth: number,
terminalHeight: number,
midTurnDrainRef?: React.RefObject<(() => string[]) | null>,
) => {
const [initError, setInitError] = useState<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
@ -1572,6 +1573,23 @@ export const useGeminiStream = (
return;
}
// Mid-turn queue drain: inject queued user messages alongside tool
// results so the model sees them in the next API call.
// Skip if the turn was cancelled — messages stay in queue for next turn.
const drained =
turnCancelledRef.current || abortControllerRef.current?.signal.aborted
? []
: (midTurnDrainRef?.current?.() ?? []);
if (drained.length > 0) {
for (const msg of drained) {
responsesToSend.push({
text: `\n[User message received during tool execution]: ${msg}`,
});
// Record in UI history so the transcript stays complete.
addItem({ type: MessageType.USER, text: msg }, Date.now());
}
}
submitQuery(responsesToSend, SendMessageType.ToolResult, prompt_ids[0]);
},
[
@ -1582,6 +1600,8 @@ export const useGeminiStream = (
performMemoryRefresh,
modelSwitchedFromQuotaError,
config,
midTurnDrainRef,
addItem,
],
);