diff --git a/agent/harness/loop/loop.go b/agent/harness/loop/loop.go index 3c46446a..e2aa03f9 100644 --- a/agent/harness/loop/loop.go +++ b/agent/harness/loop/loop.go @@ -281,12 +281,17 @@ func evaluate(ctx context.Context, evaluators []Evaluator, loopCtx *Context) (Ev } func nextMessages(cfg Config, loopCtx *Context, evaluation Evaluation) (messages []*message.Message, surfaced []*message.Message) { - if len(evaluation.Messages) > 0 { - cloned := cloneMessages(evaluation.Messages) - return cloned, cloned - } if cfg.FreshContextPerIteration { + // Fresh mode always restarts from the original input (the session is also + // reset to a pristine snapshot). Explicit ContinueWithMessages compose + // with that fresh context rather than replacing it, so the original input + // is not lost. nextMessages := cloneMessages(loopCtx.InitialMessages) + if len(evaluation.Messages) > 0 { + explicit := cloneMessages(evaluation.Messages) + nextMessages = append(nextMessages, explicit...) + return nextMessages, explicit + } feedbackMessage := aggregatedFeedbackMessage(loopCtx.Feedback, cfg.OnBehalfOfAuthorName) if feedbackMessage == nil { return nextMessages, nil @@ -294,6 +299,10 @@ func nextMessages(cfg Config, loopCtx *Context, evaluation Evaluation) (messages nextMessages = append(nextMessages, feedbackMessage) return nextMessages, []*message.Message{feedbackMessage} } + if len(evaluation.Messages) > 0 { + cloned := cloneMessages(evaluation.Messages) + return cloned, cloned + } if evaluation.Feedback == "" { return nil, nil } diff --git a/agent/harness/loop/loop_test.go b/agent/harness/loop/loop_test.go index 119dab69..1aa8488d 100644 --- a/agent/harness/loop/loop_test.go +++ b/agent/harness/loop/loop_test.go @@ -559,3 +559,32 @@ func cloneMessages(messages []*message.Message) []*message.Message { } return out } + +func TestLoop_FreshContextPerIteration_ContinueWithMessagesKeepsInitial(t *testing.T) { + capture := newCaptureAgent(func(int, []*message.Message) []*agent.ResponseUpdate { + return textUpdates("ack") + }) + a := agent.New(capture.provider(), agent.Config{ + Middlewares: []agent.Middleware{loop.New(loop.Config{ + FreshContextPerIteration: true, + Evaluators: []loop.Evaluator{loop.EvaluatorFunc(func(_ context.Context, ctx *loop.Context) (loop.Evaluation, error) { + if ctx.Iteration == 1 { + return loop.ContinueWithMessages([]*message.Message{message.NewText("explicit")}), nil + } + return loop.Stop(), nil + })}, + })}, + }) + + if _, err := a.RunText(context.Background(), "original").Collect(); err != nil { + t.Fatal(err) + } + secondCall := messageTexts(capture.messagesPerCall[1]) + // Fresh mode must re-seed the original input; explicit messages compose with it. + if len(secondCall) == 0 || secondCall[0] != "original" { + t.Fatalf("second call = %v, want the original input preserved in fresh mode", secondCall) + } + if !slices.Contains(secondCall, "explicit") { + t.Fatalf("second call = %v, want the explicit message included", secondCall) + } +}