runPlanFirstIfApplicable checks the quality config and the one-shot plan flag, then optionally runs a structured Plan-and-Solve cycle. All exits are silent (the only side effect is appending to history) because Plan-First is meant to be a behind-the-scenes accelerator, not a UI feature. /config qua
(ctx context.Context, userQuery string)
| 41 | // The plan is rendered for the user and the ReAct loop is skipped (see |
| 42 | // planDryRunHandled). |
| 43 | func (a *AgentMode) runPlanFirstIfApplicable(ctx context.Context, userQuery string) { |
| 44 | if a.agentDispatcher == nil || a.agentRegistry == nil { |
| 45 | return |
| 46 | } |
| 47 | |
| 48 | // Safety: any return path below must leave the spinner off, otherwise |
| 49 | // the /r/033[K repaint can overlap a downstream security approval |
| 50 | // prompt and swallow the user's response. |
| 51 | defer a.cli.animation.StopThinkingAnimation() |
| 52 | |
| 53 | // One-shot triggers from /plan beat the config; clear them after read |
| 54 | // so a subsequent /agent invocation behaves normally. |
| 55 | forced := a.cli.pendingPlanFirst |
| 56 | dryRun := a.cli.pendingPlanDryRun |
| 57 | a.cli.pendingPlanFirst = false |
| 58 | a.cli.pendingPlanDryRun = false |
| 59 | |
| 60 | if !forced && !dryRun && !quality.ShouldPlanFirst(a.qualityConfig.PlanFirst, userQuery) { |
| 61 | return |
| 62 | } |
| 63 | |
| 64 | planner, ok := a.agentRegistry.Get(workers.AgentTypePlanner) |
| 65 | if !ok { |
| 66 | a.logger.Warn("Plan-First skipped: planner agent not registered") |
| 67 | return |
| 68 | } |
| 69 | _ = planner // signature contract; dispatcher resolves the agent again |
| 70 | |
| 71 | a.logger.Info("Plan-First triggered", |
| 72 | zap.Bool("forced", forced), |
| 73 | zap.Bool("dry_run", dryRun), |
| 74 | zap.String("mode", a.qualityConfig.PlanFirst.Mode), |
| 75 | zap.Int("complexity", quality.ComplexityScore(userQuery))) |
| 76 | |
| 77 | // Step 1: ask the planner for a structured JSON plan via the |
| 78 | // dispatcher so model routing, effort hints, policy and reasoning |
| 79 | // auto-enable all fire correctly. |
| 80 | plannerCall := workers.AgentCall{ |
| 81 | Agent: workers.AgentTypePlanner, |
| 82 | Task: workers.PlannerStructuredOutputDirective + "\n" + userQuery, |
| 83 | ID: "plan-first", |
| 84 | } |
| 85 | a.cli.animation.ShowThinkingAnimation(i18n.T("plan_first.spinner_planning")) |
| 86 | planResults := a.agentDispatcher.Dispatch(ctx, []workers.AgentCall{plannerCall}) |
| 87 | a.cli.animation.StopThinkingAnimation() |
| 88 | if len(planResults) == 0 || planResults[0].Error != nil { |
| 89 | var errMsg string |
| 90 | if len(planResults) > 0 { |
| 91 | errMsg = planResults[0].Error.Error() |
| 92 | } |
| 93 | a.logger.Warn("Plan-First aborted: planner call failed", |
| 94 | zap.String("error", errMsg)) |
| 95 | return |
| 96 | } |
| 97 | |
| 98 | // Dry-run branch: render the plan to the user and stop. No execution, |
| 99 | // no orchestrator hand-off. The caller (Run) will skip the ReAct loop |
| 100 | // when planDryRunHandled is set. |
no test coverage detected