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>
59 lines
2.1 KiB
Swift
59 lines
2.1 KiB
Swift
import Foundation
|
|
|
|
// MARK: - MLXLocalLLMService install-state extension
|
|
//
|
|
// selectModel / isModelAvailable / installState / refreshModelInstallStates /
|
|
// downloadMetrics。纯 bookkeeping, 不触碰模型权重加载 (那在主类 load() 里)。
|
|
|
|
extension MLXLocalLLMService {
|
|
|
|
public func selectModel(id: String) -> Bool {
|
|
guard let option = Self.availableModels.first(where: { $0.id == id }),
|
|
option != selectedModel else {
|
|
return false
|
|
}
|
|
|
|
selectedModel = option
|
|
statusMessage = isLoaded
|
|
? "已选择 \(option.displayName),准备重新加载..."
|
|
: "已选择 \(option.displayName),等待加载..."
|
|
return true
|
|
}
|
|
|
|
public func isModelAvailable(_ model: BundledModelOption) -> Bool {
|
|
ModelPaths.bundled(for: model) != nil
|
|
|| ModelPaths.hasRequiredFiles(model, at: ModelPaths.downloaded(for: model))
|
|
}
|
|
|
|
public func installState(for model: BundledModelOption) -> ModelInstallState {
|
|
if ModelPaths.bundled(for: model) != nil {
|
|
return .bundled
|
|
}
|
|
if ModelPaths.hasRequiredFiles(model, at: ModelPaths.downloaded(for: model)) {
|
|
return .downloaded
|
|
}
|
|
return modelInstallStates[model.id] ?? .notInstalled
|
|
}
|
|
|
|
public func refreshModelInstallStates() {
|
|
cleanupStalePartialDirectories()
|
|
for model in Self.availableModels {
|
|
if ModelPaths.bundled(for: model) != nil {
|
|
modelInstallStates[model.id] = .bundled
|
|
} else if ModelPaths.hasRequiredFiles(model, at: ModelPaths.downloaded(for: model)) {
|
|
modelInstallStates[model.id] = .downloaded
|
|
} else if case .checkingSource = modelInstallStates[model.id] {
|
|
continue
|
|
} else if case .downloading = modelInstallStates[model.id] {
|
|
continue
|
|
} else {
|
|
modelInstallStates[model.id] = .notInstalled
|
|
}
|
|
}
|
|
}
|
|
|
|
public func downloadMetrics(for modelID: String) -> ModelDownloadMetrics? {
|
|
modelDownloadMetrics[modelID]
|
|
}
|
|
}
|