2026-11-14 01:14:08 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-11-14 14:28:08 +00:00
|
|
|
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)) {
|
2026-11-14 01:14:08 +00:00
|
|
|
for current := e; current != nil; current = current.parent {
|
|
|
|
|
start := current.count - len(current.messages)
|
2026-11-14 14:28:08 +00:00
|
|
|
visit(start, current.messages)
|
2026-11-14 01:14:08 +00:00
|
|
|
}
|
|
|
|
|
}
|