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>
95 lines
2.6 KiB
Swift
95 lines
2.6 KiB
Swift
import Foundation
|
|
|
|
// MARK: - JSON Utilities
|
|
|
|
func jsonEscape(_ str: String) -> String {
|
|
str.replacingOccurrences(of: "\\", with: "\\\\")
|
|
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
.replacingOccurrences(of: "\n", with: "\\n")
|
|
.replacingOccurrences(of: "\t", with: "\\t")
|
|
}
|
|
|
|
func jsonString(_ object: Any) -> String {
|
|
guard JSONSerialization.isValidJSONObject(object),
|
|
let data = try? JSONSerialization.data(withJSONObject: object),
|
|
let string = String(data: data, encoding: .utf8) else {
|
|
return "{\"success\": false, \"error\": \"JSON 编码失败\"}"
|
|
}
|
|
return string
|
|
}
|
|
|
|
// MARK: - Tool Result Payloads
|
|
|
|
func successPayload(
|
|
result: String,
|
|
extras: [String: Any] = [:]
|
|
) -> String {
|
|
var payload = extras
|
|
payload["success"] = true
|
|
payload["status"] = "succeeded"
|
|
payload["result"] = result
|
|
return jsonString(payload)
|
|
}
|
|
|
|
func failurePayload(error: String, extras: [String: Any] = [:]) -> String {
|
|
var payload = extras
|
|
payload["success"] = false
|
|
payload["status"] = "failed"
|
|
payload["error"] = error
|
|
return jsonString(payload)
|
|
}
|
|
|
|
// MARK: - Date Helpers
|
|
|
|
func parseISO8601Date(_ raw: String) -> Date? {
|
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return nil }
|
|
|
|
let isoFormatters: [ISO8601DateFormatter] = [
|
|
{
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.timeZone = .current
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return formatter
|
|
}(),
|
|
{
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.timeZone = .current
|
|
formatter.formatOptions = [.withInternetDateTime]
|
|
return formatter
|
|
}()
|
|
]
|
|
|
|
for formatter in isoFormatters {
|
|
if let date = formatter.date(from: trimmed) {
|
|
return date
|
|
}
|
|
}
|
|
|
|
let formats = [
|
|
"yyyy-MM-dd'T'HH:mm:ss",
|
|
"yyyy-MM-dd'T'HH:mm",
|
|
"yyyy-MM-dd HH:mm:ss",
|
|
"yyyy-MM-dd HH:mm"
|
|
]
|
|
|
|
for format in formats {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
formatter.timeZone = .current
|
|
formatter.dateFormat = format
|
|
if let date = formatter.date(from: trimmed) {
|
|
return date
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func iso8601String(from date: Date) -> String {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.timeZone = .current
|
|
formatter.formatOptions = [.withInternetDateTime]
|
|
return formatter.string(from: date)
|
|
}
|