Skip to content

fix: prevent infinite loop with self-referencing handoffs in Autopilot mode (#325128)#325145

Open
AmariahAK wants to merge 2 commits into
microsoft:mainfrom
AmariahAK:main
Open

fix: prevent infinite loop with self-referencing handoffs in Autopilot mode (#325128)#325145
AmariahAK wants to merge 2 commits into
microsoft:mainfrom
AmariahAK:main

Conversation

@AmariahAK

Copy link
Copy Markdown

Description

Fixes #325128

### What was the issue

In Autopilot mode, a custom agent with `handoffs` that have `send: true` pointing to the same agent creates an infinite loop. Each response auto-triggers a handoff back to itself, which produces the same response, which triggers the handoff again — forever.

### Where was the issue

`src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts` — the `renderChatSuggestNextWidget()` method. When Autopilot is active and a response carries handoffs with `send: true`, the code auto-triggers the first matching handoff via `handleNextPromptSelection()` without checking whether the handoff targets the same mode that produced the response. The Copilot ToolCallingLoop correctly stops when the model produces a text-only response, but the handoff auto-trigger in the UI layer creates a new turn that restarts the cycle.

### How it was fixed

A single guard added at line 1351 in `renderChatSuggestNextWidget()`:

```diff
- if (autoSendHandoff) {
+ if (autoSendHandoff && autoSendHandoff.agent !== responseMode.name.get()) {

Before auto-triggering a send: true handoff, the code now compares the handoff's target agent against the name of the mode that produced the response. If they match (self-referencing), the auto-trigger is skipped. Handoff buttons are still rendered for manual use — only the automatic send is blocked for self-referencing handoffs.

Why this method over others

  • Guard in the ToolCallingLoop: not viable — the loop has no visibility into handoffs, which live in the chat widget UI layer above it.
  • Depth counter / cycle detector: over-engineered for a single equality check, adds state and complexity.
  • Guard at the auto-trigger point (chosen): the fix lives exactly where the loop forms — minimal (1 condition), stateless, uses existing types (IHandOff.agent, IChatMode.name) already in scope. No new imports, no behavior change for legitimate cross-agent handoffs.

How to test

  1. Create a .agent.md file with a self-referencing handoff:

    ---
    name: test agent
    description: 'A custom agent with handoffs.'
    tools: [vscode, execute, read, agent, edit, search, web, browser, todo]
    handoffs:
      - label: Button 1
        agent: 'test agent'
        prompt: This is a handoff from Button 1.
        send: true
      - label: Button 2
        agent: 'test agent'
        prompt: This is a handoff from Button 2.
        send: true
    ---
    Always reply with 'Hi, I'm your custom agent. How can I assist you today?'
  2. Enable Autopilot mode and select the "test agent" mode

  3. Send "Hi"

    • Before: infinite loop of auto-triggered handoffs
    • After: one response rendered, handoff buttons displayed, no auto-trigger
  4. Verify handoff buttons are visible and clickable for manual use

  5. Verify a Plan → Agent handoff with send: true still auto-triggers (different agents should work normally)

…t mode

In Autopilot mode, a custom agent with handoffs that have send:true pointing
to the same agent creates an infinite loop. Each response auto-triggers a
handoff back to itself, which produces the same response again.

The fix adds a guard in renderChatSuggestNextWidget() to skip the auto-send
handoff when the handoff targets the same mode that produced the response.
Handoff buttons are still rendered for manual use.

Fixes microsoft#325128

Co-authored-by: atlarix-agent <agent@atlarix.dev>
Copilot AI review requested due to automatic review settings July 9, 2026 15:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents an infinite Autopilot loop in the chat widget when an agent response includes a self-referencing send: true handoff, by adding a guard before auto-triggering a handoff. This lives in the workbench chat UI layer (ChatWidget) where the auto-send behavior is implemented.

Changes:

  • Add a condition to skip auto-triggering send: true handoffs that target the same agent/mode that produced the response.

Comment on lines 1349 to 1353
if (permissionLevel === ChatPermissionLevel.Autopilot) {
const autoSendHandoff = handoffs.find(h => h.send);
if (autoSendHandoff) {
if (autoSendHandoff && autoSendHandoff.agent !== responseMode.name.get()) {
this.handleNextPromptSelection(autoSendHandoff);
return;
Move the self-referencing guard from the if-block into the find() predicate
so that subsequent send:true handoffs targeting a different agent still
auto-trigger when the first one is self-referencing. Also compare against
both responseMode.name and responseMode.id since handoff agents can be
specified by either.

Co-authored-by: atlarix-agent <agent@atlarix.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants