diff --git a/acp-model/api/acp-model.api b/acp-model/api/acp-model.api index 29026f0..a790417 100644 --- a/acp-model/api/acp-model.api +++ b/acp-model/api/acp-model.api @@ -790,8 +790,6 @@ public final class com/agentclientprotocol/model/EmbeddedResource$Companion { public abstract class com/agentclientprotocol/model/EmbeddedResourceResource : com/agentclientprotocol/model/AcpWithMeta { public static final field Companion Lcom/agentclientprotocol/model/EmbeddedResourceResource$Companion; - public synthetic fun (ILkotlinx/serialization/internal/SerializationConstructorMarker;)V - public static final synthetic fun write$Self (Lcom/agentclientprotocol/model/EmbeddedResourceResource;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V } public final class com/agentclientprotocol/model/EmbeddedResourceResource$BlobResourceContents : com/agentclientprotocol/model/EmbeddedResourceResource { diff --git a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Content.kt b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Content.kt index 05ab01b..09dbb3a 100644 --- a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Content.kt +++ b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Content.kt @@ -3,11 +3,21 @@ package com.agentclientprotocol.model +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonClassDiscriminator import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +/** + * Common discriminator key used for polymorphic serialization in ACP. + */ +internal const val TYPE_DISCRIMINATOR = "type" /** * Content blocks represent displayable information in the Agent Client Protocol. @@ -18,10 +28,10 @@ import kotlinx.serialization.json.JsonElement * See protocol docs: [Content](https://agentclientprotocol.com/protocol/content) */ @Serializable -@JsonClassDiscriminator("type") +@JsonClassDiscriminator(TYPE_DISCRIMINATOR) public sealed class ContentBlock : AcpWithMeta { public abstract val annotations: Annotations? - + /** * Plain text content * @@ -101,7 +111,7 @@ public sealed class ContentBlock : AcpWithMeta { /** * Resource content that can be embedded in a message. */ -@Serializable +@Serializable(with = EmbeddedResourceResourceSerializer::class) public sealed class EmbeddedResourceResource : AcpWithMeta { /** * Text-based resource contents. @@ -128,6 +138,28 @@ public sealed class EmbeddedResourceResource : AcpWithMeta { ) : EmbeddedResourceResource() } +/** + * Embedded resources are discriminator-less in the protocol; choose subtype by fields if + * discriminator is absent, but still honor an explicit discriminator when provided. + */ +internal object EmbeddedResourceResourceSerializer : + JsonContentPolymorphicSerializer(EmbeddedResourceResource::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val obj = element.jsonObject + + val explicitType = obj[TYPE_DISCRIMINATOR]?.jsonPrimitive?.content + when (explicitType) { + EmbeddedResourceResource.TextResourceContents::class.simpleName -> return EmbeddedResourceResource.TextResourceContents.serializer() + EmbeddedResourceResource.BlobResourceContents::class.simpleName -> return EmbeddedResourceResource.BlobResourceContents.serializer() + } + + if (EmbeddedResourceResource.TextResourceContents::text.name in obj) return EmbeddedResourceResource.TextResourceContents.serializer() + if (EmbeddedResourceResource.BlobResourceContents::blob.name in obj) return EmbeddedResourceResource.BlobResourceContents.serializer() + + throw SerializationException("Cannot determine EmbeddedResourceResource type; expected '${EmbeddedResourceResource.TextResourceContents::text.name}' or '${EmbeddedResourceResource.BlobResourceContents::blob.name}'") + } +} + /** * The contents of a resource, embedded into a prompt or tool call result. */ @@ -151,4 +183,4 @@ public data class ResourceLink( val title: String? = null, val annotations: Annotations? = null, override val _meta: JsonElement? = null -) : AcpWithMeta \ No newline at end of file +) : AcpWithMeta diff --git a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/SessionUpdate.kt b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/SessionUpdate.kt index 7d08430..ee0574a 100644 --- a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/SessionUpdate.kt +++ b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/SessionUpdate.kt @@ -15,7 +15,7 @@ import kotlinx.serialization.json.JsonElement * Specifies how the agent should collect input for this command. */ @Serializable -@JsonClassDiscriminator("type") +@JsonClassDiscriminator(TYPE_DISCRIMINATOR) public sealed class AvailableCommandInput { /** * All text typed after the command name is provided as unstructured input. diff --git a/acp-model/src/commonTest/kotlin/com/agentclientprotocol/model/EmbeddedResourceResourceTest.kt b/acp-model/src/commonTest/kotlin/com/agentclientprotocol/model/EmbeddedResourceResourceTest.kt new file mode 100644 index 0000000..111587f --- /dev/null +++ b/acp-model/src/commonTest/kotlin/com/agentclientprotocol/model/EmbeddedResourceResourceTest.kt @@ -0,0 +1,53 @@ +package com.agentclientprotocol.model + +import com.agentclientprotocol.rpc.ACPJson +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class EmbeddedResourceResourceTest { + + @Test + fun `decodes resource content without discriminator`() { + val payload = """ + { + "type": "resource", + "resource": { + "text": "hello", + "uri": "file:///tmp/example.txt" + } + } + """.trimIndent() + + val block = ACPJson.decodeFromString(ContentBlock.serializer(), payload) + + assertTrue(block is ContentBlock.Resource) + val resource = block.resource + assertTrue(resource is EmbeddedResourceResource.TextResourceContents) + assertEquals("hello", resource.text) + assertEquals("file:///tmp/example.txt", resource.uri) + } + + @Test + fun `decodes blob resource content without discriminator`() { + val payload = """ + { + "type": "resource", + "resource": { + "blob": "ZGF0YQ==", + "mimeType": "application/octet-stream", + "uri": "file:///tmp/data.bin" + } + } + """.trimIndent() + + val block = ACPJson.decodeFromString(ContentBlock.serializer(), payload) + + assertTrue(block is ContentBlock.Resource) + val resource = block.resource + assertTrue(resource is EmbeddedResourceResource.BlobResourceContents) + assertEquals("ZGF0YQ==", resource.blob) + assertEquals("application/octet-stream", resource.mimeType) + assertEquals("file:///tmp/data.bin", resource.uri) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index f329bfb..3605c69 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { private val buildNumber: String? = System.getenv("GITHUB_RUN_NUMBER") private val isReleasePublication = System.getenv("RELEASE_PUBLICATION")?.toBoolean() ?: false -private val baseVersion = "0.9.1" +private val baseVersion = "0.9.2" allprojects { group = "com.agentclientprotocol"