Files
PhoneClaw/Tools/Helpers.swift
T
KellyvvandClaude Opus 4.6 779be78843 refactor: split MLX service, introduce skill type & runtime budgets
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>
2026-04-08 09:22:20 +08:00

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)
}