Files
PhoneClaw/Tools/ToolRegistry.swift
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

190 lines
5.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
// MARK: - 原生工具注册表
//
// 所有原生 API 封装集中注册在这里。
// SKILL.md 通过 allowed-tools 字段引用工具名。
struct RegisteredTool {
let name: String
let description: String
let parameters: String
/// 必填参数名列表,用于泛化参数校验(替代 per-tool switch)。
/// 空数组表示无参数或无需强制校验。
let requiredParameters: [String]
/// 至少要提供其中一个参数名;用于表达 search/delete 这类"多选一"契约。
let requiredAnyOfParameters: [String]
/// 该工具名的别名(纯格式层面),用于泛化 canonicalToolName。
/// 例如 ["contacts_delete", "contacts-delete-contact"]
let aliases: [String]
/// 该工具是否完全无参,可直接执行。
let isParameterless: Bool
/// 如果为 true,工具执行后直接使用 result 字段作为最终回答,不再走 LLM follow-up。
let skipFollowUp: Bool
let execute: ([String: Any]) async throws -> String
init(
name: String,
description: String,
parameters: String,
requiredParameters: [String] = [],
requiredAnyOfParameters: [String] = [],
aliases: [String] = [],
isParameterless: Bool = false,
skipFollowUp: Bool = false,
execute: @escaping ([String: Any]) async throws -> String
) {
self.name = name
self.description = description
self.parameters = parameters
self.requiredParameters = requiredParameters
self.requiredAnyOfParameters = requiredAnyOfParameters
self.aliases = aliases
self.isParameterless = isParameterless
self.skipFollowUp = skipFollowUp
self.execute = execute
}
func validates(arguments: [String: Any]) -> Bool {
if isParameterless {
return arguments.isEmpty
}
for parameter in requiredParameters {
guard Self.hasMeaningfulValue(arguments[parameter]) else {
return false
}
}
if !requiredAnyOfParameters.isEmpty,
!requiredAnyOfParameters.contains(where: { Self.hasMeaningfulValue(arguments[$0]) }) {
return false
}
return true
}
private static func hasMeaningfulValue(_ value: Any?) -> Bool {
switch value {
case let string as String:
return !string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
case let array as [Any]:
return !array.isEmpty
case let dictionary as [String: Any]:
return !dictionary.isEmpty
case nil:
return false
default:
return true
}
}
}
class ToolRegistry {
static let shared = ToolRegistry()
private var tools: [String: RegisteredTool] = [:]
/// alias → canonical tool name
private var aliasIndex: [String: String] = [:]
private init() {
registerBuiltInTools()
}
// MARK: - 公开接口
func register(_ tool: RegisteredTool) {
tools[tool.name] = tool
for alias in tool.aliases {
aliasIndex[alias] = tool.name
}
}
func find(name: String) -> RegisteredTool? {
if let tool = tools[name] {
return tool
}
if let resolved = canonicalName(for: name) {
return tools[resolved]
}
return nil
}
func execute(name: String, args: [String: Any]) async throws -> String {
guard let tool = find(name: name) else {
return "{\"success\": false, \"error\": \"未知工具: \(name)\"}"
}
return try await tool.execute(args)
}
/// 根据名称列表获取工具(用于 SKILL.md 的 allowed-tools)
func toolsFor(names: [String]) -> [RegisteredTool] {
names.compactMap { find(name: $0) }
}
/// 返回 true 如果该工具已注册
func hasToolNamed(_ name: String) -> Bool {
find(name: name) != nil
}
/// 所有已注册的工具名
var allToolNames: [String] {
Array(tools.keys).sorted()
}
// MARK: - 泛化查询(替代 AgentEngine 中的 per-tool switch)
/// 将别名或格式变体解析为注册的 canonical tool name。
func canonicalName(for rawName: String) -> String? {
let normalized = rawName
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "_", with: "-")
.lowercased()
if tools[normalized] != nil { return normalized }
if let mapped = aliasIndex[normalized] { return mapped }
return nil
}
/// 某个工具的必填参数列表。
func requiredParams(for toolName: String) -> [String] {
find(name: toolName)?.requiredParameters ?? []
}
/// 某个工具的"至少一个参数"候选列表。
func requiredAnyOfParams(for toolName: String) -> [String] {
find(name: toolName)?.requiredAnyOfParameters ?? []
}
/// 某个工具是否为无参工具。
func isParameterlessTool(named toolName: String) -> Bool {
find(name: toolName)?.isParameterless ?? false
}
/// 统一的参数契约校验。
func validatesArguments(_ arguments: [String: Any], for toolName: String) -> Bool {
find(name: toolName)?.validates(arguments: arguments) ?? false
}
/// 某个工具是否声明了跳过 LLM follow-up。
func shouldSkipFollowUp(for toolName: String) -> Bool {
find(name: toolName)?.skipFollowUp ?? false
}
// MARK: - 内置工具注册
private func registerBuiltInTools() {
ClipboardTools.register(into: self)
DeviceTools.register(into: self)
TextTools.register(into: self)
CalendarTools.register(into: self)
RemindersTools.register(into: self)
ContactsTools.register(into: self)
}
}