fix(plan): auto-advance to next phase when current phase completes

During plan execution, completing the last step of a phase would call
AdvancePhase() but the LLM loop had already returned, so work on the
next phase never started. Wrap runLLMIteration + phase checks in a for
loop that rebuilds the system prompt and re-enters the LLM iteration
when a phase boundary is crossed. Capped at 10 transitions as a safety
guard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-25 16:31:25 +09:00
parent fcaea778cd
commit b31e76bcd0

View file

@ -1069,16 +1069,45 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
al.promptDirty.Store(false) al.promptDirty.Store(false)
} }
// 5. Run LLM iteration loop // 5. Run LLM iteration loop (with automatic phase transitions)
finalContent, iteration, err := al.runLLMIteration(ctx, agent, messages, opts, task, preStatus) var finalContent string
if err != nil { var iteration int
return "", err const maxPhaseTransitions = 10
}
for phaseLoop := 0; ; phaseLoop++ {
// On phase transition: rebuild system prompt with new phase context + nudge
if phaseLoop > 0 {
messages = agent.ContextBuilder.BuildMessages(
agent.Sessions.GetHistory(opts.SessionKey),
agent.Sessions.GetSummary(opts.SessionKey),
"", nil, opts.Channel, opts.ChatID,
)
messages = append(messages, providers.Message{
Role: "user",
Content: fmt.Sprintf("[System] Phase %d is now active. Continue working on the next steps.", agent.ContextBuilder.GetCurrentPhase()),
})
if len(messages) > 0 {
al.lastSystemPrompt.Store(messages[0].Content)
}
}
curPlanStatus := preStatus
if phaseLoop > 0 {
curPlanStatus = agent.ContextBuilder.GetPlanStatus()
}
var err error
finalContent, iteration, err = al.runLLMIteration(ctx, agent, messages, opts, task, curPlanStatus)
if err != nil {
return "", err
}
// 5a. Auto-advance plan phases after LLM iteration
postStatus := agent.ContextBuilder.GetPlanStatus()
if !agent.ContextBuilder.HasActivePlan() || !(postStatus == "executing" || postStatus == "review" || postStatus == "completed") {
break
}
// 5a. Auto-advance plan phases after LLM iteration
postStatus := agent.ContextBuilder.GetPlanStatus()
if agent.ContextBuilder.HasActivePlan() &&
(postStatus == "executing" || postStatus == "review" || postStatus == "completed") {
// Intercept: if AI changed status to executing or review without user approval // Intercept: if AI changed status to executing or review without user approval
// (from interviewing or review), validate and hold at "review". // (from interviewing or review), validate and hold at "review".
if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") { if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") {
@ -1086,7 +1115,6 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
_ = agent.ContextBuilder.SetPlanStatus("interviewing") _ = agent.ContextBuilder.SetPlanStatus("interviewing")
logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(), logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(),
map[string]any{"agent_id": agent.ID}) map[string]any{"agent_id": agent.ID})
// Inject rejection into session history so LLM sees it next iteration
rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again." rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again."
agent.Sessions.AddMessage(opts.SessionKey, "user", rejectionMsg) agent.Sessions.AddMessage(opts.SessionKey, "user", rejectionMsg)
} else { } else {
@ -1102,13 +1130,17 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
}) })
} }
} }
} else if postStatus == "executing" && agent.ContextBuilder.GetTotalPhases() == 0 { break
// Safeguard: executing but no phases (shouldn't happen, but be safe). }
if postStatus == "executing" && agent.ContextBuilder.GetTotalPhases() == 0 {
_ = agent.ContextBuilder.SetPlanStatus("interviewing") _ = agent.ContextBuilder.SetPlanStatus("interviewing")
logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined", logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined",
map[string]any{"agent_id": agent.ID}) map[string]any{"agent_id": agent.ID})
} else if agent.ContextBuilder.IsPlanComplete() { break
// Mark plan as completed (keep memory for review; user can /plan clear) }
if agent.ContextBuilder.IsPlanComplete() {
total := agent.ContextBuilder.GetTotalPhases() total := agent.ContextBuilder.GetTotalPhases()
_ = agent.ContextBuilder.SetCurrentPhase(total) _ = agent.ContextBuilder.SetCurrentPhase(total)
if preStatus != "completed" { if preStatus != "completed" {
@ -1133,7 +1165,15 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
}) })
} }
} }
} else if agent.ContextBuilder.IsCurrentPhaseComplete() { break
}
if agent.ContextBuilder.IsCurrentPhaseComplete() {
if phaseLoop >= maxPhaseTransitions {
logger.WarnCF("agent", "Max phase transitions reached, stopping",
map[string]interface{}{"agent_id": agent.ID, "transitions": phaseLoop})
break
}
prev := agent.ContextBuilder.GetCurrentPhase() prev := agent.ContextBuilder.GetCurrentPhase()
_ = agent.ContextBuilder.AdvancePhase() _ = agent.ContextBuilder.AdvancePhase()
next := agent.ContextBuilder.GetCurrentPhase() next := agent.ContextBuilder.GetCurrentPhase()
@ -1145,7 +1185,11 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
SkipPlaceholder: true, SkipPlaceholder: true,
}) })
} }
al.notifyStateChange()
continue
} }
break
} }
al.notifyStateChange() al.notifyStateChange()