Skip to content

Commit f8c0d98

Browse files
fix: Implement polymorphic serialization for EmbeddedResourceResource and add unit tests (#39)
* fix: Implement polymorphic serialization for EmbeddedResourceResource and add unit tests * fix: Remove synthetic constructor and write method from EmbeddedResourceResource * Improve deserializer --------- Co-authored-by: Artem.Bukhonov <artem.bukhonov@jetbrains.com>
1 parent d4ddb20 commit f8c0d98

5 files changed

Lines changed: 91 additions & 8 deletions

File tree

acp-model/api/acp-model.api

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,8 +790,6 @@ public final class com/agentclientprotocol/model/EmbeddedResource$Companion {
790790

791791
public abstract class com/agentclientprotocol/model/EmbeddedResourceResource : com/agentclientprotocol/model/AcpWithMeta {
792792
public static final field Companion Lcom/agentclientprotocol/model/EmbeddedResourceResource$Companion;
793-
public synthetic fun <init> (ILkotlinx/serialization/internal/SerializationConstructorMarker;)V
794-
public static final synthetic fun write$Self (Lcom/agentclientprotocol/model/EmbeddedResourceResource;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
795793
}
796794

797795
public final class com/agentclientprotocol/model/EmbeddedResourceResource$BlobResourceContents : com/agentclientprotocol/model/EmbeddedResourceResource {

acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Content.kt

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33

44
package com.agentclientprotocol.model
55

6+
import kotlinx.serialization.DeserializationStrategy
67
import kotlinx.serialization.ExperimentalSerializationApi
78
import kotlinx.serialization.SerialName
89
import kotlinx.serialization.Serializable
10+
import kotlinx.serialization.SerializationException
911
import kotlinx.serialization.json.JsonClassDiscriminator
1012
import kotlinx.serialization.json.JsonElement
13+
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
14+
import kotlinx.serialization.json.jsonObject
15+
import kotlinx.serialization.json.jsonPrimitive
16+
17+
/**
18+
* Common discriminator key used for polymorphic serialization in ACP.
19+
*/
20+
internal const val TYPE_DISCRIMINATOR = "type"
1121

1222
/**
1323
* Content blocks represent displayable information in the Agent Client Protocol.
@@ -18,10 +28,10 @@ import kotlinx.serialization.json.JsonElement
1828
* See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
1929
*/
2030
@Serializable
21-
@JsonClassDiscriminator("type")
31+
@JsonClassDiscriminator(TYPE_DISCRIMINATOR)
2232
public sealed class ContentBlock : AcpWithMeta {
2333
public abstract val annotations: Annotations?
24-
34+
2535
/**
2636
* Plain text content
2737
*
@@ -101,7 +111,7 @@ public sealed class ContentBlock : AcpWithMeta {
101111
/**
102112
* Resource content that can be embedded in a message.
103113
*/
104-
@Serializable
114+
@Serializable(with = EmbeddedResourceResourceSerializer::class)
105115
public sealed class EmbeddedResourceResource : AcpWithMeta {
106116
/**
107117
* Text-based resource contents.
@@ -128,6 +138,28 @@ public sealed class EmbeddedResourceResource : AcpWithMeta {
128138
) : EmbeddedResourceResource()
129139
}
130140

141+
/**
142+
* Embedded resources are discriminator-less in the protocol; choose subtype by fields if
143+
* discriminator is absent, but still honor an explicit discriminator when provided.
144+
*/
145+
internal object EmbeddedResourceResourceSerializer :
146+
JsonContentPolymorphicSerializer<EmbeddedResourceResource>(EmbeddedResourceResource::class) {
147+
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<EmbeddedResourceResource> {
148+
val obj = element.jsonObject
149+
150+
val explicitType = obj[TYPE_DISCRIMINATOR]?.jsonPrimitive?.content
151+
when (explicitType) {
152+
EmbeddedResourceResource.TextResourceContents::class.simpleName -> return EmbeddedResourceResource.TextResourceContents.serializer()
153+
EmbeddedResourceResource.BlobResourceContents::class.simpleName -> return EmbeddedResourceResource.BlobResourceContents.serializer()
154+
}
155+
156+
if (EmbeddedResourceResource.TextResourceContents::text.name in obj) return EmbeddedResourceResource.TextResourceContents.serializer()
157+
if (EmbeddedResourceResource.BlobResourceContents::blob.name in obj) return EmbeddedResourceResource.BlobResourceContents.serializer()
158+
159+
throw SerializationException("Cannot determine EmbeddedResourceResource type; expected '${EmbeddedResourceResource.TextResourceContents::text.name}' or '${EmbeddedResourceResource.BlobResourceContents::blob.name}'")
160+
}
161+
}
162+
131163
/**
132164
* The contents of a resource, embedded into a prompt or tool call result.
133165
*/
@@ -151,4 +183,4 @@ public data class ResourceLink(
151183
val title: String? = null,
152184
val annotations: Annotations? = null,
153185
override val _meta: JsonElement? = null
154-
) : AcpWithMeta
186+
) : AcpWithMeta

acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/SessionUpdate.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import kotlinx.serialization.json.JsonElement
1515
* Specifies how the agent should collect input for this command.
1616
*/
1717
@Serializable
18-
@JsonClassDiscriminator("type")
18+
@JsonClassDiscriminator(TYPE_DISCRIMINATOR)
1919
public sealed class AvailableCommandInput {
2020
/**
2121
* All text typed after the command name is provided as unstructured input.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.agentclientprotocol.model
2+
3+
import com.agentclientprotocol.rpc.ACPJson
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertTrue
7+
8+
class EmbeddedResourceResourceTest {
9+
10+
@Test
11+
fun `decodes resource content without discriminator`() {
12+
val payload = """
13+
{
14+
"type": "resource",
15+
"resource": {
16+
"text": "hello",
17+
"uri": "file:///tmp/example.txt"
18+
}
19+
}
20+
""".trimIndent()
21+
22+
val block = ACPJson.decodeFromString(ContentBlock.serializer(), payload)
23+
24+
assertTrue(block is ContentBlock.Resource)
25+
val resource = block.resource
26+
assertTrue(resource is EmbeddedResourceResource.TextResourceContents)
27+
assertEquals("hello", resource.text)
28+
assertEquals("file:///tmp/example.txt", resource.uri)
29+
}
30+
31+
@Test
32+
fun `decodes blob resource content without discriminator`() {
33+
val payload = """
34+
{
35+
"type": "resource",
36+
"resource": {
37+
"blob": "ZGF0YQ==",
38+
"mimeType": "application/octet-stream",
39+
"uri": "file:///tmp/data.bin"
40+
}
41+
}
42+
""".trimIndent()
43+
44+
val block = ACPJson.decodeFromString(ContentBlock.serializer(), payload)
45+
46+
assertTrue(block is ContentBlock.Resource)
47+
val resource = block.resource
48+
assertTrue(resource is EmbeddedResourceResource.BlobResourceContents)
49+
assertEquals("ZGF0YQ==", resource.blob)
50+
assertEquals("application/octet-stream", resource.mimeType)
51+
assertEquals("file:///tmp/data.bin", resource.uri)
52+
}
53+
}

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77
private val buildNumber: String? = System.getenv("GITHUB_RUN_NUMBER")
88
private val isReleasePublication = System.getenv("RELEASE_PUBLICATION")?.toBoolean() ?: false
99

10-
private val baseVersion = "0.9.1"
10+
private val baseVersion = "0.9.2"
1111

1212
allprojects {
1313
group = "com.agentclientprotocol"

0 commit comments

Comments
 (0)