Major restructuring of the agent stack to support content-type skills, data-driven runtime budgets, and the new translate skill. LLM/MLX/ - Split MLXLocalLLMService.swift (1500 → 700 lines) into Core/, Gemma4/, Installation/ subdirectories - New BundledModel: LinearBudgetFormula replaces tier tables, model parameters fully data-driven via ModelRuntimeProfile - New RuntimeBudgets: pure functions for text/thinking/multimodal budgets, zero hardcoded model IDs - Drop adjustedTextOutputTokens prepared-subtraction (chunked prefill made it obsolete and triggered false-positive truncation on long prompts) Skills/ - Add SkillType (.device / .content) metadata, parsed from SKILL.md frontmatter - New translate skill (type: content, no tools) with anaphora handling - Trim translate (4563→1804), device (2357→1680), calendar (2169→1790) - contacts SKILL.md adds multi-turn disambiguation + cancel guidance, semantics owned by body not framework - SkillRegistry validates allowed-tools against ToolRegistry at load time Agent/ - ToolCallParser becomes registry-aware: drops tool_call names not in ToolRegistry or protocol set (load_skill / list_skills); ToolRegistry is now the single white list, validation moves to parser boundary - Router sticky routing uses semantic user-turn boundary instead of fixed suffix(4); correctly handles long agent loops - AgentEngine system prompt template differentiates device vs content skills with explicit invocation rules - SYSPROMPT.md auto-migration: old single-placeholder template backed up to .bak when new dual-placeholder is detected - PromptBuilder.buildLoadedSkillPrompt receives availableTools list, explicitly tells model which tools (or none) are callable Tools/ - ToolRegistry split into Helpers/Stores/AppPermissions/Handlers/* for separation of concerns - Mark Tools/Handlers/* with explicit allowed argument signatures UI/ - Add long-press copy contextMenu on user and assistant bubbles - Enable .textSelection on user bubble Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
91 lines
2.9 KiB
Swift
91 lines
2.9 KiB
Swift
import Foundation
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#endif
|
|
|
|
// MARK: - MLXLocalLLMService GPU + app lifecycle extension
|
|
//
|
|
// iPhone 禁止后台继续跑 Metal compute, 否则 jetsam 直接 kill。
|
|
// 这个 extension 监听 UIApplication 生命周期通知, 当 app 即将进入非活动状态时
|
|
// 立即取消当前的 generate/load task, 并把 foregroundGPUAllowed flag 置 false。
|
|
// ensureForegroundGPUExecution() 在关键路径前调用, 阻止任何后台提交。
|
|
|
|
extension MLXLocalLLMService {
|
|
|
|
func ensureForegroundGPUExecution() async throws {
|
|
#if canImport(UIKit)
|
|
let isActive = await MainActor.run {
|
|
UIApplication.shared.applicationState == .active
|
|
}
|
|
setForegroundGPUAllowed(isActive)
|
|
guard isActive else {
|
|
throw MLXError.gpuExecutionRequiresForeground
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func configureLifecycleObservers() {
|
|
#if canImport(UIKit)
|
|
let center = NotificationCenter.default
|
|
lifecycleObserverTokens = [
|
|
center.addObserver(
|
|
forName: UIApplication.willResignActiveNotification,
|
|
object: nil,
|
|
queue: nil
|
|
) { [weak self] _ in
|
|
self?.handleApplicationLeavingForeground()
|
|
},
|
|
center.addObserver(
|
|
forName: UIApplication.didEnterBackgroundNotification,
|
|
object: nil,
|
|
queue: nil
|
|
) { [weak self] _ in
|
|
self?.handleApplicationLeavingForeground()
|
|
},
|
|
center.addObserver(
|
|
forName: UIApplication.willEnterForegroundNotification,
|
|
object: nil,
|
|
queue: nil
|
|
) { [weak self] _ in
|
|
self?.setForegroundGPUAllowed(true)
|
|
},
|
|
center.addObserver(
|
|
forName: UIApplication.didBecomeActiveNotification,
|
|
object: nil,
|
|
queue: nil
|
|
) { [weak self] _ in
|
|
self?.setForegroundGPUAllowed(true)
|
|
}
|
|
]
|
|
|
|
Task { [weak self] in
|
|
guard let self else { return }
|
|
let isActive = await MainActor.run {
|
|
UIApplication.shared.applicationState == .active
|
|
}
|
|
self.setForegroundGPUAllowed(isActive)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func handleApplicationLeavingForeground() {
|
|
setForegroundGPUAllowed(false)
|
|
cancelled = true
|
|
currentGenerationTask?.cancel()
|
|
currentLoadTask?.cancel()
|
|
}
|
|
|
|
func setForegroundGPUAllowed(_ allowed: Bool) {
|
|
foregroundStateLock.lock()
|
|
foregroundGPUAllowed = allowed
|
|
foregroundStateLock.unlock()
|
|
}
|
|
|
|
func isForegroundGPUAllowed() -> Bool {
|
|
foregroundStateLock.lock()
|
|
let allowed = foregroundGPUAllowed
|
|
foregroundStateLock.unlock()
|
|
return allowed
|
|
}
|
|
}
|