spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
1//2// Condenser.swift3// Poche4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//89import Foundation10import FoundationModels1112/// One exchange in the agent's private log, used for condensation and13/// session recycling.14struct AgentExchange: Sendable {15 enum Role: String, Sendable {16 case user17 case assistant18 case event19 }2021 let role: Role22 let text: String23}2425/// Summarizes old turns in a separate, tool-free session (CLAUDE.md §5).26struct Condenser: Sendable {27 /// Each turn is clipped before summarizing: the condensation prompt must28 /// itself fit in a fresh window.29 private static let perTurnClip = 2803031 func condense(_ exchanges: [AgentExchange]) async throws -> String {32 let rendered = exchanges.map { exchange in33 "\(label(for: exchange.role)) : \(String(exchange.text.prefix(Self.perTurnClip)))"34 }35 .joined(separator: "\n")3637 let session = LanguageModelSession(38 model: .default,39 instructions: "Tu résumes une conversation entre un utilisateur et son assistant. Sois dense, fidèle, factuel."40 )41 let response = try await session.respond(42 to: "Résume fidèlement cette conversation :\n\n\(rendered)",43 generating: CondensedSummary.self44 )45 return response.content.summary46 }4748 /// Last-resort summary when the model itself cannot condense49 /// (e.g. the condensation call is refused). Losing detail is acceptable;50 /// visibly losing the thread is not.51 func fallbackSummary(for exchanges: [AgentExchange]) -> String {52 exchanges53 .filter { $0.role == .user || $0.role == .event }54 .suffix(6)55 .map { "- \(String($0.text.prefix(120)))" }56 .joined(separator: "\n")57 }5859 private func label(for role: AgentExchange.Role) -> String {60 switch role {61 case .user: "Utilisateur"62 case .assistant: "Assistant"63 case .event: "Événement"64 }65 }66}67