// // Transcript.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Structured, persistent run history: the ordered AgentSteps, plan // snapshots, compaction records, cumulative usage, and the final outcome. // Saved incrementally to `.zyquo/transcript.json` after every mutation so a // crashed or interrupted run loses nothing; replayable by the Phase 6 UI // and inspectable by the Phase 7 trajectory review. // import Foundation /// The plan as it stood after a given step (the Plan panel's history). struct PlanSnapshot: Codable, Sendable { var stepIndex: Int var timestamp: Date = Date() var plan: TaskPlan } /// The complete on-disk run record. struct TranscriptDocument: Codable, Sendable { var version = 1 var task: String var modelID: String var provider: ProviderID var workspacePath: String var startedAt: Date = Date() var finishedAt: Date? var steps: [AgentStep] = [] var planSnapshots: [PlanSnapshot] = [] var compactions: [CompactionRecord] = [] var totalUsage = TokenUsage() var outcome: AgentRunOutcome? } /// Incremental writer around a TranscriptDocument. Plain struct — mutated /// only inside the AgentLoop actor; every mutating call persists. struct Transcript { private(set) var document: TranscriptDocument let fileURL: URL /// Creates a fresh transcript for a run, writing the initial document. init(task: String, model: AIModel, workspaceRoot: URL) { self.document = TranscriptDocument( task: task, modelID: model.id, provider: model.provider, workspacePath: workspaceRoot.path ) self.fileURL = workspaceRoot .appendingPathComponent(".zyquo") .appendingPathComponent("transcript.json") save() } /// Loads a persisted transcript (task reopen / trajectory inspection). static func load(from workspaceRoot: URL) -> TranscriptDocument? { let url = workspaceRoot .appendingPathComponent(".zyquo") .appendingPathComponent("transcript.json") guard let data = try? Data(contentsOf: url) else { return nil } return try? Self.decoder.decode(TranscriptDocument.self, from: data) } // MARK: Mutations (each one persists) /// Inserts the step or replaces the existing snapshot with the same id. mutating func upsert(step: AgentStep) { if let index = document.steps.firstIndex(where: { $0.id == step.id }) { document.steps[index] = step } else { document.steps.append(step) } save() } mutating func recordPlan(_ plan: TaskPlan, stepIndex: Int) { document.planSnapshots.append(PlanSnapshot(stepIndex: stepIndex, plan: plan)) save() } mutating func recordCompaction(_ record: CompactionRecord) { document.compactions.append(record) save() } mutating func addUsage(_ usage: TokenUsage) { document.totalUsage = document.totalUsage + usage save() } mutating func finish(outcome: AgentRunOutcome) { document.outcome = outcome document.finishedAt = Date() save() } // MARK: Persistence func save() { do { try FileManager.default.createDirectory( at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true ) try Self.encoder.encode(document).write(to: fileURL, options: .atomic) } catch { FileHandle.standardError.write(Data("Transcript save failed: \(error)\n".utf8)) } } private static let encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] encoder.dateEncodingStrategy = .iso8601 return encoder }() private static let decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder }() }