feat: inject plan-mode reminders during tool-call iterations
Add buildPlanReminder to emit interview/review workflow reminders each iteration (after the first), preventing the LLM from losing focus during tool-call loops. Remove the hard interviewMaxIter cap in favour of the softer reminder approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
adb9fa5c1b
commit
3f285b0d4b
2 changed files with 63 additions and 8 deletions
|
|
@ -666,21 +666,32 @@ func buildTaskReminder(userMessage string, lastBlocker string) providers.Message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildPlanReminder returns a reminder message for plan pre-execution states
|
||||||
|
// (interviewing / review) to keep the AI focused on the interview workflow
|
||||||
|
// during tool-call iterations.
|
||||||
|
func buildPlanReminder(planStatus string) (providers.Message, bool) {
|
||||||
|
var content string
|
||||||
|
switch planStatus {
|
||||||
|
case "interviewing":
|
||||||
|
content = "[System] You are interviewing the user to build a plan. " +
|
||||||
|
"Ask clarifying questions and save findings to ## Context in memory/MEMORY.md using edit_file. " +
|
||||||
|
"When you have enough information, write ## Phases and ## Commands sections."
|
||||||
|
case "review":
|
||||||
|
content = "[System] The plan is under review. " +
|
||||||
|
"Wait for the user to approve or request changes. Do not proceed with execution."
|
||||||
|
default:
|
||||||
|
return providers.Message{}, false
|
||||||
|
}
|
||||||
|
return providers.Message{Role: "user", Content: content}, true
|
||||||
|
}
|
||||||
|
|
||||||
// runLLMIteration executes the LLM call loop with tool handling.
|
// runLLMIteration executes the LLM call loop with tool handling.
|
||||||
func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions) (string, int, error) {
|
func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance, messages []providers.Message, opts processOptions) (string, int, error) {
|
||||||
iteration := 0
|
iteration := 0
|
||||||
var finalContent string
|
var finalContent string
|
||||||
lastReminderIdx := -1
|
lastReminderIdx := -1
|
||||||
|
|
||||||
// During pre-execution plan modes (interviewing/review), cap iterations
|
|
||||||
// to prevent runaway tool loops.
|
|
||||||
maxIter := agent.MaxIterations
|
maxIter := agent.MaxIterations
|
||||||
if isPlanPreExecution(agent.ContextBuilder.GetPlanStatus()) {
|
|
||||||
const interviewMaxIter = 3
|
|
||||||
if maxIter > interviewMaxIter {
|
|
||||||
maxIter = interviewMaxIter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for iteration < maxIter {
|
for iteration < maxIter {
|
||||||
iteration++
|
iteration++
|
||||||
|
|
@ -953,6 +964,19 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
"has_blocker": lastBlocker != "",
|
"has_blocker": lastBlocker != "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject plan-mode reminder to keep AI focused on interview/review workflow.
|
||||||
|
if iteration > 1 && isPlanPreExecution(agent.ContextBuilder.GetPlanStatus()) {
|
||||||
|
if reminder, ok := buildPlanReminder(agent.ContextBuilder.GetPlanStatus()); ok {
|
||||||
|
messages = append(messages, reminder)
|
||||||
|
logger.DebugCF("agent", "Injected plan reminder",
|
||||||
|
map[string]interface{}{
|
||||||
|
"agent_id": agent.ID,
|
||||||
|
"iteration": iteration,
|
||||||
|
"plan_status": agent.ContextBuilder.GetPlanStatus(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If max iterations exhausted with tool calls still pending,
|
// If max iterations exhausted with tool calls still pending,
|
||||||
|
|
|
||||||
|
|
@ -876,6 +876,37 @@ func TestBuildTaskReminder_Truncation(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildPlanReminder(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status string
|
||||||
|
wantOK bool
|
||||||
|
wantSubstr string
|
||||||
|
}{
|
||||||
|
{"interviewing", "interviewing", true, "interviewing the user"},
|
||||||
|
{"review", "review", true, "under review"},
|
||||||
|
{"executing returns false", "executing", false, ""},
|
||||||
|
{"empty returns false", "", false, ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msg, ok := buildPlanReminder(tt.status)
|
||||||
|
if ok != tt.wantOK {
|
||||||
|
t.Fatalf("buildPlanReminder(%q) ok = %v, want %v", tt.status, ok, tt.wantOK)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg.Role != "user" {
|
||||||
|
t.Errorf("expected role 'user', got %q", msg.Role)
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg.Content, tt.wantSubstr) {
|
||||||
|
t.Errorf("expected content to contain %q, got %q", tt.wantSubstr, msg.Content)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- /plan command tests ----------
|
// ---------- /plan command tests ----------
|
||||||
|
|
||||||
func newTestAgentLoop(t *testing.T) (*AgentLoop, func()) {
|
func newTestAgentLoop(t *testing.T) (*AgentLoop, func()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue