diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 67056def3..acd3a630c 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -396,6 +396,11 @@ func (cb *ContextBuilder) AdvancePhase() error { return cb.memory.AdvancePhase() } +// SetCurrentPhase sets the current phase number to n. +func (cb *ContextBuilder) SetCurrentPhase(n int) error { + return cb.memory.SetPhase(n) +} + // GetCurrentPhase returns the current phase number. func (cb *ContextBuilder) GetCurrentPhase() int { return cb.memory.GetCurrentPhase() diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 673f91aa1..25f7273c3 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -833,7 +833,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt // 5a. Auto-advance plan phases after LLM iteration postStatus := agent.ContextBuilder.GetPlanStatus() - if agent.ContextBuilder.HasActivePlan() && (postStatus == "executing" || postStatus == "review") { + if agent.ContextBuilder.HasActivePlan() && (postStatus == "executing" || postStatus == "review" || postStatus == "completed") { // Intercept: if AI changed status to executing or review without user approval // (from interviewing or review), validate and hold at "review". if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") { @@ -862,14 +862,19 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined", map[string]interface{}{"agent_id": agent.ID}) } else if agent.ContextBuilder.IsPlanComplete() { - _ = agent.ContextBuilder.ClearMemory() - if !constants.IsInternalChannel(opts.Channel) { - al.bus.PublishOutbound(bus.OutboundMessage{ - Channel: opts.Channel, - ChatID: opts.ChatID, - Content: "\u2705 Plan completed!", - SkipPlaceholder: true, - }) + // Mark plan as completed (keep memory for review; user can /plan clear) + total := agent.ContextBuilder.GetTotalPhases() + _ = agent.ContextBuilder.SetCurrentPhase(total) + if preStatus != "completed" { + _ = agent.ContextBuilder.SetPlanStatus("completed") + if !constants.IsInternalChannel(opts.Channel) { + al.bus.PublishOutbound(bus.OutboundMessage{ + Channel: opts.Channel, + ChatID: opts.ChatID, + Content: "\u2705 Plan completed!", + SkipPlaceholder: true, + }) + } } } else if agent.ContextBuilder.IsCurrentPhaseComplete() { prev := agent.ContextBuilder.GetCurrentPhase() diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 0e42b98ad..43b46e84d 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -1317,7 +1317,7 @@ Test } } -// TestAutoCompleteClears verifies that plan is cleared when all phases are complete. +// TestAutoCompleteClears verifies that plan is marked completed with correct phase when all phases are complete. func TestAutoCompleteClears(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-auto-complete-*") if err != nil { @@ -1369,9 +1369,15 @@ Test t.Fatalf("ProcessDirectWithChannel failed: %v", err) } - // Plan should be cleared - if agent.ContextBuilder.HasActivePlan() { - t.Error("expected plan to be cleared after completion") + // Plan should be kept with status "completed" and phase set to total + if !agent.ContextBuilder.HasActivePlan() { + t.Error("expected plan to be retained after completion") + } + if status := agent.ContextBuilder.GetPlanStatus(); status != "completed" { + t.Errorf("expected plan status 'completed', got %q", status) + } + if phase := agent.ContextBuilder.GetCurrentPhase(); phase != 1 { + t.Errorf("expected phase 1 (total phases), got %d", phase) } } diff --git a/pkg/agent/memory.go b/pkg/agent/memory.go index 4cb825898..8ad8b7502 100644 --- a/pkg/agent/memory.go +++ b/pkg/agent/memory.go @@ -335,6 +335,17 @@ func (ms *MemoryStore) AdvancePhase() error { return ms.WriteLongTerm(content) } +// SetPhase sets the current phase number to n. +func (ms *MemoryStore) SetPhase(n int) error { + content := ms.ReadLongTerm() + m := rePhase.FindString(content) + if m == "" { + return fmt.Errorf("no phase marker found") + } + content = strings.Replace(content, m, fmt.Sprintf("> Phase: %d", n), 1) + return ms.WriteLongTerm(content) +} + // MarkStep marks the nth step (1-based) in the given phase as done [x]. func (ms *MemoryStore) MarkStep(phase, step int) error { content := ms.ReadLongTerm()