Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 208 additions & 9 deletions acp-model/api/acp-model.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ public data class AuthCapabilities(
override val _meta: JsonElement? = null
): AcpWithMeta

/**
* **UNSTABLE**
*
* This capability is not part of the spec yet, and may be removed or changed at any point.
*
* Plan capabilities supported by the client.
*
* When present, signals that the client supports `plan_update` and `plan_removed`
* session update types. When absent, the agent must fall back to the existing
* `plan` session update type.
*/
@UnstableApi
@Serializable
public data class PlanCapabilities(
override val _meta: JsonElement? = null
) : AcpWithMeta

/**
* Capabilities supported by the client.
*
Expand All @@ -68,6 +85,8 @@ public data class ClientCapabilities(
@EncodeDefault val fs: FileSystemCapability? = null,
@EncodeDefault val terminal: Boolean = false,
@property:UnstableApi
val planCapabilities: PlanCapabilities? = null,
@property:UnstableApi
@EncodeDefault val auth: AuthCapabilities? = null,
@property:UnstableApi
val nes: ClientNesCapabilities? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
@file:Suppress("unused")
@file:OptIn(ExperimentalSerializationApi::class)

package com.agentclientprotocol.model

import com.agentclientprotocol.annotations.UnstableApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
import kotlinx.serialization.json.JsonElement

/**
Expand Down Expand Up @@ -64,4 +68,56 @@ public data class PlanEntry(
public data class Plan(
val entries: List<PlanEntry>,
override val _meta: JsonElement? = null
) : AcpWithMeta
) : AcpWithMeta

/**
* **UNSTABLE**
*
* This capability is not part of the spec yet, and may be removed or changed at any point.
*
* A plan variant that supports multiple formats and plan identity.
*
* Used with the `plan_update` session update type to provide richer plan
* representations including structured items, file references, and markdown.
* Each variant carries a required `id` for tracking multiple concurrent plans.
*/
@UnstableApi
@Serializable
@JsonClassDiscriminator(TYPE_DISCRIMINATOR)
public sealed class PlanVariant {
public abstract val id: String
public abstract val _meta: JsonElement?

/**
* Structured plan entries (same semantics as the existing `plan` session update).
*/
@Serializable
@SerialName("items")
public data class Items(
override val id: String,
val entries: List<PlanEntry>,
override val _meta: JsonElement? = null
) : PlanVariant(), AcpWithMeta

/**
* A plan provided as a file URI.
*/
@Serializable
@SerialName("file")
public data class File(
override val id: String,
val uri: String,
override val _meta: JsonElement? = null
) : PlanVariant(), AcpWithMeta

/**
* A plan provided as raw markdown text.
*/
@Serializable
@SerialName("markdown")
public data class Markdown(
override val id: String,
val content: String,
override val _meta: JsonElement? = null
) : PlanVariant(), AcpWithMeta
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,44 @@ public sealed class SessionUpdate {
override val _meta: JsonElement? = null
) : SessionUpdate(), AcpWithMeta

/**
* **UNSTABLE**
*
* This capability is not part of the spec yet, and may be removed or changed at any point.
*
* A plan update that supports multiple plan formats and concurrent plans.
*
* Uses the `plan_update` session update type. Requires the client to advertise
* `planCapabilities` in `ClientCapabilities`.
*
* See [PlanVariant] for available plan formats.
*/
@UnstableApi
@Serializable
@SerialName("plan_update")
public data class PlanUpdateV2(
val plan: PlanVariant,
override val _meta: JsonElement? = null
) : SessionUpdate(), AcpWithMeta

/**
* **UNSTABLE**
*
* This capability is not part of the spec yet, and may be removed or changed at any point.
*
* Signals that a plan has been dismissed/removed.
*
* Uses the `plan_removed` session update type. The `id` identifies
* which plan to remove.
*/
@UnstableApi
@Serializable
@SerialName("plan_removed")
public data class PlanRemoved(
val id: String,
override val _meta: JsonElement? = null
) : SessionUpdate(), AcpWithMeta

/**
* Available commands are ready or have changed
*/
Expand Down Expand Up @@ -301,6 +339,20 @@ internal object SessionUpdateSerializer : KSerializer<SessionUpdate> {
base.forEach { (k, v) -> put(k, v) }
}
}
is SessionUpdate.PlanUpdateV2 -> {
val base = ACPJson.encodeToJsonElement(SessionUpdate.PlanUpdateV2.serializer(), value).jsonObject
buildJsonObject {
put(SESSION_UPDATE_DISCRIMINATOR, "plan_update")
base.forEach { (k, v) -> put(k, v) }
}
}
is SessionUpdate.PlanRemoved -> {
val base = ACPJson.encodeToJsonElement(SessionUpdate.PlanRemoved.serializer(), value).jsonObject
buildJsonObject {
put(SESSION_UPDATE_DISCRIMINATOR, "plan_removed")
base.forEach { (k, v) -> put(k, v) }
}
}
is SessionUpdate.AvailableCommandsUpdate -> {
val base = ACPJson.encodeToJsonElement(SessionUpdate.AvailableCommandsUpdate.serializer(), value).jsonObject
buildJsonObject {
Expand Down Expand Up @@ -380,6 +432,14 @@ internal object SessionUpdateSerializer : KSerializer<SessionUpdate> {
SessionUpdate.PlanUpdate.serializer(),
jsonObject
)
"plan_update" -> ACPJson.decodeFromJsonElement(
SessionUpdate.PlanUpdateV2.serializer(),
jsonObject
)
"plan_removed" -> ACPJson.decodeFromJsonElement(
SessionUpdate.PlanRemoved.serializer(),
jsonObject
)
"available_commands_update" -> ACPJson.decodeFromJsonElement(
SessionUpdate.AvailableCommandsUpdate.serializer(),
jsonObject
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@

package com.agentclientprotocol.model

import com.agentclientprotocol.annotations.UnstableApi
import com.agentclientprotocol.rpc.ACPJson
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

@OptIn(UnstableApi::class)
class SerializationTests {

@Test
Expand Down Expand Up @@ -210,4 +213,48 @@ class SerializationTests {
assertEquals(StopReason.END_TURN, response.stopReason)
assertNull(response.usage)
}

@Test
fun `decodes ClientCapabilities with planCapabilities`() {
val payload = """
{
"fs": {"readTextFile": true, "writeTextFile": false},
"terminal": true,
"planCapabilities": {}
}
""".trimIndent()

val capabilities = ACPJson.decodeFromString(ClientCapabilities.serializer(), payload)

assertTrue(capabilities.planCapabilities != null)
assertTrue(capabilities.terminal)
}

@Test
fun `decodes ClientCapabilities without planCapabilities`() {
val payload = """
{
"fs": {"readTextFile": true, "writeTextFile": false},
"terminal": false
}
""".trimIndent()

val capabilities = ACPJson.decodeFromString(ClientCapabilities.serializer(), payload)

assertNull(capabilities.planCapabilities)
}

@Test
fun `round-trip serialization for ClientCapabilities with planCapabilities`() {
val original = ClientCapabilities(
terminal = true,
planCapabilities = PlanCapabilities()
)

val encoded = ACPJson.encodeToString(ClientCapabilities.serializer(), original)
assertTrue(encoded.contains("\"planCapabilities\""))

val decoded = ACPJson.decodeFromString(ClientCapabilities.serializer(), encoded)
assertTrue(decoded.planCapabilities != null)
}
}
Loading
Loading