Avoid intermediate conversation history copies

This commit is contained in:
2026-11-14 14:28:08 +00:00
parent c23393b2cc
commit 64cf84c9b5
6 changed files with 72 additions and 11 deletions

View File

@@ -51,10 +51,42 @@ func (e *Exchange) Messages() []RenderedMessage {
if e == nil {
return nil
}
messages := make([]RenderedMessage, e.count)
for current := e; current != nil; current = current.parent {
start := current.count - len(current.messages)
copyRenderedMessages(messages[start:current.count], current.messages)
return e.MessagesWithSuffix()
}
// MessagesWithSuffix returns independently owned history followed by each part.
// A nil exchange supplies no history.
func (e *Exchange) MessagesWithSuffix(parts ...[]RenderedMessage) []RenderedMessage {
count := e.MessageCount()
for _, part := range parts {
count += len(part)
}
messages := make([]RenderedMessage, count)
e.visitChunks(func(start int, chunk []RenderedMessage) {
copyRenderedMessages(messages[start:], chunk)
})
offset := e.MessageCount()
for _, part := range parts {
copyRenderedMessages(messages[offset:], part)
offset += len(part)
}
return messages
}
// VisitMessages visits indexed history entries without materializing a slice.
// Entries and their cache metadata are borrowed read-only values. Visits need
// not be in index order; callers can fill a destination using the supplied index.
func (e *Exchange) VisitMessages(visit func(int, RenderedMessage)) {
e.visitChunks(func(start int, chunk []RenderedMessage) {
for index, message := range chunk {
visit(start+index, message)
}
})
}
func (e *Exchange) visitChunks(visit func(int, []RenderedMessage)) {
for current := e; current != nil; current = current.parent {
start := current.count - len(current.messages)
visit(start, current.messages)
}
}

View File

@@ -51,3 +51,25 @@ func TestExchangeManyTurns(t *testing.T) {
}
}
}
func TestExchangeMessagesWithSuffix(t *testing.T) {
for _, history := range []*Exchange{nil, CaptureExchange(nil, "source", nil, "answer")} {
control := &CacheControl{Type: CacheControlEphemeral, TTL: "1h"}
first := []RenderedMessage{{Role: RoleUser, Content: "new", CacheControl: control}}
last := []RenderedMessage{{Role: RoleDeveloper, Content: "suffix"}}
got := history.MessagesWithSuffix(first, nil, last)
want := append(history.Messages(), first...)
want = append(want, last...)
if !reflect.DeepEqual(got, want) {
t.Fatalf("messages = %#v, want %#v", got, want)
}
got[history.MessageCount()].CacheControl.TTL = "changed"
got[history.MessageCount()].Content = "changed"
if first[0].Content != "new" || control.TTL != "1h" {
t.Fatal("assembled messages alias the caller suffix")
}
if history != nil && history.Messages()[0].Content != "answer" {
t.Fatal("assembly changed retained history")
}
}
}

View File

@@ -482,10 +482,7 @@ func (r *Runner) completePreparationWithStructuredOutput(
if state.directSessionID != "" {
effectivePrompt.SessionID = state.directSessionID
}
effectivePrompt.Messages = domain.ConcatRenderedMessages(renderedPrompt.Messages, req.AppendedMessages)
if req.ContinueFrom != nil {
effectivePrompt.Messages = domain.ConcatRenderedMessages(req.ContinueFrom.Messages(), effectivePrompt.Messages)
}
effectivePrompt.Messages = req.ContinueFrom.MessagesWithSuffix(renderedPrompt.Messages, req.AppendedMessages)
end := time.Now().UTC()
effectiveModel := state.effectiveModel