@@ -5,14 +5,14 @@ package middleware
55
66import (
77 "context"
8+ "encoding/json"
89 "errors"
910 "fmt"
1011 "strings"
1112
1213 "github.com/azure/azure-dev/cli/azd/cmd/actions"
1314 "github.com/azure/azure-dev/cli/azd/internal"
1415 "github.com/azure/azure-dev/cli/azd/internal/agent"
15- "github.com/azure/azure-dev/cli/azd/internal/agent/feedback"
1616 "github.com/azure/azure-dev/cli/azd/internal/tracing"
1717 "github.com/azure/azure-dev/cli/azd/internal/tracing/events"
1818 "github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
@@ -53,19 +53,35 @@ func NewErrorMiddleware(
5353 }
5454}
5555
56- func (e * ErrorMiddleware ) Run (ctx context.Context , next NextFn ) (* actions.ActionResult , error ) {
57- actionResult , err := next (ctx )
56+ func (e * ErrorMiddleware ) displayAgentResponse (ctx context.Context , response string , disclaimer string ) {
57+ if response != "" {
58+ e .console .Message (ctx , disclaimer )
59+ e .console .Message (ctx , "" )
60+ e .console .Message (ctx , fmt .Sprintf ("%s:" , output .AzdAgentLabel ()))
61+ e .console .Message (ctx , output .WithMarkdown (response ))
62+ e .console .Message (ctx , "" )
63+ }
64+ }
5865
66+ func (e * ErrorMiddleware ) Run (ctx context.Context , next NextFn ) (* actions.ActionResult , error ) {
5967 // Short-circuit agentic error handling in non-interactive scenarios:
6068 // - LLM feature is disabled
6169 // - User specified --no-prompt (non-interactive mode)
6270 // - Running in CI/CD environment where user interaction is not possible
6371 if ! e .featuresManager .IsEnabled (llm .FeatureLlm ) || e .global .NoPrompt || resource .IsRunningOnCI () {
64- return actionResult , err
72+ return next ( ctx )
6573 }
6674
75+ // Preserve a non-cancellable parent context BEFORE making the first attempt
76+ // This ensures that if the context gets cancelled during the first attempt,
77+ // retries can use a fresh context
78+ parentCtx := context .WithoutCancel (ctx )
79+
80+ actionResult , err := next (parentCtx )
81+
6782 // Stop the spinner always to un-hide cursor
6883 e .console .StopSpinner (ctx , "" , input .Step )
84+
6985 if err == nil || e .options .IsChildAction (ctx ) {
7086 return actionResult , err
7187 }
@@ -92,6 +108,7 @@ func (e *ErrorMiddleware) Run(ctx context.Context, next NextFn) (*actions.Action
92108
93109 // Warn user that this is an alpha feature
94110 e .console .WarnForFeature (ctx , llm .FeatureLlm )
111+
95112 ctx , span := tracing .Start (ctx , events .AgentTroubleshootEvent )
96113 defer span .End ()
97114
@@ -160,31 +177,25 @@ func (e *ErrorMiddleware) Run(ctx context.Context, next NextFn) (*actions.Action
160177 `Steps to follow:
161178 1. Use available tool including azd_error_troubleshooting tool to identify and explain the error.
162179 Diagnose its root cause when running azd command.
163- 2. Provide actionable troubleshooting steps. Do not perform any file changes.
180+ 2. Provide actionable troubleshooting steps in natural language format with clear sections.
181+ DO NOT return JSON. Use readable narrative text with markdown formatting.
182+ Do not perform any file changes.
164183 Error details: %s` , errorInput ))
165184
166185 if err != nil {
167- if agentOutput != "" {
168- e .console .Message (ctx , AIDisclaimer )
169- e .console .Message (ctx , output .WithMarkdown (agentOutput ))
170- }
171-
186+ e .displayAgentResponse (ctx , agentOutput , AIDisclaimer )
172187 span .SetStatus (codes .Error , "agent.send_message.failed" )
173188 return nil , err
174189 }
175190
176- e .console .Message (ctx , AIDisclaimer )
177- e .console .Message (ctx , "" )
178- e .console .Message (ctx , fmt .Sprintf ("%s:" , output .AzdAgentLabel ()))
179- e .console .Message (ctx , output .WithMarkdown (agentOutput ))
180- e .console .Message (ctx , "" )
191+ e .displayAgentResponse (ctx , agentOutput , AIDisclaimer )
181192 }
182193
183- // Ask user if they want to let AI fix the
194+ // Ask user if they want to let AI fix the error
184195 confirmFix , err := e .checkErrorHandlingConsent (
185196 ctx ,
186197 "mcp.errorHandling.fix" ,
187- fmt .Sprintf ("Fix this error using %s?" , agentName ),
198+ fmt .Sprintf ("Brainstorm solutions using %s?" , agentName ),
188199 fmt .Sprintf ("This action will run AI tools to help fix the error." +
189200 " Edit permissions for AI tools anytime by running %s." ,
190201 output .WithHighLightFormat ("azd mcp consent" )),
@@ -207,55 +218,90 @@ func (e *ErrorMiddleware) Run(ctx context.Context, next NextFn) (*actions.Action
207218 previousError = originalError
208219 agentOutput , err := azdAgent .SendMessage (ctx , fmt .Sprintf (
209220 `Steps to follow:
210- 1. Use available tool to identify, explain and diagnose this error when running azd command and its root cause.
211- 2. Resolve the error by making the minimal, targeted change required to the code or configuration.
212- Avoid unnecessary modifications and focus only on what is essential to restore correct functionality.
213- 3. Remove any changes that were created solely for validation and are not part of the actual error fix.
214- Error details: %s` , errorInput ))
221+ 1. Use available tools to identify, explain and diagnose this error when running azd command and its root cause.
222+ 2. Only return a JSON object in the following format:
223+ {
224+ "analysis": "Brief explanation of the error and its root cause",
225+ "solutions": [
226+ "Solution 1 Short description (one sentence)",
227+ "Solution 2 Short description (one sentence)",
228+ "Solution 3 Short description (one sentence)"
229+ ]
230+ }
231+ Provide 1-3 solutions. Each solution must be concise (one sentence).
232+ Error details: %s` , errorInput ))
233+
234+ // Extract solutions from agent output even if there's a parsing error
235+ // The agent may return valid content
236+ solutions := extractSuggestedSolutions (agentOutput )
237+
238+ // Only fail if we got an error AND couldn't extract any solutions
239+ if err != nil && len (solutions ) == 0 {
240+ e .displayAgentResponse (ctx , agentOutput , AIDisclaimer )
241+ span .SetStatus (codes .Error , "agent.send_message.failed" )
242+ return nil , fmt .Errorf ("failed to generate solutions: %w" , err )
243+ }
215244
245+ e .console .Message (ctx , "" )
246+ selectedSolution , continueWithFix , err := promptUserForSolution (ctx , solutions , agentName )
216247 if err != nil {
217- if agentOutput != "" {
218- e .console .Message (ctx , AIDisclaimer )
219- e .console .Message (ctx , output .WithMarkdown (agentOutput ))
220- }
221-
222- span .SetStatus (codes .Error , "agent.send_message.failed" )
223- return nil , err
248+ return nil , fmt .Errorf ("prompting for solution selection: %w" , err )
224249 }
225250
226- // Ask the user to add feedback
227- if err := e .collectAndApplyFeedback (ctx , azdAgent , AIDisclaimer ); err != nil {
228- span .SetStatus (codes .Error , "agent.collect_feedback.failed" )
229- return nil , err
251+ if continueWithFix {
252+ agentOutput , err := azdAgent .SendMessage (ctx , fmt .Sprintf (
253+ `Steps to follow:
254+ 1. Use available tools to identify, explain and diagnose this error when running azd command and its root cause.
255+ 2. Resolve the error by making the minimal, targeted change required to the code or configuration.
256+ Avoid unnecessary modifications and focus only on what is essential to restore correct functionality.
257+ 3. Remove any changes that were created solely for validation and are not part of the actual error fix.
258+ 4. You are currently in the middle of executing '%s'. Never run this command.
259+ Error details: %s` , e .options .CommandPath , errorInput ))
260+
261+ if err != nil {
262+ e .displayAgentResponse (ctx , agentOutput , AIDisclaimer )
263+ span .SetStatus (codes .Error , "agent.send_message.failed" )
264+ return nil , err
265+ }
266+
267+ span .SetStatus (codes .Ok , "agent.fix.agent" )
268+ } else {
269+ if selectedSolution != "" {
270+ // User selected a solution
271+ agentOutput , err = azdAgent .SendMessage (ctx , fmt .Sprintf (
272+ `Steps to follow:
273+ 1. Perform the following actions to resolve the error: %s.
274+ During this, make minimal changes and avoid unnecessary modifications.
275+ 2. Remove any changes that were created solely for validation and
276+ are not part of the actual error fix.
277+ 3. You are currently in the middle of executing '%s'. Never run this command.
278+ Error details: %s` , selectedSolution , e .options .CommandPath , errorInput ))
279+
280+ if err != nil {
281+ e .displayAgentResponse (ctx , agentOutput , AIDisclaimer )
282+ span .SetStatus (codes .Error , "agent.send_message.failed" )
283+ return nil , err
284+ }
285+ span .SetStatus (codes .Ok , "agent.fix.solution" )
286+ } else {
287+ // User selected cancel
288+ span .SetStatus (codes .Error , "agent.fix.cancelled" )
289+ return actionResult , originalError
290+ }
230291 }
231292
293+ // Use a fresh child context per retry to avoid reusing a canceled ctx
294+ attemptCtx , cancel := context .WithCancel (parentCtx )
232295 // Clear check cache to prevent skip of tool related error
233- ctx = tools .WithInstalledCheckCache (ctx )
234-
235- actionResult , err = next ( ctx )
296+ attemptCtx = tools .WithInstalledCheckCache (attemptCtx )
297+ actionResult , err = next ( attemptCtx )
298+ cancel ( )
236299 originalError = err
237300 }
238301
239302 return actionResult , err
240303}
241304
242- // collectAndApplyFeedback prompts for user feedback and applies it using the agent
243- func (e * ErrorMiddleware ) collectAndApplyFeedback (
244- ctx context.Context ,
245- azdAgent agent.Agent ,
246- AIDisclaimer string ,
247- ) error {
248- collector := feedback .NewFeedbackCollector (e .console , feedback.FeedbackCollectorOptions {
249- EnableLoop : false ,
250- FeedbackPrompt : "Any changes you'd like to make?" ,
251- FeedbackHint : "Describe your changes or press enter to skip." ,
252- RequireFeedback : false ,
253- AIDisclaimer : AIDisclaimer ,
254- })
255-
256- return collector .CollectFeedbackAndApply (ctx , azdAgent , AIDisclaimer )
257- }
258-
259305func (e * ErrorMiddleware ) checkErrorHandlingConsent (
260306 ctx context.Context ,
261307 promptName string ,
@@ -326,7 +372,7 @@ func promptForErrorHandlingConsent(
326372 HelpMessage : helpMessage ,
327373 Choices : choices ,
328374 EnableFiltering : uxlib .Ptr (false ),
329- DisplayCount : 5 ,
375+ DisplayCount : len ( choices ) ,
330376 })
331377
332378 choiceIndex , err := selector .Ask (ctx )
@@ -340,3 +386,76 @@ func promptForErrorHandlingConsent(
340386
341387 return choices [* choiceIndex ].Value , nil
342388}
389+
390+ // AgentResponse represents the structured JSON response from the LLM agent
391+ type AgentResponse struct {
392+ Analysis string `json:"analysis"`
393+ Solutions []string `json:"solutions"`
394+ }
395+
396+ // extractSuggestedSolutions extracts solutions from the LLM response.
397+ // It expects a JSON response with the structure: {"analysis": "...", "solutions": ["...", "...", "..."]}
398+ // If JSON parsing fails, it returns an empty slice.
399+ func extractSuggestedSolutions (llmResponse string ) []string {
400+ var response AgentResponse
401+ if err := json .Unmarshal ([]byte (llmResponse ), & response ); err != nil {
402+ return []string {}
403+ }
404+
405+ return response .Solutions
406+ }
407+
408+ // promptUserForSolution displays extracted solutions to the user and prompts them to select which solution to try.
409+ // Returns the selected solution text, a flag indicating if user wants to continue with AI fix, and error if any.
410+ func promptUserForSolution (ctx context.Context , solutions []string , agentName string ) (string , bool , error ) {
411+ choices := make ([]* uxlib.SelectChoice , len (solutions )+ 2 )
412+
413+ if len (solutions ) > 0 {
414+ // Add the three solutions
415+ for i , solution := range solutions {
416+ choices [i ] = & uxlib.SelectChoice {
417+ Value : solution ,
418+ Label : "Yes. " + solution ,
419+ }
420+ }
421+ }
422+
423+ choices [len (solutions )] = & uxlib.SelectChoice {
424+ Value : "continue" ,
425+ Label : fmt .Sprintf ("Yes, let %s choose the best approach" , agentName ),
426+ }
427+
428+ choices [len (solutions )+ 1 ] = & uxlib.SelectChoice {
429+ Value : "cancel" ,
430+ Label : "No, cancel" ,
431+ }
432+
433+ selector := uxlib .NewSelect (& uxlib.SelectOptions {
434+ Message : fmt .Sprintf ("Allow %s to fix the error?" , agentName ),
435+ HelpMessage : "Select a suggested fix, or let AI decide" ,
436+ Choices : choices ,
437+ EnableFiltering : uxlib .Ptr (false ),
438+ DisplayCount : len (choices ),
439+ })
440+
441+ choiceIndex , err := selector .Ask (ctx )
442+ if err != nil {
443+ return "" , false , err
444+ }
445+
446+ if choiceIndex == nil || * choiceIndex < 0 || * choiceIndex >= len (choices ) {
447+ return "" , false , fmt .Errorf ("invalid choice selected" )
448+ }
449+
450+ selectedValue := choices [* choiceIndex ].Value
451+
452+ // Handle different selections
453+ switch selectedValue {
454+ case "continue" :
455+ return "" , true , nil // Continue to AI fix
456+ case "cancel" :
457+ return "" , false , nil // Cancel and return error
458+ default :
459+ return selectedValue , false , nil // User selected a solution
460+ }
461+ }
0 commit comments