Files
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

88 lines
3.8 KiB
Swift

import EventKit
import Foundation
enum CalendarTools {
static func register(into registry: ToolRegistry) {
// ── calendar-create-event ──
registry.register(RegisteredTool(
name: "calendar-create-event",
description: "创建新的日历事项,可写入标题、开始时间、结束时间、地点和备注",
parameters: "title: 事件标题, start: ISO 8601 开始时间, end: ISO 8601 结束时间(可选), location: 地点(可选), notes: 备注(可选)",
// 只有 start 是硬参 (EventKit API 强制要求)。
// title 是软参,缺失时 handler 用默认值,不强制 caller 提供。
requiredParameters: ["start"]
) { args in
// title 是软参: 没传或为空时使用默认标题, 不阻断流程
let rawTitle = (args["title"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let title = rawTitle.isEmpty ? "新日历事项" : rawTitle
guard let startRaw = args["start"] as? String,
let startDate = parseISO8601Date(startRaw) else {
return failurePayload(error: "缺少有效的 start 参数,必须是 ISO 8601 时间字符串")
}
let endRaw = (args["end"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let endDate = endRaw.flatMap(parseISO8601Date) ?? startDate.addingTimeInterval(3600)
guard endDate >= startDate else {
return failurePayload(error: "end 不能早于 start")
}
let location = (args["location"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let notes = (args["notes"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
do {
guard try await ToolRegistry.shared.requestAccess(for: .calendar) else {
return failurePayload(error: "未获得日历写入权限")
}
guard let calendar = writableEventCalendar() else {
return failurePayload(error: "没有可用于新建事项的可写日历,请先在系统日历中启用或创建一个日历")
}
let event = EKEvent(eventStore: SystemStores.event)
event.calendar = calendar
event.title = title
event.startDate = startDate
event.endDate = endDate
if let location, !location.isEmpty {
event.location = location
}
if let notes, !notes.isEmpty {
event.notes = notes
}
try SystemStores.event.save(event, span: .thisEvent, commit: true)
return successPayload(
result: "已创建日历事项\u{201C}\(title)\u{201D},开始时间为 \(iso8601String(from: startDate))。",
extras: [
"eventId": event.eventIdentifier ?? "",
"title": title,
"start": iso8601String(from: startDate),
"end": iso8601String(from: endDate),
"location": location ?? "",
"notes": notes ?? ""
]
)
} catch {
return failurePayload(error: "创建日历事项失败:\(error.localizedDescription)")
}
})
}
// MARK: - Private Helpers
private static func writableEventCalendar() -> EKCalendar? {
if let calendar = SystemStores.event.defaultCalendarForNewEvents,
calendar.allowsContentModifications {
return calendar
}
return SystemStores.event.calendars(for: .event)
.first(where: \.allowsContentModifications)
}
}