package domain // Exchange is an immutable logical conversation ending in an assistant message. type Exchange struct { parent *Exchange runID string parentRunID string messages []RenderedMessage count int } // CaptureExchange creates a new exchange without modifying its parent. func CaptureExchange(parent *Exchange, runID string, messages []RenderedMessage, output string) *Exchange { turn := make([]RenderedMessage, len(messages)+1) copyRenderedMessages(turn, messages) turn[len(messages)] = RenderedMessage{Role: RoleAssistant, Content: output} exchange := &Exchange{parent: parent, runID: runID, messages: turn, count: len(turn)} if parent != nil { exchange.parentRunID = parent.runID exchange.count += parent.count } return exchange } // RunID returns the identity of the run that produced this exchange. func (e *Exchange) RunID() string { if e == nil { return "" } return e.runID } // ParentRunID returns the immediate source run identity, if present. func (e *Exchange) ParentRunID() string { if e == nil { return "" } return e.parentRunID } // MessageCount returns the number of messages in the logical history. func (e *Exchange) MessageCount() int { if e == nil { return 0 } return e.count } // Messages returns a detached copy of the complete logical history. func (e *Exchange) Messages() []RenderedMessage { if e == nil { return nil } 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) } }