diff --git a/Memory/ARCHITECTURE.md b/Memory/ARCHITECTURE.md index 7b731185..4994a48c 100644 --- a/Memory/ARCHITECTURE.md +++ b/Memory/ARCHITECTURE.md @@ -155,6 +155,13 @@ adapters at the LLM boundary. Models without tool capability receive no tools. Stop cancels the active stream and actions that have not started; steering messages are consumed at the next turn boundary. +Model purposes are `chat`, `embedding`, `rerank`, `image_generation`, and +`video_generation`. Only recognised purpose values from model discovery are +asserted as reported metadata. Unknown values, including Anthropic's `model` +object discriminator, leave catalog and name inference available and otherwise +default to chat. Only chat models enter conversation pickers. Persisted unknown +purpose strings also default to chat; there are no legacy purpose aliases. + A turn's tool calls run concurrently only when every call in it is marked parallel; one sequential call makes the whole turn serial rather than being fenced into its own segment. Concurrency is declared per tool and may be diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj index e8082250..0c15ac93 100644 --- a/Termind.xcodeproj/project.pbxproj +++ b/Termind.xcodeproj/project.pbxproj @@ -675,7 +675,7 @@ repositoryURL = "https://github.com/AkinoKaede/AgentKit.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.5.0; + minimumVersion = 0.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 34ed7bed..a4807c5d 100644 --- a/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Termind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/AkinoKaede/AgentKit.git", "state" : { - "revision" : "a7345ee68fd92b52589cecfc812517c089f39368", - "version" : "0.5.0" + "revision" : "ad0ab2f4f707657611faa671d41f229ff019d4e8", + "version" : "0.6.0" } }, { diff --git a/TermindTests/AIProviderPersistenceTests.swift b/TermindTests/AIProviderPersistenceTests.swift index 3d8bd477..362b8796 100644 --- a/TermindTests/AIProviderPersistenceTests.swift +++ b/TermindTests/AIProviderPersistenceTests.swift @@ -336,6 +336,32 @@ struct AIProviderPersistenceTests { #expect(store.resolve(conversation: nil)?.model.id == hidden.id) } + @Test(arguments: ["image", "other", "unknown-purpose"]) + func unrecognisedStoredModelKindsLoadAsChat(kind: String) throws { + let container = try makeContainer("UnknownModelKind-" + kind) + let provider = ModelProvider(name: "Gateway", models: [AIModel(id: "private-chat")]) + let store = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false) + store.upsert(provider) + let row = try #require(container.mainContext.fetch(FetchDescriptor()).first) + row.kind = kind + try container.mainContext.save() + + let reloaded = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false) + let restored = try #require(reloaded.modelProvider(provider.id)) + #expect(restored.models.first?.kind == .chat) + #expect(restored.assistantModels.map(\.id) == ["private-chat"]) + } + + @Test(arguments: AIModel.Kind.allCases) + func modelPurposesRoundTripThroughPersistence(kind: AIModel.Kind) throws { + let container = try makeContainer("ModelPurpose-" + kind.rawValue) + let provider = ModelProvider(name: "Gateway", models: [AIModel(id: "custom", kind: kind)]) + let store = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false) + store.upsert(provider) + let reloaded = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false) + #expect(reloaded.modelProvider(provider.id)?.models.first?.kind == kind) + } + private func makeContainer(_ name: String) throws -> ModelContainer { let configuration = localModelConfiguration( name, schema: HostPersistence.schema, isStoredInMemoryOnly: true @@ -368,6 +394,17 @@ private enum TestPersistenceError: Error { @Suite struct ModelCapabilityResolverTests { + @Test(arguments: ["model", "unknown-purpose", "", " \n"]) + func unrecognisedWireTypeAllowsCatalogClassification(type: String) throws { + let data = try JSONSerialization.data(withJSONObject: [ + "data": [["id": "gpt-5.4", "type": type]] + ]) + let model = try #require(ModelCatalogClient.parseOpenAIPage(data).models.first) + #expect(model.kind == .chat) + #expect(model.contextLength == 272_000) + #expect(model.abilities.contains(.toolCall)) + } + @Test func bundledPiCatalogEnrichesKnownModelWithoutRegex() { let provider = ModelProvider(name: "OpenAI") @@ -376,8 +413,8 @@ struct ModelCapabilityResolverTests { provider: provider ) - #expect(ModelCapabilityResolver.catalogMetadata?.version == "0.84.3") - #expect(ModelCapabilityResolver.catalogMetadata?.modelCount == 1_312) + #expect(ModelCapabilityResolver.catalogMetadata?.version == "0.85.1") + #expect(ModelCapabilityResolver.catalogMetadata?.modelCount == 1_354) #expect(resolution.catalogSource == .init(provider: "openai", modelID: "gpt-5.4")) #expect(resolution.model.input == [.text, .image]) // `webSearch` is the one ability that does come from the model id, and @@ -542,7 +579,7 @@ struct ModelCapabilityResolverTests { func reportedFieldsWinOverCodexAutoReviewOverride() { let provider = ModelProvider(name: "OpenAI") var model = AIModel(id: "codex-auto-review") - model.kind = .other + model.kind = .embedding model.input = [.audio] model.output = [.image] model.abilities = [] @@ -555,7 +592,7 @@ struct ModelCapabilityResolverTests { reported: [.kind, .input, .output, .abilities] ) - #expect(resolution.model.kind == .other) + #expect(resolution.model.kind == .embedding) #expect(resolution.model.input == [.audio]) #expect(resolution.model.output == [.image]) #expect(resolution.model.abilities.isEmpty) @@ -568,7 +605,7 @@ struct ModelCapabilityResolverTests { func explicitlyReportedFieldsWinOverPiCatalog() { let provider = ModelProvider(name: "OpenAI") var model = AIModel(id: "gpt-5.4") - model.kind = .other + model.kind = .embedding model.input = [.audio] model.output = [.image] model.abilities = [] @@ -581,7 +618,7 @@ struct ModelCapabilityResolverTests { reported: [.kind, .input, .output, .abilities] ) - #expect(resolution.model.kind == .other) + #expect(resolution.model.kind == .embedding) #expect(resolution.model.input == [.audio]) #expect(resolution.model.output == [.image]) // Reported abilities still win — `toolCall` and `reasoning` stay off