Skip to content

ACP path: align non-hook middleware with GeminiClient.sendMessageStream #3480

@zhangxy-zju

Description

@zhangxy-zju

What would you like to be added?

The ACP Session.#executePrompt sends turns via chat.sendMessageStream() directly, bypassing GeminiClient.sendMessageStream() and all the per-turn middleware attached there. Hooks have been individually wired into Session.ts over time, but non-hook middleware is still missing.

Goal: bring the ACP path to full parity with the React TUI / non-interactive paths so features don't silently no-op in ACP.

Current gap matrix

Comparing client.ts:590-1020 (GeminiClient) vs Session.ts (ACP):

Middleware slice In GeminiClient In Session.ts Tracked
UserPromptSubmit hook
Stop / StopFailure hooks
Pre/Post ToolUse hooks
Notification hook
System reminder: plan mode partial (in PR #3479) #1151
System reminder: subagent partial (in PR #3479) #1151
System reminder: arena partial (in PR #3479) #1151
System reminder: relevantAutoMemory this issue
Auto-memory recall prefetch this issue
IDE context injection (cursor, selection, diff) this issue
Loop detector (reset + runtime check) this issue
Arena control-signal check (pre-turn) this issue
stripOrphanedUserEntriesFromHistory on Retry this issue

User-facing symptoms of each gap

  • Auto-memory: managed memory recall (MemoryManager.recall) never runs in ACP. Projects relying on managed memory see the model missing context.
  • IDE context: VSCode companion users don't get cursor/selection/diff hints piped into the turn.
  • Loop detector: infinite-tool-call loops aren't caught; ACP users can burn through tokens silently.
  • Arena control-signal: arena sessions can't be cancelled via control channel mid-turn.
  • Retry history cleanup: retried turns may leave orphaned user entries in history.

Two paths forward

Option A — keep copying middleware into Session.ts

Continue the pattern from PR #3479: for each missing slice, reimplement in Session.ts. Low per-step risk but drift continues.

Option B — switch ACP to geminiClient.sendMessageStream

Structurally correct. Requires:

  1. Delete Session.ts's hand-rolled UserPromptSubmit hook (L359–401) and #handleStopHookLoop to avoid double-firing.
  2. Adapt the event stream: GeminiClient yields ServerGeminiStreamEvent, ACP currently consumes raw StreamEvent (chat stream chunks). Need a translation layer.
  3. Handle SendMessageType.{UserQuery,Cron,Hook,Retry,Notification} branch semantics properly (Session.ts currently doesn't distinguish).
  4. Make sure the tool-call fan-out added in ACP Session: Agent tool calls should run concurrently #2516 / fix(cli): run ACP Agent tool calls concurrently (#2516) #3463 still works after the switch.

This is effectively a subset of #3247 (full unification refactor).

Option C — extract middleware as a primitive

Move the per-turn middleware (system reminders, IDE context, loop detector, auto-memory prefetch) out of GeminiClient.sendMessageStream into a pure function / class that both GeminiClient and Session import. Then ACP keeps its own loop but gets the middleware for free.

Recommendation

Start with Option C for the cheapest, lowest-risk convergence; Option B can come later as part of #3247.

Related

Relevant files

  • packages/core/src/core/client.ts:590-1020GeminiClient.sendMessageStream middleware (reference)
  • packages/cli/src/acp-integration/session/Session.ts:289, 857 — ACP prompt entry points
  • packages/cli/src/acp-integration/session/Session.ts:1106+#buildInitialSystemReminders (to be extended or superseded)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions