From c124a91dbeea2b5c2fd80d90439afe86c3e2c497 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sun, 13 Sep 2026 18:41:06 +0530 Subject: [PATCH] Keep the original input under FreshContextPerIteration with ContinueWithMessages nextMessages returned the evaluator's explicit ContinueWithMessages messages before the FreshContextPerIteration branch, so in fresh mode the reinvocation received only the explicit messages. FreshContextPerIteration also resets the session to a pristine snapshot taken before iteration 1 (which does not contain the original request), so the original user input was lost from both the message list and the session - contradicting the documented contract that fresh mode 'restarts each reinvocation from the original input messages'. In fresh mode, always re-seed InitialMessages and compose the explicit messages on top; non-fresh behavior (explicit messages verbatim) is unchanged. --- agent/harness/loop/loop.go | 17 +++++++++++++---- agent/harness/loop/loop_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) 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) + } +}