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.3 KB · 109 lines swift
Raw Blame History
1//2//  AgentSettingsStore.swift3//  Zyquo Agent4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  App-wide agent preferences (Settings › Agent + Safety), persisted to9//  AgentSettings.json via PersistenceService and folded into the10//  AgentConfiguration each run starts with (RunController.start).11//12//  Two Safety settings are persisted and surfaced but not yet enforced by13//  the PolicyEngine (it has no hook for them; documented gap):14//    - requireApprovalForAppleScript: the engine already asks in Manual and15//      Guarded; this toggle is meant to force asking in Autonomous too.16//    - workspaceEscapePolicy: the engine currently ALWAYS asks on any file17//      access outside the workspace (the strict default); the "deny" choice18//      is stored for a future engine hook.19//2021import Combine22import Foundation2324/// How file access outside the task workspace should be handled.25enum WorkspaceEscapePolicy: String, Codable, CaseIterable, Identifiable {26    /// Every outside-workspace read/write asks for approval (built-in default).27    case alwaysAsk28    /// Outside-workspace access is denied without asking.29    case deny3031    var id: String { rawValue }3233    var displayName: String {34        switch self {35        case .alwaysAsk: return "Always ask"36        case .deny: return "Deny without asking"37        }38    }39}4041/// The persisted settings document (AgentSettings.json).42struct AgentSettings: Codable {43    // Agent tab44    var maxSteps: Int = LoopGuardConfiguration.default.maxSteps45    var tokenBudget: Int = LoopGuardConfiguration.default.tokenBudget46    /// Wall-clock budget in minutes.47    var timeBudgetMinutes: Int = Int(LoopGuardConfiguration.default.wallClockBudget / 60)48    /// Per-command timeout in seconds.49    var perCommandTimeoutSeconds: Int = Int(ExecutionConfiguration.default.defaultTimeout)50    var parallelToolCalls: Bool = false51    /// Context-window fraction that triggers memory compaction.52    var compactionThreshold: Double = MemoryConfiguration.default.compactionThreshold5354    // Safety tab55    var defaultSafetyMode: SafetyMode = .guarded56    var requireApprovalForAppleScript: Bool = true57    var workspaceEscapePolicy: WorkspaceEscapePolicy = .alwaysAsk5859    // Models tab: persisted default agent model ("provider|modelID").60    var defaultAgentModelKey: String?61}6263/// Observable wrapper the Settings tabs bind to; every change persists.64@MainActor65final class AgentSettingsStore: ObservableObject {66    static let fileName = "AgentSettings.json"6768    @Published var settings: AgentSettings {69        didSet { persistence.save(settings, to: Self.fileName) }70    }7172    private let persistence: PersistenceService7374    init(persistence: PersistenceService = .shared) {75        self.persistence = persistence76        self.settings = persistence.load(AgentSettings.self, from: Self.fileName) ?? AgentSettings()77    }7879    /// The engine configuration a new run starts with, built from the80    /// persisted settings (clamped to sane bounds).81    func agentConfiguration(personaAddendum: String? = nil) -> AgentConfiguration {82        var configuration = AgentConfiguration.default83        configuration.loopGuard.maxSteps = max(1, settings.maxSteps)84        configuration.loopGuard.tokenBudget = max(10_000, settings.tokenBudget)85        configuration.loopGuard.wallClockBudget = TimeInterval(max(1, settings.timeBudgetMinutes)) * 6086        configuration.memory.compactionThreshold = min(0.95, max(0.5, settings.compactionThreshold))87        configuration.parallelToolCalls = settings.parallelToolCalls88        configuration.perCommandTimeout = TimeInterval(max(5, settings.perCommandTimeoutSeconds))89        configuration.personaAddendum = personaAddendum90        return configuration91    }9293    /// The persisted default agent model resolved against the catalog.94    func defaultAgentModel(in catalog: ModelCatalog) -> AIModel? {95        if let key = settings.defaultAgentModelKey {96            let parts = key.split(separator: "|", maxSplits: 1)97            if parts.count == 2, let provider = ProviderID(rawValue: String(parts[0])),98               let model = catalog.model(id: String(parts[1]), provider: provider) {99                return model100            }101        }102        return catalog.defaultAgentModel103    }104105    func setDefaultAgentModel(_ model: AIModel) {106        settings.defaultAgentModelKey = "\(model.provider.rawValue)|\(model.id)"107    }108}109