Files
Termind/Frameworks/TermindPresentation/AgentKnowledgeToolPresentation.swift
T

90 lines
4.2 KiB
Swift

import AgentKit
import Foundation
import TermindApplication
/// Render-time repair also covers cards persisted with the original, incorrect skill fallback.
/// A remote tool or an unknown explicit presenter ID can never opt into this policy by name alone.
nonisolated enum AgentKnowledgeToolPresentation {
enum Kind: String {
case skillsList = "skills_list"
case skillRead = "skill_read_file"
case skillManage = "skill_manage"
case skillInstall = "skill_install"
case memory
case sessionSearch = "session_search"
case legacySkill = "load_skill"
}
struct Operation {
let action: AgentToolDescriptor.Presentation.ActionKind
let activity: String
let batchActivity: String
var isArgument = false
}
static func kind(for descriptor: AgentToolDescriptor) -> Kind? {
guard descriptor.target == .local, let kind = Kind(rawValue: descriptor.name) else { return nil }
if let id = descriptor.presentation?.presenterID, id != "builtin.\(kind.rawValue)" { return nil }
return kind
}
static func operation(for card: AgentToolCard, locale: Locale, approval: Bool = false) -> Operation? {
guard let kind = kind(for: card.descriptor) else { return nil }
let args = card.invocation.call.arguments.objectValue ?? [:]
func localized(_ key: String.LocalizationValue) -> String { AppLocalization.string(key, locale: locale) }
func string(_ key: String) -> String? {
AgentToolDetailFormatting.nonempty(args[key]?.stringValue)
}
let skills = localized("skills")
let memory = localized("persistent memory")
let sessions = localized("saved conversations")
switch kind {
case .skillsList:
return .init(action: .list, activity: skills, batchActivity: skills)
case .skillRead, .legacySkill:
let listing = string("path") == "."
let fallback = listing ? localized("skill files") : localized("a skill")
guard !approval, let name = string("name") else {
return .init(action: listing ? .list : .read, activity: fallback, batchActivity: skills)
}
let path = string("path") ?? "SKILL.md"
return .init(
action: listing ? .list : .read, activity: listing ? name : "\(name) / \(path)", batchActivity: skills,
isArgument: true)
case .skillManage:
let operations = args["operations"]?.arrayValue ?? []
let names = Set(
operations.compactMap { AgentToolDetailFormatting.nonempty($0.objectValue?["name"]?.stringValue) })
let name = !approval && names.count == 1 ? names.first : nil
let activity = name ?? skills
let counted = operations.count > 1 ? localized("\(activity) (\(operations.count) operations)") : activity
return .init(action: .update, activity: counted, batchActivity: skills, isArgument: name != nil)
case .skillInstall:
let name = approval ? nil : string("name")
return .init(
action: .install, activity: name ?? localized("a skill"), batchActivity: skills,
isArgument: name != nil
)
case .memory:
let operations = args["operations"]?.arrayValue
let reading = operations?.isEmpty == true
let count = operations?.count ?? 0
let activity = count > 1 ? localized("\(memory) (\(count) operations)") : memory
return .init(action: reading ? .read : .update, activity: activity, batchActivity: memory)
case .sessionSearch:
if let query = string("query") {
return .init(
action: .search, activity: approval ? sessions : query, batchActivity: sessions,
isArgument: !approval)
}
return .init(
action: string("conversation_id") == nil ? .list : .read, activity: sessions, batchActivity: sessions)
}
}
static func title(_ text: String) -> String {
let oneLine = text.split(whereSeparator: \.isWhitespace).joined(separator: " ")
return oneLine.count > 80 ? String(oneLine.prefix(79)) + "…" : oneLine
}
}