package loop import ( "context" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // injectMemory retrieves the persona's relevant long-term memories and prepends // them as a leading system message, so the model can ground its answer in recalled // context (design §6 / §2). It is re-run every round via assembleContext so the // retrieval reflects the latest conversation rather than a first-turn snapshot. It // returns the input messages unchanged when no retriever/persona is configured, // retrieval fails, or nothing relevant is found. func (l *Loop) injectMemory(ctx context.Context, messages []model.Message) []model.Message { if l.retriever == nil || l.personaID == "" { return messages } block, err := l.retriever.Inject(ctx, l.personaID, queryFromMessages(messages), 0) if err != nil { l.log.Warn("memory inject", "error", err) return messages } if block == "" { return messages } out := make([]model.Message, 0, len(messages)+1) out = append(out, model.TextMessage(model.RoleSystem, block)) out = append(out, messages...) return out } // assembleContext builds the model-visible message list for one round (design // §1.3 每轮 assemble, the loop's equivalent of OpenClaw's transformContext): it // re-injects the persona's relevant long-term memory — retrieval is re-run every // round from the current conversation, not cached from the first turn, so it // supports cross-round refresh — folds in the per-session working-memory summary // when present, and applies the optional TransformContext hook last so a caller // can project/filter what the model actually sees. It returns the input unchanged // when nothing is configured. func (l *Loop) assembleContext(ctx context.Context, messages []model.Message, round int) []model.Message { var leading []model.Message if l.working != nil { if s := l.working.Summarize(); s != "" { leading = append(leading, model.TextMessage(model.RoleSystem, "工作记忆: "+s)) } } leading = append(leading, l.injectMemory(ctx, messages)...) out := leading if l.transformContext != nil { out = l.transformContext(ctx, leading, round) } return out } // compactIfNeeded compacts the conversation when its estimated context exceeds // the configured MaxContextTokens. It is a no-op when compaction is disabled // (non-positive MaxContextTokens), the compactor is nil, the estimate is within // budget, or the compactor returns an error — a compaction failure is logged and // the conversation is kept as-is rather than failing the run. func (l *Loop) compactIfNeeded(ctx context.Context, messages []model.Message) []model.Message { if l.maxContextTokens <= 0 || l.compactor == nil { return messages } compacted, err := l.compactor.Compact(ctx, messages, l.maxContextTokens) if err != nil { l.log.Warn("context compact", "error", err, "tokens", l.maxContextTokens) return messages } if compacted == nil { return messages } return compacted } // extractMemory runs the extraction hook (design §6) over the current // conversation and persists any derived items. Errors are logged, never fatal, so // a bad extraction cannot abort the run. func (l *Loop) extractMemory(ctx context.Context, messages []model.Message, step int) { if l.extractor == nil || l.memory == nil { return } items, err := l.extractor.Extract(ctx, messages) if err != nil { l.log.Warn("memory extract", "step", step, "error", err) return } for _, it := range items { if it.PersonaID == "" && l.personaID != "" { it.PersonaID = l.personaID } if _, err := l.memory.Add(ctx, it); err != nil { l.log.Warn("memory store", "step", step, "error", err) } } } // queryFromMessages derives a retrieval query from the conversation: the last // user turn's text, falling back to the last message's text, then "". func queryFromMessages(messages []model.Message) string { for i := len(messages) - 1; i >= 0; i-- { if messages[i].Role == model.RoleUser { return messageText(messages[i]) } } if n := len(messages); n > 0 { return messageText(messages[n-1]) } return "" } // messageText returns the concatenated plain-text payload of a message's text // parts, which is the shared view used to build a retrieval query. func messageText(m model.Message) string { var b strings.Builder for _, part := range m.Content { if part.Type != model.ContentTypeText { continue } if b.Len() > 0 { b.WriteByte(' ') } b.WriteString(part.Text) } return b.String() }