From b94a7b3ed2a0d9cda211c25c2c486039cd411375 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Wed, 13 May 2026 13:44:03 +0200 Subject: [PATCH] Implement token usage from session-usage rfd Co-Authored-By: Claude Sonnet 4.6 --- acp-model/api/acp-model.api | 52 ++++++++++++-- .../com/agentclientprotocol/model/Requests.kt | 21 ++++++ .../model/SerializationTests.kt | 70 +++++++++++++++++++ 3 files changed, 138 insertions(+), 5 deletions(-) diff --git a/acp-model/api/acp-model.api b/acp-model/api/acp-model.api index 1aa53cc..97caf46 100644 --- a/acp-model/api/acp-model.api +++ b/acp-model/api/acp-model.api @@ -4885,15 +4885,17 @@ public final class com/agentclientprotocol/model/PromptRequest$Companion { public final class com/agentclientprotocol/model/PromptResponse : com/agentclientprotocol/model/AcpResponse { public static final field Companion Lcom/agentclientprotocol/model/PromptResponse$Companion; - public synthetic fun (Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lcom/agentclientprotocol/model/Usage;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lcom/agentclientprotocol/model/Usage;Lkotlinx/serialization/json/JsonElement;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lcom/agentclientprotocol/model/StopReason; public final fun component2-f2XpNSU ()Ljava/lang/String; - public final fun component3 ()Lkotlinx/serialization/json/JsonElement; - public final fun copy-5MJPFCQ (Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;)Lcom/agentclientprotocol/model/PromptResponse; - public static synthetic fun copy-5MJPFCQ$default (Lcom/agentclientprotocol/model/PromptResponse;Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILjava/lang/Object;)Lcom/agentclientprotocol/model/PromptResponse; + public final fun component3 ()Lcom/agentclientprotocol/model/Usage; + public final fun component4 ()Lkotlinx/serialization/json/JsonElement; + public final fun copy-hdWohc0 (Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lcom/agentclientprotocol/model/Usage;Lkotlinx/serialization/json/JsonElement;)Lcom/agentclientprotocol/model/PromptResponse; + public static synthetic fun copy-hdWohc0$default (Lcom/agentclientprotocol/model/PromptResponse;Lcom/agentclientprotocol/model/StopReason;Ljava/lang/String;Lcom/agentclientprotocol/model/Usage;Lkotlinx/serialization/json/JsonElement;ILjava/lang/Object;)Lcom/agentclientprotocol/model/PromptResponse; public fun equals (Ljava/lang/Object;)Z public final fun getStopReason ()Lcom/agentclientprotocol/model/StopReason; + public final fun getUsage ()Lcom/agentclientprotocol/model/Usage; public final fun getUserMessageId-f2XpNSU ()Ljava/lang/String; public fun get_meta ()Lkotlinx/serialization/json/JsonElement; public fun hashCode ()I @@ -7483,6 +7485,46 @@ public final class com/agentclientprotocol/model/UrlElicitationRequiredItem$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class com/agentclientprotocol/model/Usage : com/agentclientprotocol/model/AcpWithMeta { + public static final field Companion Lcom/agentclientprotocol/model/Usage$Companion; + public fun (JJJLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonElement;)V + public synthetic fun (JJJLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component2 ()J + public final fun component3 ()J + public final fun component4 ()Ljava/lang/Long; + public final fun component5 ()Ljava/lang/Long; + public final fun component6 ()Ljava/lang/Long; + public final fun component7 ()Lkotlinx/serialization/json/JsonElement; + public final fun copy (JJJLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonElement;)Lcom/agentclientprotocol/model/Usage; + public static synthetic fun copy$default (Lcom/agentclientprotocol/model/Usage;JJJLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonElement;ILjava/lang/Object;)Lcom/agentclientprotocol/model/Usage; + public fun equals (Ljava/lang/Object;)Z + public final fun getCachedReadTokens ()Ljava/lang/Long; + public final fun getCachedWriteTokens ()Ljava/lang/Long; + public final fun getInputTokens ()J + public final fun getOutputTokens ()J + public final fun getThoughtTokens ()Ljava/lang/Long; + public final fun getTotalTokens ()J + public fun get_meta ()Lkotlinx/serialization/json/JsonElement; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class com/agentclientprotocol/model/Usage$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lcom/agentclientprotocol/model/Usage$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/agentclientprotocol/model/Usage; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/agentclientprotocol/model/Usage;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class com/agentclientprotocol/model/Usage$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class com/agentclientprotocol/model/WaitForTerminalExitRequest : com/agentclientprotocol/model/AcpRequest, com/agentclientprotocol/model/AcpWithSessionId { public static final field Companion Lcom/agentclientprotocol/model/WaitForTerminalExitRequest$Companion; public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Requests.kt b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Requests.kt index 69ceb16..b930d3f 100644 --- a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Requests.kt +++ b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Requests.kt @@ -687,6 +687,25 @@ public data class NewSessionResponse( override val _meta: JsonElement? = null ) : AcpCreatedSessionResponse, AcpResponse, AcpWithSessionId +/** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Token usage for a prompt turn. + */ +@UnstableApi +@Serializable +public data class Usage( + val inputTokens: Long, + val outputTokens: Long, + val totalTokens: Long, + val thoughtTokens: Long? = null, + val cachedReadTokens: Long? = null, + val cachedWriteTokens: Long? = null, + override val _meta: JsonElement? = null +) : AcpWithMeta + /** * Response from processing a user prompt. * @@ -698,6 +717,8 @@ public data class PromptResponse( val stopReason: StopReason, @property:UnstableApi val userMessageId: MessageId? = null, + @property:UnstableApi + val usage: Usage? = null, override val _meta: JsonElement? = null ) : AcpResponse diff --git a/acp-model/src/commonTest/kotlin/com/agentclientprotocol/model/SerializationTests.kt b/acp-model/src/commonTest/kotlin/com/agentclientprotocol/model/SerializationTests.kt index f5c00ed..bcbf4fb 100644 --- a/acp-model/src/commonTest/kotlin/com/agentclientprotocol/model/SerializationTests.kt +++ b/acp-model/src/commonTest/kotlin/com/agentclientprotocol/model/SerializationTests.kt @@ -3,6 +3,7 @@ package com.agentclientprotocol.model import com.agentclientprotocol.rpc.ACPJson import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNull import kotlin.test.assertTrue class SerializationTests { @@ -140,4 +141,73 @@ class SerializationTests { assertEquals(500000L, update.size) assertEquals(null, update.cost) } + + @Test + fun `decodes PromptResponse with usage`() { + val payload = """ + { + "stopReason": "end_turn", + "usage": { + "inputTokens": 100, + "outputTokens": 200, + "totalTokens": 300, + "thoughtTokens": 50, + "cachedReadTokens": 10, + "cachedWriteTokens": 5 + } + } + """.trimIndent() + + val response = ACPJson.decodeFromString(PromptResponse.serializer(), payload) + + assertEquals(StopReason.END_TURN, response.stopReason) + val usage = response.usage + assertTrue(usage != null) + assertEquals(100L, usage.inputTokens) + assertEquals(200L, usage.outputTokens) + assertEquals(300L, usage.totalTokens) + assertEquals(50L, usage.thoughtTokens) + assertEquals(10L, usage.cachedReadTokens) + assertEquals(5L, usage.cachedWriteTokens) + } + + @Test + fun `decodes PromptResponse with usage omitting optional fields`() { + val payload = """ + { + "stopReason": "end_turn", + "usage": { + "inputTokens": 100, + "outputTokens": 200, + "totalTokens": 300 + } + } + """.trimIndent() + + val response = ACPJson.decodeFromString(PromptResponse.serializer(), payload) + + assertEquals(StopReason.END_TURN, response.stopReason) + val usage = response.usage + assertTrue(usage != null) + assertEquals(100L, usage.inputTokens) + assertEquals(200L, usage.outputTokens) + assertEquals(300L, usage.totalTokens) + assertNull(usage.thoughtTokens) + assertNull(usage.cachedReadTokens) + assertNull(usage.cachedWriteTokens) + } + + @Test + fun `decodes PromptResponse without usage`() { + val payload = """ + { + "stopReason": "end_turn" + } + """.trimIndent() + + val response = ACPJson.decodeFromString(PromptResponse.serializer(), payload) + + assertEquals(StopReason.END_TURN, response.stopReason) + assertNull(response.usage) + } }