// // Condenser.swift // Poche // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // import Foundation import FoundationModels /// One exchange in the agent's private log, used for condensation and /// session recycling. struct AgentExchange: Sendable { enum Role: String, Sendable { case user case assistant case event } let role: Role let text: String } /// Summarizes old turns in a separate, tool-free session (CLAUDE.md §5). struct Condenser: Sendable { /// Each turn is clipped before summarizing: the condensation prompt must /// itself fit in a fresh window. private static let perTurnClip = 280 func condense(_ exchanges: [AgentExchange]) async throws -> String { let rendered = exchanges.map { exchange in "\(label(for: exchange.role)) : \(String(exchange.text.prefix(Self.perTurnClip)))" } .joined(separator: "\n") let session = LanguageModelSession( model: .default, instructions: "Tu résumes une conversation entre un utilisateur et son assistant. Sois dense, fidèle, factuel." ) let response = try await session.respond( to: "Résume fidèlement cette conversation :\n\n\(rendered)", generating: CondensedSummary.self ) return response.content.summary } /// Last-resort summary when the model itself cannot condense /// (e.g. the condensation call is refused). Losing detail is acceptable; /// visibly losing the thread is not. func fallbackSummary(for exchanges: [AgentExchange]) -> String { exchanges .filter { $0.role == .user || $0.role == .event } .suffix(6) .map { "- \(String($0.text.prefix(120)))" } .joined(separator: "\n") } private func label(for role: AgentExchange.Role) -> String { switch role { case .user: "Utilisateur" case .assistant: "Assistant" case .event: "Événement" } } }