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%
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