This commit is contained in:
Kaede Akino committed 2026-10-19 02:17:05 +08:00
1 parent a6f13b8dd3
commit b2b3927db0
22 files changed
+414 -71

No files matched your search

Submodule AgentKit updated: 3661d15620...42769e5e36.
@@ -0,0 +1,33 @@
import Foundation
import TermindPresentation
import Testing
@Suite @MainActor
struct AssistantFailurePresentationTests {
@Test func endpointFailureUsesTheRemoteMessageInsteadOfTheJSONEnvelope() {
let reason =
#"The model endpoint returned 400: {"error":{"message":"Missing required parameter: 'tools[48].description'.","code":"invalid_request_body"}}"#
#expect(
AssistantFailurePresentation.summary(for: reason) == "Missing required parameter: 'tools[48].description'.")
}
@Test(arguments: [
(#"{"message":"Request timed out"}"#, "Request timed out"),
(#"{"error":"Permission denied"}"#, "Permission denied"),
(#"{"error":{"message":" Rate limit\nexceeded "}}"#, "Rate limit exceeded"),
("Connection refused\nAdditional diagnostics", "Connection refused"),
(#"The endpoint returned 500: {"broken"#, "The endpoint returned 500:"),
(#"The endpoint returned 503: {"error":{"code":123}}"#, "The endpoint returned 503:"),
])
func summariesHandleDifferentErrorShapes(_ reason: String, _ expected: String) {
#expect(AssistantFailurePresentation.summary(for: reason) == expected)
}
@Test func emptyDiagnosticUsesLocalizedFallbackAndLongMessagesAreBounded() {
#expect(
AssistantFailurePresentation.summary(for: " \n\t ")
== String(localized: AppStrings.Assistant.agentError))
let message = String(repeating: "A", count: 200)
#expect(AssistantFailurePresentation.summary(for: message) == String(repeating: "A", count: 159) + "\u{2026}")
}
}
@@ -0,0 +1,71 @@
import AgentKit
import Foundation
import SwiftData
import TermindApplication
import TermindDomain
import TermindPersistence
import Testing
@Suite @MainActor
struct MCPServerMetadataTests {
@Test func rediscoveryReplacesAndClearsTheDescriptionWithoutResettingAccessPolicy() {
let tool = MCPTool(id: "search", accessPolicy: .approve)
let server = MCPServer(name: "Docs", serverDescription: "Old description", tools: [tool])
let model = AIConfigurationModel(mcpServers: [server])
model.setTools(
[MCPTool(id: tool.id)], version: "2026-07-28", serverDescription: "Search documents.", for: server.id)
#expect(model.mcpServer(server.id)?.serverDescription == "Search documents.")
#expect(model.mcpServer(server.id)?.tools.first?.accessPolicy == .approve)
model.setTools([MCPTool(id: tool.id)], version: "2026-07-28", for: server.id)
#expect(model.mcpServer(server.id)?.serverDescription == nil)
}
@Test func syncedMetadataRoundTripsAndOlderPayloadsRemainReadable() throws {
let server = MCPServer(name: "Docs", serverDescription: "Search documents.")
let encoded = try JSONEncoder().encode(SyncedMCPServer(server, sortIndex: 0))
let decoded = try JSONDecoder().decode(SyncedMCPServer.self, from: encoded)
#expect(decoded.value.serverDescription == server.serverDescription)
var old = try #require(JSONSerialization.jsonObject(with: encoded) as? [String: Any])
old.removeValue(forKey: "serverDescription")
let legacy = try JSONDecoder().decode(
SyncedMCPServer.self, from: JSONSerialization.data(withJSONObject: old))
#expect(legacy.value.serverDescription == nil)
#expect(legacy.value.id == server.id)
}
@Test func historicalServerSurvivesMigrationAndDescriptionSurvivesReopening() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let url = directory.appendingPathComponent("metadata.store")
let server = MCPServer(
name: "Docs", url: "https://docs.test/mcp",
tools: [MCPTool(id: "search", accessPolicy: .approve)])
try writeHistoricalServer(server, to: url)
try upgradeAndSaveDescription(server, at: url)
let reopened = try TermindPersistence.makeContainer(url: url)
let restored = try #require(AIPersistence(context: reopened.mainContext).load().mcpServers.first)
#expect(restored.id == server.id)
#expect(restored.tools == server.tools)
#expect(restored.serverDescription == "Search documents.")
}
private func writeHistoricalServer(_ server: MCPServer, to url: URL) throws {
let schema = Schema(versionedSchema: TermindSchemaV58.self)
let container = try ModelContainer(
for: schema,
configurations: [ModelConfiguration("Termind", schema: schema, url: url, cloudKitDatabase: .none)])
container.mainContext.insert(PersistentMCPServer(server, sortIndex: 0))
try container.mainContext.save()
}
private func upgradeAndSaveDescription(_ server: MCPServer, at url: URL) throws {
let container = try TermindPersistence.makeContainer(url: url)
let store = AIPersistence(context: container.mainContext)
var snapshot = try store.load()
#expect(snapshot.mcpServers.first?.id == server.id)
#expect(snapshot.mcpServers.first?.serverDescription == nil)
snapshot.mcpServers[0].serverDescription = "Search documents."
try store.save(snapshot)
}
}
+164 -41
View File
@@ -12317,6 +12317,88 @@
}
}
},
"Called" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aufgerufen"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Appelé"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "呼び出し済み"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "호출 완료"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "已调用"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "已呼叫"
}
}
}
},
"Calling" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wird aufgerufen"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Appel en cours"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "呼び出し中"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "호출 중"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "正在调用"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "正在呼叫"
}
}
}
},
"Calls appear here after an MCP client invokes a tool." : {
"extractionState" : "manual",
"localizations" : {
@@ -27766,6 +27848,47 @@
}
}
},
"Endpoint URL" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Endpunkt-URL"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "URL du point de terminaison"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "エンドポイントURL"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "엔드포인트 URL"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "端点URL"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "端點URL"
}
}
}
},
"Enjoying Termind?" : {
"extractionState" : "manual",
"localizations" : {
@@ -39773,47 +39896,6 @@
}
}
},
"MCP endpoint URL" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "MCP-Endpunkt-URL"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "URL du point de terminaison MCP"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "MCPエンドポイントURL"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "MCP 엔드포인트 URL"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "MCP端点URL"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "MCP端點URL"
}
}
}
},
"MagicDNS" : {
"extractionState" : "manual",
"localizations" : {
@@ -87169,6 +87251,47 @@
}
}
},
"call" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "aufrufen"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "appeler"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "呼び出し"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "호출"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "调用"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "呼叫"
}
}
}
},
"change permissions for" : {
"extractionState" : "manual",
"localizations" : {
@@ -712,7 +712,9 @@ public final class AIConfigurationModel {
/// reconnect would silently undo the decision made about this server. A
/// genuinely new tool uses its Termind default: a non-destructive tool with
/// `readOnlyHint == true` is approved, while every other tool asks.
public func setTools(_ discovered: [MCPTool], version: String, for id: MCPServer.ID) {
public func setTools(
_ discovered: [MCPTool], version: String, serverDescription: String? = nil, for id: MCPServer.ID
) {
mutatePersistedState {
guard let index = mcpServers.firstIndex(where: { $0.id == id }) else { return }
let previous = Dictionary(
@@ -726,6 +728,7 @@ public final class AIConfigurationModel {
return merged
}
mcpServers[index].negotiatedVersion = version
mcpServers[index].serverDescription = serverDescription
mcpServers[index].lastConnectedAt = Date()
}
}
@@ -118,6 +118,7 @@ public nonisolated struct SyncedMCPServer: Codable, Hashable, Sendable {
public var id: UUID
public var name: String
public var namespaceID: String
public var serverDescription: String?
public var transport: MCPServer.Transport
public var url: String
public var isEnabled: Bool
@@ -131,6 +132,7 @@ public nonisolated struct SyncedMCPServer: Codable, Hashable, Sendable {
id = server.id
name = server.name
namespaceID = server.namespaceID
serverDescription = server.serverDescription
transport = server.transport
url = server.url
isEnabled = server.isEnabled
@@ -143,7 +145,7 @@ public nonisolated struct SyncedMCPServer: Codable, Hashable, Sendable {
public var value: MCPServer {
MCPServer(
id: id, name: name, namespaceID: namespaceID, transport: transport,
id: id, name: name, namespaceID: namespaceID, serverDescription: serverDescription, transport: transport,
url: url, isEnabled: isEnabled, credentialRef: credentialRef, headers: headers, tools: tools, oauth: oauth)
}
}
@@ -948,6 +948,14 @@ public enum TermindSchemaV58: VersionedSchema {
}
}
/// Retains optional MCP implementation descriptions discovered from remote servers.
public enum TermindSchemaV59: VersionedSchema {
public static let versionIdentifier = Schema.Version(59, 0, 0)
public static var models: [any PersistentModel.Type] {
TermindSchemaV58.models + [PersistentMCPServerMetadata.self]
}
}
public enum TermindMigrationPlan: SchemaMigrationPlan {
public static var schemas: [any VersionedSchema.Type] {
[
@@ -966,7 +974,7 @@ public enum TermindMigrationPlan: SchemaMigrationPlan {
TermindSchemaV46.self, TermindSchemaV47.self, TermindSchemaV48.self, TermindSchemaV49.self,
TermindSchemaV50.self, TermindSchemaV51.self, TermindSchemaV52.self, TermindSchemaV53.self,
TermindSchemaV54.self, TermindSchemaV55.self, TermindSchemaV56.self, TermindSchemaV57.self,
TermindSchemaV58.self,
TermindSchemaV58.self, TermindSchemaV59.self,
]
}
@@ -1084,6 +1092,7 @@ public enum TermindMigrationPlan: SchemaMigrationPlan {
MigrationStage.lightweight(fromVersion: TermindSchemaV55.self, toVersion: TermindSchemaV56.self),
MigrationStage.lightweight(fromVersion: TermindSchemaV56.self, toVersion: TermindSchemaV57.self),
MigrationStage.lightweight(fromVersion: TermindSchemaV57.self, toVersion: TermindSchemaV58.self),
MigrationStage.lightweight(fromVersion: TermindSchemaV58.self, toVersion: TermindSchemaV59.self),
]
}
}
@@ -0,0 +1,14 @@
import Foundation
import SwiftData
/// Discovery metadata is separate so historical MCP server schemas remain unchanged.
@Model
public final class PersistentMCPServerMetadata {
@Attribute(.unique) public var serverID: UUID = UUID()
public var serverDescription = ""
public init(serverID: UUID, serverDescription: String) {
self.serverID = serverID
self.serverDescription = serverDescription
}
}
@@ -68,11 +68,15 @@ public final class AIPersistence {
)
let oauthRows = try context.fetch(FetchDescriptor<PersistentMCPOAuthConfiguration>())
let oauth = Dictionary(oauthRows.map { ($0.serverID, $0.value) }, uniquingKeysWith: { first, _ in first })
let metadataRows = try context.fetch(FetchDescriptor<PersistentMCPServerMetadata>())
let descriptions = Dictionary(
metadataRows.map { ($0.serverID, $0.serverDescription) }, uniquingKeysWith: { first, _ in first })
var seenMCPServers: Set<UUID> = []
let mcpServers = mcpRows.compactMap { row -> MCPServer? in
guard seenMCPServers.insert(row.id).inserted else { return nil }
var server = row.value
server.oauth = oauth[row.id]
server.serverDescription = descriptions[row.id]
return server
}
@@ -98,6 +102,7 @@ public final class AIPersistence {
try context.delete(model: PersistentAISelection.self)
try context.delete(model: PersistentMCPServer.self)
try context.delete(model: PersistentMCPOAuthConfiguration.self)
try context.delete(model: PersistentMCPServerMetadata.self)
for (providerIndex, provider) in snapshot.providers.enumerated() {
// A provider with no recorded endpoint is read back as
@@ -122,6 +127,9 @@ public final class AIPersistence {
context.insert(PersistentAISelection(snapshot.lastSelectedModel))
for (serverIndex, server) in snapshot.mcpServers.enumerated() {
context.insert(PersistentMCPServer(server, sortIndex: serverIndex))
if let description = server.serverDescription {
context.insert(PersistentMCPServerMetadata(serverID: server.id, serverDescription: description))
}
if let configuration = server.oauth {
context.insert(PersistentMCPOAuthConfiguration(serverID: server.id, configuration: configuration))
}
@@ -20,7 +20,7 @@ public enum TermindPersistence {
#endif
// Local schema migrations do not change the independently versioned CloudKit envelopes.
public static let schema = Schema(versionedSchema: TermindSchemaV58.self)
public static let schema = Schema(versionedSchema: TermindSchemaV59.self)
public static func makeContainer(inMemory: Bool = false, url: URL? = nil) throws -> ModelContainer {
precondition(!inMemory || url == nil, "An in-memory store cannot also have a file URL")
@@ -1138,6 +1138,11 @@ nonisolated public enum AgentToolPresentation {
if usesLocalizedActionTemplates(locale) { return localizedActionVerb(kind, locale: locale) }
let forms: ActionForms =
switch kind {
case .call:
.init(
infinitive: localized(AppStrings.MCP.callVerb, locale: locale),
progressive: localized(AppStrings.MCP.calling, locale: locale),
past: localized(AppStrings.MCP.called, locale: locale))
case .cancel: .init(infinitive: "cancel", progressive: "Cancelling", past: "Cancelled")
case .ask: .init(infinitive: "ask", progressive: "Asking", past: "Asked")
case .compare: .init(infinitive: "compare", progressive: "Comparing", past: "Compared")
@@ -1185,6 +1190,7 @@ nonisolated public enum AgentToolPresentation {
_ kind: AgentToolDescriptor.Presentation.ActionKind, locale: Locale
) -> String {
switch kind {
case .call: localized(AppStrings.MCP.callVerb, locale: locale)
case .cancel: localized(AppStrings.Common.cancel, locale: locale)
case .ask: localized("ask", locale: locale)
case .compare: localized("compare", locale: locale)
@@ -0,0 +1,29 @@
import Foundation
/// Compact disclosure titles derived from diagnostics, without changing the full error detail.
public enum AssistantFailurePresentation {
public static func summary(for reason: String) -> String {
let trimmed = reason.trimmingCharacters(in: .whitespacesAndNewlines)
var message = trimmed.components(separatedBy: .newlines).first ?? ""
if let start = trimmed.firstIndex(of: "{") {
let prefix = String(trimmed[..<start]).trimmingCharacters(in: .whitespacesAndNewlines)
if !prefix.isEmpty { message = prefix }
if let end = trimmed.lastIndex(of: "}"), start <= end,
trimmed.utf8.count <= 65_536,
let object = try? JSONSerialization.jsonObject(with: Data(trimmed[start...end].utf8))
as? [String: Any]
{
let error = object["error"] as? [String: Any]
let candidates = [error?["message"], object["message"], object["error"]]
if let detail = candidates.compactMap({ $0 as? String })
.first(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })
{
message = detail
}
}
}
let singleLine = message.split(whereSeparator: \.isWhitespace).joined(separator: " ")
guard !singleLine.isEmpty else { return String(localized: AppStrings.Assistant.agentError) }
return singleLine.count > 160 ? String(singleLine.prefix(159)) + "\u{2026}" : singleLine
}
}
@@ -2,6 +2,9 @@ import Foundation
extension AppStrings {
public nonisolated enum MCP {
public static var callVerb: LocalizedStringResource { "call" }
public static var calling: LocalizedStringResource { "Calling" }
public static var called: LocalizedStringResource { "Called" }
public static var oauth: LocalizedStringResource { "OAuth" }
public static var oauthAuthorization: LocalizedStringResource { "OAuth Authorization" }
public static var oauthSignedIn: LocalizedStringResource { "Signed in on this device" }
@@ -452,7 +452,7 @@ extension AppStrings {
}
public static var mcpEndpointUrl: LocalizedStringResource {
"MCP endpoint URL"
"Endpoint URL"
}
public static var mergeWithThisICloudAccount: LocalizedStringResource {
@@ -109,6 +109,7 @@ final class MobileMCPEditorModel {
return tool
}
draft.negotiatedVersion = outcome.protocolVersion
draft.serverDescription = outcome.serverDescription
draft.lastConnectedAt = .now
connection = String(
localized: AppStrings.Settings.discoveredTools(
@@ -137,6 +138,7 @@ final class MobileMCPEditorModel {
for tool in draft.tools { policies[tool.id] = tool.accessPolicy }
draft.tools = []
draft.negotiatedVersion = ""
draft.serverDescription = nil
draft.lastConnectedAt = nil
}
@@ -212,6 +214,7 @@ final class MobileMCPEditorModel {
if draft.lastConnectedAt != original.lastConnectedAt {
value.lastConnectedAt = draft.lastConnectedAt
value.negotiatedVersion = draft.negotiatedVersion
value.serverDescription = draft.serverDescription
}
}
value.url = value.url.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -710,20 +710,16 @@ struct MobileAssistantConversation: View {
.foregroundStyle(.red)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
AssistantCopyButton(text: failure.reason) {
UIPasteboard.general.string = $0
}
.labelStyle(.iconOnly)
.buttonStyle(.plain)
.foregroundStyle(.secondary)
}
.padding(.leading, 24)
} label: {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.red)
Text(AppStrings.Assistant.agentError)
Text(verbatim: AssistantFailurePresentation.summary(for: failure.reason))
.foregroundStyle(.primary)
.lineLimit(2)
.frame(maxWidth: .infinity, alignment: .leading)
}
.font(.callout)
}
+5 -1
View File
@@ -328,7 +328,7 @@ struct MobileAISettingsTests {
#expect(request.url == "https://draft.invalid/mcp")
#expect(token == "draft-token")
return MCPDiscovery(
protocolVersion: "test", serverName: "Test",
protocolVersion: "test", serverName: "Test", serverDescription: "Manage gateway tools.",
tools: [
MCPTool(id: "existing"),
MCPTool(id: "new"),
@@ -350,11 +350,15 @@ struct MobileAISettingsTests {
while editor.running { await Task.yield() }
#expect(ai.mcpServers.isEmpty)
#expect(editor.draft.tools.map(\.accessPolicy) == [.deny, .ask, .approve, .ask])
#expect(editor.draft.serverDescription == "Manage gateway tools.")
let conflictingIndex = try #require(editor.draft.tools.firstIndex { $0.id == "conflicting" })
editor.draft.tools[conflictingIndex].accessPolicy = .approve
try editor.save()
#expect(ai.mcpServer(server.id)?.namespaceID == "gateway")
#expect(ai.mcpServer(server.id)?.serverDescription == "Manage gateway tools.")
#expect(ai.mcpServer(server.id)?.tools.map(\.accessPolicy) == [.deny, .ask, .approve, .approve])
editor.invalidateConnection()
#expect(editor.draft.serverDescription == nil)
}
@Test func mcpCancellationAndRemoteDeletionCannotRestoreResults() async {
+11 -12
View File
@@ -1358,7 +1358,7 @@ private enum NativeTranscriptFactory {
let expanded = context.presentation.disclosures.isExpanded(disclosureID)
let header = NativeDisclosureHeader(
symbol: "exclamationmark.triangle",
title: String(localized: AppStrings.Assistant.agentError),
title: AssistantFailurePresentation.summary(for: failure.reason),
iconColor: .systemRed,
titleColor: .labelColor,
expanded: expanded,
@@ -1378,17 +1378,16 @@ private enum NativeTranscriptFactory {
top: 0, left: Metrics.Sidebar.rowIcon, bottom: 0, right: 0
)
detail.addArrangedSubview(NativeVerticalRule())
detail.addArrangedSubview(
NativeLabel(
failure.reason,
font: .monospacedSystemFont(
ofSize: NSFont.smallSystemFontSize,
weight: .regular
),
color: .systemRed,
selectable: true
))
detail.addArrangedSubview(NativeCopyButton(text: failure.reason))
let label = NativeLabel(
failure.reason,
font: .monospacedSystemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular),
color: .systemRed,
selectable: true
)
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
detail.addArrangedSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.trailingAnchor.constraint(equalTo: detail.trailingAnchor).isActive = true
stack.addArrangedSubview(detail)
}
return stack
@@ -385,7 +385,9 @@ struct MCPServerEditorPage: View {
guard !Task.isCancelled, generation == session.generation,
app.aiConfiguration.mcpServer(serverID) == server
else { return }
app.aiConfiguration.setTools(discovery.tools, version: discovery.protocolVersion, for: serverID)
app.aiConfiguration.setTools(
discovery.tools, version: discovery.protocolVersion,
serverDescription: discovery.serverDescription, for: serverID)
app.aiConfiguration.setMCPStatus(.connected, for: serverID)
session.connection = .succeeded(
String(
+38
View File
@@ -12,6 +12,44 @@ import struct TermindDomain.Host
@Suite
struct AgentToolPresentationTests {
@Test
func mcpCallsUseCallingAndCalledWithoutChangingCommandVerbs() {
var card = makeCard(
name: "docs.search", target: .mcp,
presentation: .init(symbol: "puzzlepiece.extension", activity: .label("docs.search"), actionKind: .call))
let english = Locale(identifier: "en")
card.state = .running
#expect(AgentToolPresentation.summary(for: card, locale: english).hasPrefix("Calling "))
card.state = .completed
#expect(AgentToolPresentation.summary(for: card, locale: english).hasPrefix("Called "))
#expect(AgentToolPresentation.batchSummary(for: [card, card], locale: english).hasPrefix("Called "))
card.state = .failed
#expect(AgentToolPresentation.summary(for: card, locale: english).hasPrefix("Failed to call "))
card.state = .waitingForApproval
#expect(AgentToolPresentation.summary(for: card, locale: english).hasPrefix("Waiting to call "))
card.descriptor.target = .local
card.descriptor.presentation?.actionKind = .run
card.state = .running
#expect(AgentToolPresentation.summary(for: card, locale: english).hasPrefix("Running "))
card.state = .completed
#expect(AgentToolPresentation.summary(for: card, locale: english).hasPrefix("Ran "))
}
@Test(arguments: ["zh-Hans", "zh-Hant"])
func mcpCallVerbsAreLocalized(_ language: String) {
var card = makeCard(
name: "docs.search", target: .mcp,
presentation: .init(symbol: "puzzlepiece.extension", activity: .label("docs.search"), actionKind: .call))
let locale = Locale(identifier: language)
card.state = .running
let running = language == "zh-Hans" ? "正在调用docs.search" : "正在呼叫docs.search"
#expect(AgentToolPresentation.summary(for: card, locale: locale) == running)
card.state = .completed
let expected = language == "zh-Hans" ? "已调用docs.search" : "已呼叫docs.search"
#expect(AgentToolPresentation.summary(for: card, locale: locale) == expected)
}
@Test
func approvalUsesTheRegisteredToolArgumentProjectionWithoutRedactingIt() throws {
let hostID = UUID()
+1 -1
View File
@@ -3021,7 +3021,7 @@
repositoryURL = "https://github.com/AkinoKaede/AgentKit.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 3.0.1;
minimumVersion = 3.0.2;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/AkinoKaede/AgentKit.git",
"state" : {
"revision" : "3661d156202e9da8d59e5e36ac461913c8bd8536",
"version" : "3.0.1"
"revision" : "1bcf65a06a68b15f7d8010c8ebb82c70b440bc90",
"version" : "3.0.2"
}
},
{