SPB Git

spb/zyquo-agent Public MIT

The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.

Swift 94.7% Shell 4.1% Python 0.7% Makefile 0.5%
4.0 KB · 128 lines swift
Raw Blame History
1//2//  Transcript.swift3//  Zyquo Agent4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Structured, persistent run history: the ordered AgentSteps, plan9//  snapshots, compaction records, cumulative usage, and the final outcome.10//  Saved incrementally to `.zyquo/transcript.json` after every mutation so a11//  crashed or interrupted run loses nothing; replayable by the Phase 6 UI12//  and inspectable by the Phase 7 trajectory review.13//1415import Foundation1617/// The plan as it stood after a given step (the Plan panel's history).18struct PlanSnapshot: Codable, Sendable {19    var stepIndex: Int20    var timestamp: Date = Date()21    var plan: TaskPlan22}2324/// The complete on-disk run record.25struct TranscriptDocument: Codable, Sendable {26    var version = 127    var task: String28    var modelID: String29    var provider: ProviderID30    var workspacePath: String31    var startedAt: Date = Date()32    var finishedAt: Date?33    var steps: [AgentStep] = []34    var planSnapshots: [PlanSnapshot] = []35    var compactions: [CompactionRecord] = []36    var totalUsage = TokenUsage()37    var outcome: AgentRunOutcome?38}3940/// Incremental writer around a TranscriptDocument. Plain struct — mutated41/// only inside the AgentLoop actor; every mutating call persists.42struct Transcript {43    private(set) var document: TranscriptDocument44    let fileURL: URL4546    /// Creates a fresh transcript for a run, writing the initial document.47    init(task: String, model: AIModel, workspaceRoot: URL) {48        self.document = TranscriptDocument(49            task: task,50            modelID: model.id,51            provider: model.provider,52            workspacePath: workspaceRoot.path53        )54        self.fileURL = workspaceRoot55            .appendingPathComponent(".zyquo")56            .appendingPathComponent("transcript.json")57        save()58    }5960    /// Loads a persisted transcript (task reopen / trajectory inspection).61    static func load(from workspaceRoot: URL) -> TranscriptDocument? {62        let url = workspaceRoot63            .appendingPathComponent(".zyquo")64            .appendingPathComponent("transcript.json")65        guard let data = try? Data(contentsOf: url) else { return nil }66        return try? Self.decoder.decode(TranscriptDocument.self, from: data)67    }6869    // MARK: Mutations (each one persists)7071    /// Inserts the step or replaces the existing snapshot with the same id.72    mutating func upsert(step: AgentStep) {73        if let index = document.steps.firstIndex(where: { $0.id == step.id }) {74            document.steps[index] = step75        } else {76            document.steps.append(step)77        }78        save()79    }8081    mutating func recordPlan(_ plan: TaskPlan, stepIndex: Int) {82        document.planSnapshots.append(PlanSnapshot(stepIndex: stepIndex, plan: plan))83        save()84    }8586    mutating func recordCompaction(_ record: CompactionRecord) {87        document.compactions.append(record)88        save()89    }9091    mutating func addUsage(_ usage: TokenUsage) {92        document.totalUsage = document.totalUsage + usage93        save()94    }9596    mutating func finish(outcome: AgentRunOutcome) {97        document.outcome = outcome98        document.finishedAt = Date()99        save()100    }101102    // MARK: Persistence103104    func save() {105        do {106            try FileManager.default.createDirectory(107                at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true108            )109            try Self.encoder.encode(document).write(to: fileURL, options: .atomic)110        } catch {111            FileHandle.standardError.write(Data("Transcript save failed: \(error)\n".utf8))112        }113    }114115    private static let encoder: JSONEncoder = {116        let encoder = JSONEncoder()117        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]118        encoder.dateEncodingStrategy = .iso8601119        return encoder120    }()121122    private static let decoder: JSONDecoder = {123        let decoder = JSONDecoder()124        decoder.dateDecodingStrategy = .iso8601125        return decoder126    }()127}128