| import Foundation |
| import Observation |
|
|
| enum AssistantSessionError: LocalizedError, Sendable { |
| case persistenceUnavailable |
| case sessionUnavailable |
| case memoryLimitReached |
| case knowledgeLimitReached |
| case handoffLimitReached |
| case handoffNotFound |
| case handoffNotActive |
| case taskLimitReached |
| case taskNotFound |
| case taskAlreadyOpen |
| case unreceiptableToolResult |
|
|
| var errorDescription: String? { |
| switch self { |
| case .persistenceUnavailable: |
| "Local assistant storage is unavailable." |
| case .sessionUnavailable: |
| "The assistant session is no longer available." |
| case .memoryLimitReached: |
| "The local memory limit has been reached. Delete an item before saving another." |
| case .knowledgeLimitReached: |
| "The local knowledge limit has been reached. Delete an item before capturing another." |
| case .handoffLimitReached: |
| "The local handoff limit has been reached. Delete a handoff before creating another." |
| case .handoffNotFound: |
| "The requested session handoff no longer exists." |
| case .handoffNotActive: |
| "The requested session handoff is not the active checkpoint." |
| case .taskLimitReached: |
| "The local task limit has been reached. Delete a task before adding another." |
| case .taskNotFound: |
| "The requested task no longer exists." |
| case .taskAlreadyOpen: |
| "The requested task is already open." |
| case .unreceiptableToolResult: |
| "The local action result could not be recorded safely." |
| } |
| } |
| } |
|
|
| @MainActor |
| @Observable |
| final class AssistantSession { |
| var workspace: AssistantWorkspace = .empty |
| var draft = "" |
| private(set) var isWorkspaceReady = false |
| private(set) var isRunning = false |
| private(set) var isModelLoaded = false |
| private(set) var isModelLoading = false |
| private(set) var modelStatus = "Loading local data…" |
| private(set) var pendingApproval: ToolApprovalRequest? |
| private(set) var errorMessage: String? |
| private(set) var calendarAuthorization: SystemCalendarAuthorization = .notDetermined |
| private(set) var isCalendarAccessRequesting = false |
| private(set) var resourceSnapshot: AssistantResourceSnapshot? |
| private(set) var resourceWarning: String? |
|
|
| @ObservationIgnored private let calendarReader: EventKitCalendarReader |
| @ObservationIgnored private let toolRegistry: AgentToolRegistry |
| @ObservationIgnored private let resourceProvider = SystemResourceSnapshotProvider() |
| @ObservationIgnored private var persistence: AssistantPersistence? |
| @ObservationIgnored private var runtime: DolphinModelRuntime? |
| @ObservationIgnored private var agentLoop: AgentLoop? |
| @ObservationIgnored private var loadWorkspaceTask: Task<Void, Never>? |
| @ObservationIgnored private var loadModelTask: Task<Void, Never>? |
| @ObservationIgnored private var runTask: Task<Void, Never>? |
| @ObservationIgnored private var runTimeoutTask: Task<Void, Never>? |
| @ObservationIgnored private var persistenceTask: Task<Void, Never>? |
| @ObservationIgnored private var approvalExpiryTask: Task<Void, Never>? |
| @ObservationIgnored private var approvalContinuation: |
| CheckedContinuation<Bool, Never>? |
| @ObservationIgnored private var activeRunID: UUID? |
| @ObservationIgnored private var applicationIsActive = false |
|
|
| var canSubmitDraft: Bool { |
| guard |
| isWorkspaceReady, |
| !isRunning, |
| !isModelLoading, |
| !submittedDraftText.isEmpty |
| else { return false } |
|
|
| return isModelLoaded |
| || AgentRequestRouter.plan(for: submittedDraftText).mode == .deterministic |
| } |
|
|
| private var submittedDraftText: String { |
| String( |
| draft.trimmingCharacters(in: .whitespacesAndNewlines).prefix(8_000) |
| ) |
| } |
|
|
| private var idleModelStatus: String { |
| guard isWorkspaceReady else { return "Local storage unavailable" } |
| return isModelLoaded ? "Model ready" : "Model not loaded" |
| } |
|
|
| init() { |
| let calendarReader = EventKitCalendarReader() |
| self.calendarReader = calendarReader |
| toolRegistry = AgentToolRegistry(calendarReader: calendarReader) |
|
|
| do { |
| let persistence = try AssistantPersistence() |
| self.persistence = persistence |
| loadWorkspaceTask = Task { [weak self, persistence] in |
| do { |
| let savedWorkspace = try await persistence.load() |
| try Task.checkCancellation() |
| guard let self else { return } |
| let recoveredWorkspace = self.sanitized(savedWorkspace) |
| let recoveredInterruptedRuns = recoveredWorkspace != savedWorkspace |
| self.workspace = recoveredWorkspace |
| self.isWorkspaceReady = true |
| self.modelStatus = "Model not loaded" |
| Task { await self.refreshCalendarAuthorization() } |
| if recoveredInterruptedRuns { |
| self.persistSoon() |
| } |
| } catch is CancellationError { |
| return |
| } catch { |
| guard let self else { return } |
| |
| |
| self.persistence = nil |
| self.isWorkspaceReady = false |
| self.modelStatus = "Local storage unavailable" |
| self.errorMessage = |
| "Saved data could not be read and was left untouched: \(error.localizedDescription)" |
| } |
| self?.loadWorkspaceTask = nil |
| } |
| } catch { |
| persistence = nil |
| isWorkspaceReady = false |
| modelStatus = "Local storage unavailable" |
| errorMessage = error.localizedDescription |
| } |
| } |
|
|
| func loadModel() { |
| guard |
| isWorkspaceReady, |
| !isModelLoaded, |
| !isModelLoading, |
| loadModelTask == nil |
| else { |
| return |
| } |
| errorMessage = nil |
| refreshResourceSnapshot() |
| if let resourceSnapshot { |
| switch ResourceBudgetPolicy.modelLoadDecision(for: resourceSnapshot) { |
| case .allow: |
| resourceWarning = nil |
| case .warn(let warning): |
| resourceWarning = warning |
| recordStandalone( |
| kind: .model, |
| status: .information, |
| title: "Resource advisory", |
| detail: warning |
| ) |
| case .deny(let reason): |
| resourceWarning = nil |
| modelStatus = "Model load paused" |
| errorMessage = reason |
| recordStandalone( |
| kind: .model, |
| status: .failed, |
| title: "Model load blocked", |
| detail: reason |
| ) |
| return |
| } |
| } |
| isModelLoading = true |
| modelStatus = "Loading 1.81 GB model…" |
| recordStandalone( |
| kind: .model, |
| status: .running, |
| title: "Loading model", |
| detail: DolphinModelRuntime.modelIdentifier |
| ) |
|
|
| loadModelTask = Task { [weak self] in |
| defer { |
| self?.isModelLoading = false |
| self?.loadModelTask = nil |
| } |
| do { |
| let runtime = try await DolphinModelRuntime() |
| let information = await runtime.information() |
| try Task.checkCancellation() |
| guard let self else { return } |
| guard self.isWorkspaceReady, self.persistence != nil else { |
| self.modelStatus = "Local storage unavailable" |
| return |
| } |
| self.runtime = runtime |
| self.agentLoop = AgentLoop(runtime: runtime) |
| self.isModelLoaded = true |
| self.modelStatus = "Model ready" |
| self.recordStandalone( |
| kind: .model, |
| status: .succeeded, |
| title: "Model ready", |
| detail: |
| "Loaded in \(Self.format(information.loadSeconds)) s · \(information.maxContextLength)-token state · \(information.maxQueryLength)-token chunks" |
| ) |
| } catch is CancellationError { |
| guard let self else { return } |
| self.modelStatus = self.isWorkspaceReady |
| ? "Model load cancelled" |
| : "Local storage unavailable" |
| self.recordStandalone( |
| kind: .model, |
| status: .cancelled, |
| title: "Model load cancelled", |
| detail: "" |
| ) |
| } catch { |
| guard let self else { return } |
| self.modelStatus = "Model load failed" |
| self.errorMessage = error.localizedDescription |
| self.recordStandalone( |
| kind: .model, |
| status: .failed, |
| title: "Model load failed", |
| detail: error.localizedDescription |
| ) |
| } |
| } |
| } |
|
|
| func cancelModelLoad() { |
| guard isModelLoading, let loadModelTask else { return } |
| modelStatus = "Cancelling model load…" |
| loadModelTask.cancel() |
| } |
|
|
| func releaseModel() { |
| guard !isRunning, !isModelLoading else { return } |
| agentLoop = nil |
| runtime = nil |
| isModelLoaded = false |
| modelStatus = "Model not loaded" |
| recordStandalone( |
| kind: .model, |
| status: .information, |
| title: "Model released", |
| detail: "Core ML model memory can now be reclaimed by iOS." |
| ) |
| } |
|
|
| func send() { |
| let message = submittedDraftText |
| guard |
| !message.isEmpty, |
| isWorkspaceReady, |
| !isRunning, |
| !isModelLoading, |
| let persistence |
| else { return } |
|
|
| let requestPlan = AgentRequestRouter.plan(for: message) |
| let selectedAgentLoop: AgentLoop |
| let usesLoadedModel: Bool |
| if isModelLoaded, let agentLoop { |
| selectedAgentLoop = agentLoop |
| usesLoadedModel = true |
| } else if requestPlan.mode == .deterministic { |
| selectedAgentLoop = AgentLoop(runtime: ToolOnlyDolphinRuntime()) |
| usesLoadedModel = false |
| } else { |
| return |
| } |
|
|
| errorMessage = nil |
| draft = "" |
| let settings = sanitized(workspace.settings) |
| workspace.settings = settings |
| let userMessage = AssistantMessage( |
| role: .user, |
| content: message |
| ) |
| workspace.messages.append(userMessage) |
| trimWorkspaceHistory() |
|
|
| let runID = UUID() |
| let conversation = workspace.messages |
| workspace.runs.append( |
| AgentRunRecord( |
| id: runID, |
| requestMessageID: userMessage.id, |
| requestText: userMessage.content, |
| contextMessageIDs: conversation.map(\.id), |
| settingsSnapshot: settings |
| ) |
| ) |
| trimRunHistory() |
| activeRunID = runID |
| isRunning = true |
| modelStatus = usesLoadedModel ? "Thinking…" : "Running verified plan…" |
| let activeHandoff = workspace.activeHandoffID.flatMap { activeID in |
| workspace.handoffs.first { $0.id == activeID } |
| } |
| var memoryContext = KnowledgeEngine().promptContext( |
| knowledge: workspace.knowledgeItems, |
| query: userMessage.content, |
| limit: 4 |
| ) |
| if let activeHandoff { |
| memoryContext.insert( |
| MemoryItem( |
| id: activeHandoff.id, |
| content: "[active handoff] \(activeHandoff.title): \(activeHandoff.summary)", |
| createdAt: activeHandoff.restoredAt ?? activeHandoff.createdAt |
| ), |
| at: 0 |
| ) |
| } |
| if memoryContext.count < 5 { |
| let existingIDs = Set(memoryContext.map(\.id)) |
| memoryContext.append( |
| contentsOf: workspace.memories.reversed().filter { |
| !existingIDs.contains($0.id) |
| }.prefix(5 - memoryContext.count) |
| ) |
| } |
| let registry = toolRegistry |
| let context = toolExecutionContext(runID: runID) |
|
|
| runTimeoutTask?.cancel() |
| runTimeoutTask = Task { [weak self] in |
| do { |
| try await Task.sleep(for: .seconds(settings.maxRunSeconds)) |
| } catch { |
| return |
| } |
| guard let self, self.activeRunID == runID else { return } |
| self.markCancellationRequested(runID: runID, reason: .deadline) |
| self.modelStatus = "Stopping at the run deadline…" |
| self.resolveApproval(approved: false) |
| self.runTask?.cancel() |
| } |
|
|
| runTask = Task { |
| [weak self, selectedAgentLoop, registry, context, persistence] in |
| guard let self else { return } |
| do { |
| try await self.persistImmediately(using: persistence) |
| try Task.checkCancellation() |
| } catch is CancellationError { |
| |
| |
| } catch { |
| self.markRunTerminal( |
| runID: runID, |
| status: .failed, |
| stopReason: .storageFailure, |
| finalMessageID: nil, |
| errorSummary: error.localizedDescription |
| ) |
| self.markPersistenceUnavailable(error) |
| self.finishRunState(runID: runID) |
| return |
| } |
| let callbacks = AgentLoopCallbacks( |
| normalizeTool: { call in |
| try registry.normalize(call) |
| }, |
| executeTool: { call in |
| await registry.execute(call, context: context) |
| }, |
| requestApproval: { [weak self] request in |
| guard let self else { return false } |
| return await self.waitForApproval(request) |
| }, |
| emit: { [weak self] event in |
| self?.record(event) |
| }, |
| modelProgress: { [weak self] progress in |
| self?.modelStatus = "Thinking · \(progress.generatedTokens) tokens" |
| }, |
| checkpoint: { [weak self] update in |
| guard let self else { |
| throw AssistantSessionError.sessionUnavailable |
| } |
| try await self.applyCheckpoint(update, runID: runID) |
| } |
| ) |
|
|
| do { |
| let result = try await selectedAgentLoop.run( |
| runID: runID, |
| conversation: conversation, |
| memoryContext: memoryContext, |
| settings: settings, |
| toolDefinitions: registry.definitions, |
| callbacks: callbacks |
| ) |
| try Task.checkCancellation() |
| guard self.activeRunID == runID else { return } |
| let finalMessage = AssistantMessage( |
| role: .assistant, |
| content: result.finalText |
| ) |
| self.workspace.messages.append(finalMessage) |
| self.trimWorkspaceHistory() |
| self.markRunTerminal( |
| runID: runID, |
| status: result.outcome == .succeeded ? .succeeded : .failed, |
| stopReason: nil, |
| finalMessageID: finalMessage.id, |
| errorSummary: result.outcome == .failed |
| ? "The run stopped without completing every requested action." |
| : nil, |
| toolCallsUsed: result.toolCalls, |
| modelTurns: result.modelTurns |
| ) |
| self.modelStatus = self.idleModelStatus |
| do { |
| try await self.persistImmediately(using: persistence) |
| } catch { |
| self.markTerminalPersistenceFailure(runID: runID, error: error) |
| self.markPersistenceUnavailable(error) |
| } |
| } catch is CancellationError { |
| await self.terminalizeStoppedRun( |
| runID: runID, |
| reason: self.runStopReason(runID: runID), |
| persistence: persistence |
| ) |
| } catch let error as AgentLoopError where error == .timeLimitReached { |
| await self.terminalizeStoppedRun( |
| runID: runID, |
| reason: .deadline, |
| persistence: persistence |
| ) |
| } catch { |
| guard self.activeRunID == runID else { return } |
| self.errorMessage = error.localizedDescription |
| let finalMessage = AssistantMessage( |
| role: .assistant, |
| content: |
| "I couldn’t finish this run safely. \(error.localizedDescription)" |
| + self.terminalReceiptAppendix(for: runID) |
| ) |
| self.workspace.messages.append(finalMessage) |
| self.record( |
| AgentEvent( |
| runID: runID, |
| sequence: self.nextSequence(for: runID), |
| kind: .run, |
| status: .failed, |
| title: "Run failed", |
| detail: error.localizedDescription |
| ) |
| ) |
| self.markRunTerminal( |
| runID: runID, |
| status: .failed, |
| stopReason: self.runStopReason(runID: runID), |
| finalMessageID: finalMessage.id, |
| errorSummary: error.localizedDescription |
| ) |
| self.modelStatus = self.idleModelStatus |
| do { |
| try await self.persistImmediately(using: persistence) |
| } catch { |
| self.markTerminalPersistenceFailure(runID: runID, error: error) |
| self.markPersistenceUnavailable(error) |
| } |
| } |
|
|
| self.finishRunState(runID: runID) |
| } |
| } |
|
|
| func stop() { |
| guard isRunning else { return } |
| if let activeRunID { |
| markCancellationRequested(runID: activeRunID, reason: .user) |
| } |
| modelStatus = "Stopping after the current operation…" |
| runTimeoutTask?.cancel() |
| runTimeoutTask = nil |
| resolveApproval(approved: false) |
| runTask?.cancel() |
| } |
|
|
| func enteredBackground() { |
| applicationIsActive = false |
| guard isRunning else { return } |
| if let activeRunID { |
| markCancellationRequested(runID: activeRunID, reason: .background) |
| } |
| modelStatus = "Stopping because Dolphin left the foreground…" |
| runTimeoutTask?.cancel() |
| runTimeoutTask = nil |
| resolveApproval(approved: false) |
| runTask?.cancel() |
| } |
|
|
| func enteredForeground() { |
| applicationIsActive = true |
| refreshResourceSnapshot() |
| Task { await refreshCalendarAuthorization() } |
| } |
|
|
| func refreshResourceSnapshot() { |
| let snapshot = resourceProvider.snapshot() |
| resourceSnapshot = snapshot |
| switch ResourceBudgetPolicy.modelLoadDecision(for: snapshot) { |
| case .allow: |
| resourceWarning = nil |
| case .warn(let warning), .deny(let warning): |
| resourceWarning = warning |
| } |
| } |
|
|
| func refreshCalendarAuthorization() async { |
| calendarAuthorization = await calendarReader.authorizationStatus() |
| } |
|
|
| |
| |
| @discardableResult |
| func requestAndEnableCalendarAccess() async -> Bool { |
| guard |
| isWorkspaceReady, |
| !isRunning, |
| applicationIsActive, |
| !isCalendarAccessRequesting |
| else { return false } |
|
|
| isCalendarAccessRequesting = true |
| errorMessage = nil |
| defer { isCalendarAccessRequesting = false } |
|
|
| do { |
| let authorization = try await calendarReader.requestFullAccess() |
| calendarAuthorization = authorization |
| guard authorization == .fullAccess else { |
| errorMessage = SystemCalendarReaderError |
| .fullAccessRequired(authorization) |
| .localizedDescription |
| return false |
| } |
| guard applicationIsActive else { |
| errorMessage = |
| "Calendar access was granted by iOS, but Dolphin stayed disabled because the app left the foreground. Return to Settings and tap Enable Calendar Context." |
| return false |
| } |
| workspace.settings.enabledSystemCapabilities.insert(.calendarRead) |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .information, |
| title: "Calendar context enabled", |
| detail: "Dolphin may request bounded event-title and time reads. Every read still requires one-time approval." |
| ) |
| return true |
| } catch { |
| calendarAuthorization = await calendarReader.authorizationStatus() |
| errorMessage = "Calendar access was not enabled: \(error.localizedDescription)" |
| return false |
| } |
| } |
|
|
| @discardableResult |
| func enableCalendarContext() -> Bool { |
| guard |
| isWorkspaceReady, |
| !isRunning, |
| applicationIsActive, |
| calendarAuthorization == .fullAccess |
| else { return false } |
| workspace.settings.enabledSystemCapabilities.insert(.calendarRead) |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .information, |
| title: "Calendar context enabled", |
| detail: "Calendar reads remain bounded and require approval once per exact request." |
| ) |
| return true |
| } |
|
|
| func disableCalendarContext() { |
| guard isWorkspaceReady, !isRunning else { return } |
| guard workspace.settings.enabledSystemCapabilities.remove(.calendarRead) != nil else { |
| return |
| } |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .information, |
| title: "Calendar context disabled", |
| detail: "Dolphin will deny Calendar tool requests. You can separately revoke OS access in iOS Settings." |
| ) |
| } |
|
|
| func approvePending() { |
| guard let request = pendingApproval else { return } |
| resolveApproval(approved: !request.isExpired(), matching: request.id) |
| } |
|
|
| func denyPending() { |
| guard let request = pendingApproval else { return } |
| resolveApproval(approved: false, matching: request.id) |
| } |
|
|
| func clearConversation() { |
| guard isWorkspaceReady, !isRunning else { return } |
| workspace.messages.removeAll() |
| workspace.runs.removeAll() |
| workspace.events.removeAll { $0.runID != nil } |
| scrubConversationCopiesFromHandoffs() |
| persistSoon() |
| } |
|
|
| func deleteMemory(id: UUID) { |
| guard isWorkspaceReady, !isRunning else { return } |
| guard let memory = workspace.memories.first(where: { $0.id == id }) else { |
| return |
| } |
| workspace.memories.removeAll { $0.id == id } |
| let canonicalMirror = KnowledgeEngine.canonicalCaptureFields( |
| title: "Saved memory", |
| content: memory.content, |
| tags: ["memory"], |
| relatedItemIDs: [] |
| ) |
| let mirrorIDs = Set(workspace.knowledgeItems.compactMap { item -> UUID? in |
| guard |
| item.id == memory.id |
| || ( |
| item.title == canonicalMirror.title |
| && item.content == canonicalMirror.content |
| && item.tags == canonicalMirror.tags |
| && item.createdAt == memory.createdAt |
| ) |
| else { return nil } |
| return item.id |
| }) |
| removeKnowledgeAndDerivedState(ids: mirrorIDs) |
| persistSoon() |
| } |
|
|
| @discardableResult |
| func captureKnowledge( |
| kind: KnowledgeKind, |
| title: String, |
| content: String, |
| tags: [String] = [] |
| ) -> Bool { |
| guard |
| isWorkspaceReady, |
| !isRunning, |
| workspace.knowledgeItems.count < KnowledgeEngine.Limits.maxCorpusItems |
| else { return false } |
| let item = KnowledgeItem( |
| kind: kind, |
| title: title, |
| content: content, |
| tags: tags, |
| source: .user |
| ) |
| workspace.knowledgeItems.append(item) |
| appendAutomaticKnowledgeDigestIfNeeded(trigger: "manual capture") |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .succeeded, |
| title: "Knowledge captured", |
| detail: "\(item.kind.rawValue): \(item.title) · revision \(item.revision)" |
| ) |
| return true |
| } |
|
|
| func deleteKnowledge(id: UUID) { |
| guard isWorkspaceReady, !isRunning else { return } |
| guard workspace.knowledgeItems.contains(where: { $0.id == id }) else { |
| return |
| } |
| workspace.memories.removeAll { $0.id == id } |
| removeKnowledgeAndDerivedState(ids: [id]) |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .information, |
| title: "Knowledge deleted", |
| detail: id.uuidString |
| ) |
| } |
|
|
| @discardableResult |
| func createHandoff(title: String, focus: String) -> Bool { |
| guard isWorkspaceReady, !isRunning else { return false } |
| let handoff = KnowledgeEngine().makeHandoff( |
| title: title, |
| focus: focus, |
| knowledge: workspace.knowledgeItems, |
| tasks: workspace.tasks, |
| recentMessages: workspace.messages.suffix(6).map(\.content) |
| ) |
| if let index = workspace.handoffs.firstIndex(where: { $0.id == handoff.id }) { |
| workspace.handoffs[index] = handoff |
| } else { |
| guard workspace.handoffs.count < 100 else { return false } |
| workspace.handoffs.append(handoff) |
| } |
| workspace.activeHandoffID = handoff.id |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .succeeded, |
| title: "Session handoff created", |
| detail: "\(handoff.title) [\(handoff.id.uuidString)]" |
| ) |
| return true |
| } |
|
|
| @discardableResult |
| func restoreHandoff(id: UUID) -> Bool { |
| guard |
| isWorkspaceReady, |
| !isRunning, |
| let index = workspace.handoffs.firstIndex(where: { $0.id == id }) |
| else { return false } |
| workspace.handoffs[index].restoredAt = Date() |
| workspace.activeHandoffID = id |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .succeeded, |
| title: "Session handoff restored", |
| detail: "\(workspace.handoffs[index].title) [\(id.uuidString)]. No prior action was replayed." |
| ) |
| return true |
| } |
|
|
| func deleteHandoff(id: UUID) { |
| guard isWorkspaceReady, !isRunning else { return } |
| let previousCount = workspace.handoffs.count |
| workspace.handoffs.removeAll { $0.id == id } |
| guard workspace.handoffs.count != previousCount else { return } |
| if workspace.activeHandoffID == id { workspace.activeHandoffID = nil } |
| |
| |
| workspace.knowledgeDigests.removeAll() |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .information, |
| title: "Session handoff deleted", |
| detail: id.uuidString |
| ) |
| } |
|
|
| func deleteTask(id: UUID) { |
| guard isWorkspaceReady, !isRunning else { return } |
| guard workspace.tasks.contains(where: { $0.id == id }) else { return } |
| workspace.tasks.removeAll { $0.id == id } |
| let removedHandoffIDs = Set( |
| workspace.handoffs.filter { $0.taskIDs.contains(id) }.map(\.id) |
| ) |
| workspace.handoffs.removeAll { removedHandoffIDs.contains($0.id) } |
| if let activeID = workspace.activeHandoffID, |
| removedHandoffIDs.contains(activeID) |
| { |
| workspace.activeHandoffID = nil |
| } |
| |
| workspace.knowledgeDigests.removeAll() |
| persistSoon() |
| } |
|
|
| @discardableResult |
| func deactivateHandoff() -> Bool { |
| guard |
| isWorkspaceReady, |
| !isRunning, |
| let activeID = workspace.activeHandoffID |
| else { return false } |
| workspace.activeHandoffID = nil |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .information, |
| title: "Session handoff deactivated", |
| detail: "\(activeID.uuidString). The checkpoint remains saved." |
| ) |
| return true |
| } |
|
|
| @discardableResult |
| func setTaskCompletion(id: UUID, isCompleted: Bool) -> Bool { |
| guard |
| isWorkspaceReady, |
| !isRunning, |
| let index = workspace.tasks.firstIndex(where: { $0.id == id }) |
| else { return false } |
| guard workspace.tasks[index].isCompleted != isCompleted else { |
| return false |
| } |
| workspace.tasks[index].isCompleted = isCompleted |
| workspace.tasks[index].completedAt = isCompleted ? Date() : nil |
| persistSoon() |
| return true |
| } |
|
|
| @discardableResult |
| func saveSettings(_ settings: AgentSettings) -> AgentSettings? { |
| guard isWorkspaceReady, !isRunning else { return nil } |
| let appliedSettings = sanitized(settings) |
| workspace.settings = appliedSettings |
| persistSoon() |
| recordStandalone( |
| kind: .persistence, |
| status: .information, |
| title: "Settings updated", |
| detail: "The local save was queued. Any storage failure will appear here." |
| ) |
| return appliedSettings |
| } |
|
|
| private func waitForApproval(_ request: ToolApprovalRequest) async -> Bool { |
| guard activeRunID == request.runID, pendingApproval == nil else { |
| return false |
| } |
| return await withTaskCancellationHandler { |
| await withCheckedContinuation { continuation in |
| guard !Task.isCancelled else { |
| continuation.resume(returning: false) |
| return |
| } |
| pendingApproval = request |
| approvalContinuation = continuation |
| let interval = max(0, request.expiresAt.timeIntervalSinceNow) |
| approvalExpiryTask = Task { [weak self] in |
| do { |
| try await Task.sleep(for: .seconds(interval)) |
| } catch { |
| return |
| } |
| guard let self else { return } |
| self.resolveApproval(approved: false, matching: request.id) |
| } |
| } |
| } onCancel: { [weak self] in |
| Task { @MainActor in |
| self?.resolveApproval(approved: false, matching: request.id) |
| } |
| } |
| } |
|
|
| private func resolveApproval( |
| approved: Bool, |
| matching requestID: UUID? = nil |
| ) { |
| if let requestID, pendingApproval?.id != requestID { return } |
| approvalExpiryTask?.cancel() |
| approvalExpiryTask = nil |
| pendingApproval = nil |
| let continuation = approvalContinuation |
| approvalContinuation = nil |
| continuation?.resume(returning: approved) |
| } |
|
|
| private func applyCheckpoint( |
| _ update: AgentRunCheckpointUpdate, |
| runID: UUID |
| ) async throws { |
| guard |
| isWorkspaceReady, |
| let persistence, |
| let index = workspace.runs.firstIndex(where: { $0.id == runID }), |
| !workspace.runs[index].status.isTerminal |
| else { |
| throw AssistantSessionError.persistenceUnavailable |
| } |
|
|
| switch update { |
| case .phase(let phase): |
| workspace.runs[index].checkpoint.phase = phase |
| workspace.runs[index].checkpoint.activeCall = nil |
| case .activeCall(let call, let phase): |
| workspace.runs[index].checkpoint.phase = phase |
| workspace.runs[index].checkpoint.activeCall = call |
| case .counters(let toolCallsUsed, let modelTurns): |
| workspace.runs[index].checkpoint.toolCallsUsed = toolCallsUsed |
| workspace.runs[index].checkpoint.modelTurns = modelTurns |
| case .completedReceipt(let receipt, let toolCallsUsed, let modelTurns): |
| if !workspace.runs[index].checkpoint.completedReceipts.contains( |
| where: { $0.call.id == receipt.call.id } |
| ) { |
| workspace.runs[index].checkpoint.completedReceipts.append(receipt) |
| workspace.runs[index].checkpoint.completedReceipts = Array( |
| workspace.runs[index].checkpoint.completedReceipts.suffix( |
| AgentSettings.maxToolStepsRange.upperBound |
| ) |
| ) |
| } |
| workspace.runs[index].checkpoint.activeCall = nil |
| workspace.runs[index].checkpoint.toolCallsUsed = toolCallsUsed |
| workspace.runs[index].checkpoint.modelTurns = modelTurns |
| } |
| workspace.runs[index].checkpoint.updatedAt = Date() |
| try await persistImmediately(using: persistence) |
| } |
|
|
| private func persistImmediately( |
| using persistence: AssistantPersistence |
| ) async throws { |
| if let persistenceTask { |
| await persistenceTask.value |
| } |
| guard |
| isWorkspaceReady, |
| let currentPersistence = self.persistence, |
| currentPersistence === persistence |
| else { |
| throw AssistantSessionError.persistenceUnavailable |
| } |
| try await persistence.save(workspace) |
| } |
|
|
| private func markCancellationRequested( |
| runID: UUID, |
| reason: AgentRunStopReason |
| ) { |
| guard |
| let index = workspace.runs.firstIndex(where: { $0.id == runID }), |
| !workspace.runs[index].status.isTerminal |
| else { return } |
| workspace.runs[index].status = .cancellationRequested |
| workspace.runs[index].stopReason = workspace.runs[index].stopReason ?? reason |
| workspace.runs[index].checkpoint.updatedAt = Date() |
| persistSoon() |
| } |
|
|
| private func markRunTerminal( |
| runID: UUID, |
| status: AgentRunStatus, |
| stopReason: AgentRunStopReason?, |
| finalMessageID: UUID?, |
| errorSummary: String?, |
| toolCallsUsed: Int? = nil, |
| modelTurns: Int? = nil |
| ) { |
| guard |
| status.isTerminal, |
| let index = workspace.runs.firstIndex(where: { $0.id == runID }), |
| !workspace.runs[index].status.isTerminal |
| else { return } |
| workspace.runs[index].status = status |
| workspace.runs[index].stopReason = stopReason |
| ?? workspace.runs[index].stopReason |
| workspace.runs[index].checkpoint.phase = .finalizing |
| workspace.runs[index].checkpoint.activeCall = nil |
| if let toolCallsUsed { |
| workspace.runs[index].checkpoint.toolCallsUsed = toolCallsUsed |
| } |
| if let modelTurns { |
| workspace.runs[index].checkpoint.modelTurns = modelTurns |
| } |
| workspace.runs[index].checkpoint.updatedAt = Date() |
| workspace.runs[index].completedAt = Date() |
| workspace.runs[index].finalMessageID = finalMessageID |
| workspace.runs[index].errorSummary = errorSummary.map { |
| String($0.prefix(500)) |
| } |
| trimRunHistory() |
| } |
|
|
| private func markTerminalPersistenceFailure(runID: UUID, error: Error) { |
| guard let index = workspace.runs.firstIndex(where: { $0.id == runID }) else { |
| return |
| } |
| workspace.runs[index].status = .failed |
| workspace.runs[index].stopReason = .storageFailure |
| workspace.runs[index].checkpoint.phase = .finalizing |
| workspace.runs[index].checkpoint.activeCall = nil |
| workspace.runs[index].checkpoint.updatedAt = Date() |
| workspace.runs[index].completedAt = workspace.runs[index].completedAt |
| ?? Date() |
| workspace.runs[index].errorSummary = String( |
| "The terminal result could not be confirmed durable: \(error.localizedDescription)" |
| .prefix(500) |
| ) |
| } |
|
|
| private func runStopReason(runID: UUID) -> AgentRunStopReason? { |
| workspace.runs.first(where: { $0.id == runID })?.stopReason |
| } |
|
|
| private func terminalizeStoppedRun( |
| runID: UUID, |
| reason: AgentRunStopReason?, |
| persistence: AssistantPersistence |
| ) async { |
| guard activeRunID == runID else { return } |
| let resolution = AgentRunTerminalClassifier.cancellation(for: reason) |
| let finalMessage = AssistantMessage( |
| role: .assistant, |
| content: resolution.message + terminalReceiptAppendix(for: runID), |
| wasStopped: true |
| ) |
| workspace.messages.append(finalMessage) |
| record( |
| AgentEvent( |
| runID: runID, |
| sequence: nextSequence(for: runID), |
| kind: .run, |
| status: resolution.eventStatus, |
| title: resolution.title, |
| detail: resolution.detail |
| ) |
| ) |
| markRunTerminal( |
| runID: runID, |
| status: resolution.status, |
| stopReason: resolution.stopReason, |
| finalMessageID: finalMessage.id, |
| errorSummary: resolution.errorSummary |
| ) |
| modelStatus = idleModelStatus |
| do { |
| try await persistImmediately(using: persistence) |
| } catch { |
| markTerminalPersistenceFailure(runID: runID, error: error) |
| markPersistenceUnavailable(error) |
| } |
| } |
|
|
| private func finishRunState(runID: UUID) { |
| guard activeRunID == runID else { return } |
| runTimeoutTask?.cancel() |
| runTimeoutTask = nil |
| resolveApproval(approved: false) |
| activeRunID = nil |
| isRunning = false |
| runTask = nil |
| } |
|
|
| private func trimRunHistory() { |
| let removable = workspace.runs.filter { $0.status.isTerminal } |
| let overflow = max(0, removable.count - 100) |
| guard overflow > 0 else { return } |
| let removedIDs = Set(removable.prefix(overflow).map(\.id)) |
| workspace.runs.removeAll { removedIDs.contains($0.id) } |
| } |
|
|
| private func toolExecutionContext(runID: UUID) -> AgentToolExecutionContext { |
| AgentToolExecutionContext( |
| snapshot: { [weak self] in |
| guard let self else { return .empty } |
| return await self.workspaceSnapshot() |
| }, |
| isApplicationActive: { [weak self] in |
| guard let self else { return false } |
| return await self.applicationIsActive |
| }, |
| commitLocalWrite: { |
| [weak self] mutation, call, definition, resultBuilder in |
| guard let self else { |
| throw AssistantSessionError.sessionUnavailable |
| } |
| return try await self.applyWorkspaceMutation( |
| mutation, |
| runID: runID, |
| call: call, |
| definition: definition, |
| resultBuilder: resultBuilder |
| ) |
| } |
| ) |
| } |
|
|
| private func workspaceSnapshot() -> AssistantWorkspace { |
| workspace |
| } |
|
|
| private func applyWorkspaceMutation( |
| _ mutation: AssistantWorkspaceMutation, |
| runID: UUID, |
| call: AgentToolCall, |
| definition: AgentToolDefinition, |
| resultBuilder: AssistantWorkspaceResultBuilder |
| ) async throws -> AgentToolResult { |
| guard isWorkspaceReady, let persistence else { |
| throw AssistantSessionError.persistenceUnavailable |
| } |
|
|
| |
| |
| if let persistenceTask { |
| await persistenceTask.value |
| } |
| try Task.checkCancellation() |
| guard |
| isWorkspaceReady, |
| let currentPersistence = self.persistence, |
| currentPersistence === persistence, |
| activeRunID == runID |
| else { |
| throw AssistantSessionError.persistenceUnavailable |
| } |
|
|
| let previousWorkspace = workspace |
| let result: AssistantWorkspaceMutationResult |
|
|
| switch mutation { |
| case .saveMemory(let item): |
| guard workspace.memories.count < 500 else { |
| throw AssistantSessionError.memoryLimitReached |
| } |
| workspace.memories.append(item) |
| if workspace.knowledgeItems.count < KnowledgeEngine.Limits.maxCorpusItems { |
| workspace.knowledgeItems.append( |
| KnowledgeItem( |
| id: item.id, |
| kind: .fact, |
| title: "Saved memory", |
| content: item.content, |
| tags: ["memory"], |
| source: .user, |
| createdAt: item.createdAt |
| ) |
| ) |
| appendAutomaticKnowledgeDigestIfNeeded(trigger: "memory capture") |
| } |
| result = .memory(item) |
|
|
| case .captureKnowledge(let item): |
| guard workspace.knowledgeItems.count < KnowledgeEngine.Limits.maxCorpusItems else { |
| throw AssistantSessionError.knowledgeLimitReached |
| } |
| workspace.knowledgeItems.append(item) |
| appendAutomaticKnowledgeDigestIfNeeded(trigger: "fifth capture") |
| result = .knowledge(item) |
|
|
| case .createHandoff(let handoff): |
| if let index = workspace.handoffs.firstIndex(where: { $0.id == handoff.id }) { |
| workspace.handoffs[index] = handoff |
| } else { |
| guard workspace.handoffs.count < 100 else { |
| throw AssistantSessionError.handoffLimitReached |
| } |
| workspace.handoffs.append(handoff) |
| } |
| workspace.activeHandoffID = handoff.id |
| result = .handoff(handoff) |
|
|
| case .restoreHandoff(let id, let restoredAt): |
| guard let index = workspace.handoffs.firstIndex(where: { $0.id == id }) else { |
| throw AssistantSessionError.handoffNotFound |
| } |
| workspace.handoffs[index].restoredAt = restoredAt |
| workspace.activeHandoffID = id |
| result = .handoff(workspace.handoffs[index]) |
|
|
| case .deactivateHandoff(let id): |
| guard workspace.activeHandoffID == id else { |
| throw AssistantSessionError.handoffNotActive |
| } |
| guard let handoff = workspace.handoffs.first(where: { $0.id == id }) else { |
| throw AssistantSessionError.handoffNotFound |
| } |
| workspace.activeHandoffID = nil |
| result = .handoff(handoff) |
|
|
| case .addTask(let item): |
| guard workspace.tasks.count < 500 else { |
| throw AssistantSessionError.taskLimitReached |
| } |
| workspace.tasks.append(item) |
| result = .task(item) |
|
|
| case .completeTask(let id, let completedAt): |
| guard let index = workspace.tasks.firstIndex(where: { $0.id == id }) else { |
| throw AssistantSessionError.taskNotFound |
| } |
| workspace.tasks[index].isCompleted = true |
| workspace.tasks[index].completedAt = completedAt |
| result = .task(workspace.tasks[index]) |
|
|
| case .reopenTask(let id): |
| guard let index = workspace.tasks.firstIndex(where: { $0.id == id }) else { |
| throw AssistantSessionError.taskNotFound |
| } |
| guard workspace.tasks[index].isCompleted else { |
| throw AssistantSessionError.taskAlreadyOpen |
| } |
| workspace.tasks[index].isCompleted = false |
| workspace.tasks[index].completedAt = nil |
| result = .task(workspace.tasks[index]) |
| } |
|
|
| let toolResult: AgentToolResult |
| do { |
| toolResult = try resultBuilder(result).bounded( |
| toMaximumCharacters: definition.maxOutputCharacters |
| ) |
| } catch { |
| workspace = previousWorkspace |
| throw error |
| } |
| guard |
| toolResult.succeeded, |
| let receipt = AgentToolReceipt( |
| runID: runID, |
| call: call, |
| definition: definition, |
| result: toolResult |
| ) |
| else { |
| workspace = previousWorkspace |
| throw AssistantSessionError.unreceiptableToolResult |
| } |
| workspace.events.append( |
| AgentEvent( |
| runID: runID, |
| sequence: nextSequence(for: runID), |
| kind: .tool, |
| status: .succeeded, |
| title: "Tool completed", |
| detail: receipt.evidenceJSON, |
| receipt: receipt |
| ) |
| ) |
| if let runIndex = workspace.runs.firstIndex(where: { $0.id == runID }), |
| !workspace.runs[runIndex].checkpoint.completedReceipts.contains( |
| where: { $0.call.id == receipt.call.id } |
| ) |
| { |
| workspace.runs[runIndex].checkpoint.completedReceipts.append(receipt) |
| workspace.runs[runIndex].checkpoint.completedReceipts = Array( |
| workspace.runs[runIndex].checkpoint.completedReceipts.suffix( |
| AgentSettings.maxToolStepsRange.upperBound |
| ) |
| ) |
| workspace.runs[runIndex].checkpoint.activeCall = nil |
| workspace.runs[runIndex].checkpoint.updatedAt = Date() |
| } |
| if workspace.events.count > 500 { |
| workspace.events.removeFirst(workspace.events.count - 500) |
| } |
|
|
| do { |
| |
| |
| |
| try await persistence.save(workspace) |
| return toolResult |
| } catch { |
| workspace = previousWorkspace |
| markPersistenceUnavailable(error) |
| throw error |
| } |
| } |
|
|
| private func record(_ event: AgentEvent) { |
| if let receipt = event.receipt, |
| workspace.events.contains(where: { |
| $0.receipt?.runID == receipt.runID |
| && $0.receipt?.call.id == receipt.call.id |
| }) |
| { |
| return |
| } |
| workspace.events.append(event) |
| if workspace.events.count > 500 { |
| workspace.events.removeFirst(workspace.events.count - 500) |
| } |
| persistSoon() |
| } |
|
|
| private func terminalReceiptAppendix(for runID: UUID) -> String { |
| var seen: Set<UUID> = [] |
| let receipts = workspace.events.compactMap(\.receipt).filter { receipt in |
| receipt.runID == runID && seen.insert(receipt.call.id).inserted |
| } |
| guard !receipts.isEmpty else { |
| return "" |
| } |
| let hasLocalWrites = receipts.contains { $0.risk == .localWrite } |
| return hasLocalWrites |
| ? "\n\nSome local changes completed before the stop. Review Activity before retrying." |
| : "\n\nSome read-only steps completed before the stop. Details are in Activity." |
| } |
|
|
| private func recordStandalone( |
| kind: AgentEventKind, |
| status: AgentEventStatus, |
| title: String, |
| detail: String |
| ) { |
| record( |
| AgentEvent( |
| sequence: nextSequence(for: nil), |
| kind: kind, |
| status: status, |
| title: title, |
| detail: detail |
| ) |
| ) |
| } |
|
|
| private func nextSequence(for runID: UUID?) -> Int { |
| (workspace.events |
| .filter { $0.runID == runID } |
| .map(\.sequence) |
| .max() ?? 0) + 1 |
| } |
|
|
| private func persistSoon() { |
| guard isWorkspaceReady, let persistence else { return } |
| let snapshot = workspace |
| let previousTask = persistenceTask |
| persistenceTask = Task { [weak self, persistence] in |
| if let previousTask { |
| await previousTask.value |
| } |
| do { |
| try await persistence.save(snapshot) |
| } catch { |
| self?.markPersistenceUnavailable(error) |
| } |
| } |
| } |
|
|
| private func markPersistenceUnavailable(_ error: Error) { |
| if let activeRunID, |
| let index = workspace.runs.firstIndex(where: { $0.id == activeRunID }), |
| !workspace.runs[index].status.isTerminal |
| { |
| workspace.runs[index].status = .cancellationRequested |
| workspace.runs[index].stopReason = .storageFailure |
| workspace.runs[index].checkpoint.updatedAt = Date() |
| } |
| persistence = nil |
| isWorkspaceReady = false |
| errorMessage = |
| "Local storage became unavailable. Unsaved changes may be visible but are not confirmed durable: \(error.localizedDescription)" |
| modelStatus = "Local storage unavailable" |
| resolveApproval(approved: false) |
| runTimeoutTask?.cancel() |
| runTimeoutTask = nil |
| loadModelTask?.cancel() |
| runTask?.cancel() |
| } |
|
|
| private func trimWorkspaceHistory() { |
| if workspace.messages.count > 200 { |
| workspace.messages.removeFirst(workspace.messages.count - 200) |
| } |
| while workspace.messages.first?.role == .assistant { |
| workspace.messages.removeFirst() |
| } |
| } |
|
|
| private func sanitized(_ workspace: AssistantWorkspace) -> AssistantWorkspace { |
| var value = workspace |
| value.messages = Array(value.messages.suffix(200)).compactMap { message in |
| guard message.role == .assistant else { return message } |
| var cleaned = message |
| cleaned.content = AssistantChatText.cleaned(message.content) |
| return cleaned.content.isEmpty ? nil : cleaned |
| } |
| while value.messages.first?.role == .assistant { |
| value.messages.removeFirst() |
| } |
| value.events = Array(value.events.suffix(500)) |
| value.memories = Array(value.memories.suffix(500)) |
| value.knowledgeItems = Array( |
| value.knowledgeItems.suffix(KnowledgeEngine.Limits.maxCorpusItems) |
| ) |
| value.knowledgeDigests = Array(value.knowledgeDigests.suffix(50)) |
| value.handoffs = Array(value.handoffs.suffix(100)) |
| if let activeHandoffID = value.activeHandoffID, |
| !value.handoffs.contains(where: { $0.id == activeHandoffID }) |
| { |
| value.activeHandoffID = nil |
| } |
| value.tasks = Array(value.tasks.suffix(500)) |
| value.recoverInterruptedRuns() |
| value.events = Array(value.events.suffix(500)) |
| let removableRuns = value.runs.filter { $0.status.isTerminal } |
| let overflow = max(0, removableRuns.count - 100) |
| if overflow > 0 { |
| let removedIDs = Set(removableRuns.prefix(overflow).map(\.id)) |
| value.runs.removeAll { removedIDs.contains($0.id) } |
| } |
| value.settings = sanitized(value.settings) |
| return value |
| } |
|
|
| private func sanitized(_ settings: AgentSettings) -> AgentSettings { |
| settings.sanitized() |
| } |
|
|
| private func appendAutomaticKnowledgeDigestIfNeeded(trigger: String) { |
| guard |
| !workspace.knowledgeItems.isEmpty, |
| workspace.knowledgeItems.count.isMultiple(of: 5) |
| else { return } |
| let activeHandoff = workspace.activeHandoffID.flatMap { activeID in |
| workspace.handoffs.first { $0.id == activeID } |
| } |
| let digest = KnowledgeEngine().synthesizeContext( |
| knowledge: workspace.knowledgeItems, |
| tasks: workspace.tasks, |
| handoff: activeHandoff, |
| trigger: trigger |
| ) |
| if let index = workspace.knowledgeDigests.firstIndex(where: { $0.id == digest.id }) { |
| workspace.knowledgeDigests[index] = digest |
| } else { |
| workspace.knowledgeDigests.append(digest) |
| } |
| workspace.knowledgeDigests = Array(workspace.knowledgeDigests.suffix(50)) |
| } |
|
|
| private func removeKnowledgeAndDerivedState(ids: Set<UUID>) { |
| guard !ids.isEmpty else { return } |
| let cascade = KnowledgeEngine().deletionCascade( |
| deletingKnowledgeIDs: ids, |
| handoffs: workspace.handoffs, |
| digests: workspace.knowledgeDigests |
| ) |
| workspace.knowledgeItems.removeAll { ids.contains($0.id) } |
| for index in workspace.knowledgeItems.indices { |
| workspace.knowledgeItems[index].relatedItemIDs.removeAll { |
| ids.contains($0) |
| } |
| } |
| workspace.handoffs.removeAll { cascade.handoffIDs.contains($0.id) } |
| workspace.knowledgeDigests.removeAll { cascade.digestIDs.contains($0.id) } |
| if let activeID = workspace.activeHandoffID, |
| cascade.handoffIDs.contains(activeID) |
| { |
| workspace.activeHandoffID = nil |
| } |
| } |
|
|
| private func scrubConversationCopiesFromHandoffs() { |
| workspace.handoffs = workspace.handoffs.map { |
| $0.removingRecentConversationContext() |
| } |
| } |
|
|
| private static func format(_ value: Double) -> String { |
| String(format: "%.2f", value) |
| } |
| } |
|
|