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