diff --git a/acp-ktor-test/src/commonTest/kotlin/com/agentclientprotocol/FeaturesTest.kt b/acp-ktor-test/src/commonTest/kotlin/com/agentclientprotocol/FeaturesTest.kt index d80f757..6ca2b83 100644 --- a/acp-ktor-test/src/commonTest/kotlin/com/agentclientprotocol/FeaturesTest.kt +++ b/acp-ktor-test/src/commonTest/kotlin/com/agentclientprotocol/FeaturesTest.kt @@ -192,6 +192,37 @@ abstract class FeaturesTest(protocolDriver: ProtocolDriver) : ProtocolDriver by + @Test + fun `authenticate returns null when agent returns null`() = testWithProtocols { clientProtocol, agentProtocol -> + val client = Client(protocol = clientProtocol) + + val agentSupport = object : AgentSupport { + override suspend fun initialize(clientInfo: ClientInfo): AgentInfo { + return AgentInfo(clientInfo.protocolVersion) + } + + override suspend fun authenticate(methodId: AuthMethodId, _meta: JsonElement?): AuthenticateResponse? { + // Return null to simulate {"jsonrpc":"2.0","id":3,"result":null} response + return null + } + + override suspend fun createSession(sessionParameters: SessionCreationParameters): AgentSession { + return TestAgentSession() + } + + override suspend fun loadSession(sessionId: SessionId, sessionParameters: SessionCreationParameters): AgentSession { + return TestAgentSession() + } + } + Agent(agentProtocol, agentSupport) + + client.initialize(ClientInfo()) + + // This should correctly handle the null response without throwing an exception + val result = client.authenticate(AuthMethodId("test-auth")) + assertNull(result, "authenticate should return null when agent returns null") + } + // @Test // fun `call agent extension from client`(): TestResult = testWithProtocols { clientProtocol, agentProtocol -> // diff --git a/acp-model/api/acp-model.api b/acp-model/api/acp-model.api index 3ab25c3..3bb4a89 100644 --- a/acp-model/api/acp-model.api +++ b/acp-model/api/acp-model.api @@ -30,6 +30,12 @@ public class com/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod : public final fun getResponseSerializer ()Lkotlinx/serialization/KSerializer; } +public class com/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod : com/agentclientprotocol/model/AcpMethod { + public fun (Ljava/lang/String;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;)V + public final fun getRequestSerializer ()Lkotlinx/serialization/KSerializer; + public final fun getResponseSerializer ()Lkotlinx/serialization/KSerializer; +} + public class com/agentclientprotocol/model/AcpMethod$AcpSessionNotificationMethod : com/agentclientprotocol/model/AcpMethod$AcpNotificationMethod { public fun (Ljava/lang/String;Lkotlinx/serialization/KSerializer;)V } @@ -38,11 +44,15 @@ public class com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseMe public fun (Ljava/lang/String;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;)V } +public class com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseNullableMethod : com/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod { + public fun (Ljava/lang/String;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/KSerializer;)V +} + public final class com/agentclientprotocol/model/AcpMethod$AgentMethods { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$AgentMethods; } -public final class com/agentclientprotocol/model/AcpMethod$AgentMethods$Authenticate : com/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod { +public final class com/agentclientprotocol/model/AcpMethod$AgentMethods$Authenticate : com/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$AgentMethods$Authenticate; } @@ -66,11 +76,11 @@ public final class com/agentclientprotocol/model/AcpMethod$AgentMethods$SessionP public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$AgentMethods$SessionPrompt; } -public final class com/agentclientprotocol/model/AcpMethod$AgentMethods$SessionSetMode : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseMethod { +public final class com/agentclientprotocol/model/AcpMethod$AgentMethods$SessionSetMode : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseNullableMethod { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$AgentMethods$SessionSetMode; } -public final class com/agentclientprotocol/model/AcpMethod$AgentMethods$SessionSetModel : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseMethod { +public final class com/agentclientprotocol/model/AcpMethod$AgentMethods$SessionSetModel : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseNullableMethod { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$AgentMethods$SessionSetModel; } @@ -82,7 +92,7 @@ public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$FsReadT public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$ClientMethods$FsReadTextFile; } -public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$FsWriteTextFile : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseMethod { +public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$FsWriteTextFile : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseNullableMethod { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$ClientMethods$FsWriteTextFile; } @@ -98,7 +108,7 @@ public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$Termina public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalCreate; } -public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalKill : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseMethod { +public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalKill : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseNullableMethod { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalKill; } @@ -106,11 +116,11 @@ public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$Termina public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalOutput; } -public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalRelease : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseMethod { +public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalRelease : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseNullableMethod { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalRelease; } -public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalWaitForExit : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseMethod { +public final class com/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalWaitForExit : com/agentclientprotocol/model/AcpMethod$AcpSessionRequestResponseNullableMethod { public static final field INSTANCE Lcom/agentclientprotocol/model/AcpMethod$ClientMethods$TerminalWaitForExit; } diff --git a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Methods.kt b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Methods.kt index e39fb2a..d1ceaf7 100644 --- a/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Methods.kt +++ b/acp-model/src/commonMain/kotlin/com/agentclientprotocol/model/Methods.kt @@ -20,12 +20,24 @@ public open class AcpMethod(public val methodName: MethodName) { public val responseSerializer: KSerializer ) : AcpMethod(MethodName(method)) + public open class AcpRequestResponseNullableMethod( + method: String, + public val requestSerializer: KSerializer, + public val responseSerializer: KSerializer + ) : AcpMethod(MethodName(method)) + public open class AcpSessionRequestResponseMethod(method: String, requestSerializer: KSerializer, responseSerializer: KSerializer ) : AcpRequestResponseMethod(method, requestSerializer, responseSerializer) where TRequest : AcpRequest, TRequest : AcpWithSessionId + public open class AcpSessionRequestResponseNullableMethod(method: String, + requestSerializer: KSerializer, + responseSerializer: KSerializer + ) : AcpRequestResponseNullableMethod(method, requestSerializer, responseSerializer) + where TRequest : AcpRequest, TRequest : AcpWithSessionId + public open class AcpNotificationMethod( method: String, public val serializer: KSerializer, @@ -43,16 +55,16 @@ public open class AcpMethod(public val methodName: MethodName) { public object AgentMethods { // Agent-side operations (methods that agents can call on clients) public object Initialize : AcpRequestResponseMethod("initialize", InitializeRequest.serializer(), InitializeResponse.serializer()) - public object Authenticate : AcpRequestResponseMethod("authenticate", AuthenticateRequest.serializer(), AuthenticateResponse.serializer()) + public object Authenticate : AcpRequestResponseNullableMethod("authenticate", AuthenticateRequest.serializer(), AuthenticateResponse.serializer()) public object SessionNew : AcpRequestResponseMethod("session/new", NewSessionRequest.serializer(), NewSessionResponse.serializer()) public object SessionLoad : AcpRequestResponseMethod("session/load", LoadSessionRequest.serializer(), LoadSessionResponse.serializer()) // session specific public object SessionPrompt : AcpSessionRequestResponseMethod("session/prompt", PromptRequest.serializer(), PromptResponse.serializer()) public object SessionCancel : AcpSessionNotificationMethod("session/cancel", CancelNotification.serializer()) - public object SessionSetMode : AcpSessionRequestResponseMethod("session/set_mode", SetSessionModeRequest.serializer(), SetSessionModeResponse.serializer()) + public object SessionSetMode : AcpSessionRequestResponseNullableMethod("session/set_mode", SetSessionModeRequest.serializer(), SetSessionModeResponse.serializer()) @UnstableApi - public object SessionSetModel : AcpSessionRequestResponseMethod("session/set_model", SetSessionModelRequest.serializer(), SetSessionModelResponse.serializer()) + public object SessionSetModel : AcpSessionRequestResponseNullableMethod("session/set_model", SetSessionModelRequest.serializer(), SetSessionModelResponse.serializer()) } public object ClientMethods { @@ -62,12 +74,12 @@ public open class AcpMethod(public val methodName: MethodName) { // extensions public object FsReadTextFile : AcpSessionRequestResponseMethod("fs/read_text_file", ReadTextFileRequest.serializer(), ReadTextFileResponse.serializer()) - public object FsWriteTextFile : AcpSessionRequestResponseMethod("fs/write_text_file", WriteTextFileRequest.serializer(), WriteTextFileResponse.serializer()) + public object FsWriteTextFile : AcpSessionRequestResponseNullableMethod("fs/write_text_file", WriteTextFileRequest.serializer(), WriteTextFileResponse.serializer()) public object TerminalCreate : AcpSessionRequestResponseMethod("terminal/create", CreateTerminalRequest.serializer(), CreateTerminalResponse.serializer()) public object TerminalOutput : AcpSessionRequestResponseMethod("terminal/output", TerminalOutputRequest.serializer(), TerminalOutputResponse.serializer()) - public object TerminalRelease : AcpSessionRequestResponseMethod("terminal/release", ReleaseTerminalRequest.serializer(), ReleaseTerminalResponse.serializer()) - public object TerminalWaitForExit : AcpSessionRequestResponseMethod("terminal/wait_for_exit", WaitForTerminalExitRequest.serializer(), WaitForTerminalExitResponse.serializer()) - public object TerminalKill : AcpSessionRequestResponseMethod("terminal/kill", KillTerminalCommandRequest.serializer(), KillTerminalCommandResponse.serializer()) + public object TerminalRelease : AcpSessionRequestResponseNullableMethod("terminal/release", ReleaseTerminalRequest.serializer(), ReleaseTerminalResponse.serializer()) + public object TerminalWaitForExit : AcpSessionRequestResponseNullableMethod("terminal/wait_for_exit", WaitForTerminalExitRequest.serializer(), WaitForTerminalExitResponse.serializer()) + public object TerminalKill : AcpSessionRequestResponseNullableMethod("terminal/kill", KillTerminalCommandRequest.serializer(), KillTerminalCommandResponse.serializer()) } diff --git a/acp/api/acp.api b/acp/api/acp.api index f75626e..21a5ad5 100644 --- a/acp/api/acp.api +++ b/acp/api/acp.api @@ -272,6 +272,7 @@ public final class com/agentclientprotocol/protocol/Protocol : com/agentclientpr public fun sendRequestRaw-TF-lsE4 (Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun setNotificationHandlerRaw (Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V public fun setRequestHandlerRaw (Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V + public fun setRequestHandlerRaw (Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V public final fun start ()V public fun toString ()Ljava/lang/String; } @@ -293,13 +294,17 @@ public final class com/agentclientprotocol/protocol/Protocol_extensionsKt { public static final fun getJsonRpcRequest (Lkotlin/coroutines/CoroutineContext;)Lcom/agentclientprotocol/rpc/JsonRpcRequest; public static final fun invoke (Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpNotification;)V public static final fun invoke (Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun invoke (Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun sendNotification (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lcom/agentclientprotocol/model/AcpNotification;)V public static synthetic fun sendNotification$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lcom/agentclientprotocol/model/AcpNotification;ILjava/lang/Object;)V public static final fun sendRequest (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lcom/agentclientprotocol/model/AcpRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun sendRequestNullable (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lcom/agentclientprotocol/model/AcpRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun setNotificationHandler (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V public static synthetic fun setNotificationHandler$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public static final fun setRequestHandler (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V + public static final fun setRequestHandler (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V public static synthetic fun setRequestHandler$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun setRequestHandler$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } public abstract interface class com/agentclientprotocol/protocol/RpcMethodsOperations { @@ -310,7 +315,9 @@ public abstract interface class com/agentclientprotocol/protocol/RpcMethodsOpera public abstract fun setNotificationHandlerRaw (Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V public static synthetic fun setNotificationHandlerRaw$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public abstract fun setRequestHandlerRaw (Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V + public abstract fun setRequestHandlerRaw (Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)V public static synthetic fun setRequestHandlerRaw$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun setRequestHandlerRaw$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } public final class com/agentclientprotocol/protocol/RpcMethodsOperations$DefaultImpls { @@ -318,6 +325,7 @@ public final class com/agentclientprotocol/protocol/RpcMethodsOperations$Default public static synthetic fun sendRequestRaw-TF-lsE4$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun setNotificationHandlerRaw$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpNotificationMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public static synthetic fun setRequestHandlerRaw$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun setRequestHandlerRaw$default (Lcom/agentclientprotocol/protocol/RpcMethodsOperations;Lcom/agentclientprotocol/model/AcpMethod$AcpRequestResponseNullableMethod;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } public abstract class com/agentclientprotocol/transport/BaseTransport : com/agentclientprotocol/transport/Transport { diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/AgentSupport.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/AgentSupport.kt index 4bd99b0..3e6c437 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/AgentSupport.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/AgentSupport.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.json.JsonElement public interface AgentSupport { public suspend fun initialize(clientInfo: ClientInfo): AgentInfo - public suspend fun authenticate(methodId: AuthMethodId, _meta: JsonElement?): AuthenticateResponse = AuthenticateResponse() + public suspend fun authenticate(methodId: AuthMethodId, _meta: JsonElement?): AuthenticateResponse? = AuthenticateResponse() public suspend fun createSession(sessionParameters: SessionCreationParameters): AgentSession public suspend fun loadSession(sessionId: SessionId, sessionParameters: SessionCreationParameters): AgentSession } \ No newline at end of file diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/RemoteClientSessionOperations.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/RemoteClientSessionOperations.kt index 947fb66..503b8e3 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/RemoteClientSessionOperations.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/agent/RemoteClientSessionOperations.kt @@ -36,7 +36,7 @@ internal class RemoteClientSessionOperations(private val rpc: RpcMethodsOperatio path: String, content: String, _meta: JsonElement?, - ): WriteTextFileResponse { + ): WriteTextFileResponse? { if (clientCapabilities.fs?.writeTextFile != true) error("Client does not support fs.writeTextFile capability") return AcpMethod.ClientMethods.FsWriteTextFile(rpc, WriteTextFileRequest(sessionId, path, content, _meta)) } @@ -64,7 +64,7 @@ internal class RemoteClientSessionOperations(private val rpc: RpcMethodsOperatio override suspend fun terminalRelease( terminalId: String, _meta: JsonElement?, - ): ReleaseTerminalResponse { + ): ReleaseTerminalResponse? { if (!clientCapabilities.terminal) error("Client does not support terminal capability") return AcpMethod.ClientMethods.TerminalRelease(rpc, ReleaseTerminalRequest(sessionId, terminalId, _meta)) } @@ -72,7 +72,7 @@ internal class RemoteClientSessionOperations(private val rpc: RpcMethodsOperatio override suspend fun terminalWaitForExit( terminalId: String, _meta: JsonElement?, - ): WaitForTerminalExitResponse { + ): WaitForTerminalExitResponse? { if (!clientCapabilities.terminal) error("Client does not support terminal capability") return AcpMethod.ClientMethods.TerminalWaitForExit(rpc, WaitForTerminalExitRequest(sessionId, terminalId, _meta)) } @@ -80,7 +80,7 @@ internal class RemoteClientSessionOperations(private val rpc: RpcMethodsOperatio override suspend fun terminalKill( terminalId: String, _meta: JsonElement?, - ): KillTerminalCommandResponse { + ): KillTerminalCommandResponse? { if (!clientCapabilities.terminal) error("Client does not support terminal capability") return AcpMethod.ClientMethods.TerminalKill(rpc, KillTerminalCommandRequest(sessionId, terminalId, _meta)) } diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/client/Client.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/client/Client.kt index 0841b58..e180bc1 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/client/Client.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/client/Client.kt @@ -148,7 +148,7 @@ public class Client( * Performs authentication of the agent with the specified [methodId]. * The method may throw an exception if the authentication fails. */ - public suspend fun authenticate(methodId: AuthMethodId, _meta: JsonElement? = null): AuthenticateResponse { + public suspend fun authenticate(methodId: AuthMethodId, _meta: JsonElement? = null): AuthenticateResponse? { return AcpMethod.AgentMethods.Authenticate(protocol, AuthenticateRequest(methodId, _meta)) } diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSession.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSession.kt index 0c2564e..ca03429 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSession.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSession.kt @@ -49,7 +49,7 @@ public interface ClientSession { /** * Changes the session mode to the specified mode. The real change will be reported by an agent via [currentMode] and [ClientSessionOperations.notify]. */ - public suspend fun setMode(modeId: SessionModeId, _meta: JsonElement? = null): SetSessionModeResponse + public suspend fun setMode(modeId: SessionModeId, _meta: JsonElement? = null): SetSessionModeResponse? /** * The flag indicates whether the agent supports the session model changing. @@ -75,5 +75,5 @@ public interface ClientSession { * Changes the session model to the specified model. The real change will be reported by an agent via [currentModel] and [ClientSessionOperations.notify]. */ @UnstableApi - public suspend fun setModel(modelId: ModelId, _meta: JsonElement? = null): SetSessionModelResponse + public suspend fun setModel(modelId: ModelId, _meta: JsonElement? = null): SetSessionModelResponse? } \ No newline at end of file diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSessionImpl.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSessionImpl.kt index bd2135e..a5b4d23 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSessionImpl.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/client/ClientSessionImpl.kt @@ -96,7 +96,7 @@ internal class ClientSessionImpl( get() = _currentMode - override suspend fun setMode(modeId: SessionModeId, _meta: JsonElement?): SetSessionModeResponse { + override suspend fun setMode(modeId: SessionModeId, _meta: JsonElement?): SetSessionModeResponse? { return AcpMethod.AgentMethods.SessionSetMode(protocol, SetSessionModeRequest(sessionId, modeId, _meta)) } @@ -113,7 +113,7 @@ internal class ClientSessionImpl( get() = _currentModel @UnstableApi - override suspend fun setModel(modelId: ModelId, _meta: JsonElement?): SetSessionModelResponse { + override suspend fun setModel(modelId: ModelId, _meta: JsonElement?): SetSessionModelResponse? { return AcpMethod.AgentMethods.SessionSetModel(protocol, SetSessionModelRequest(sessionId, modelId, _meta)) } diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/common/FileSystemOperations.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/common/FileSystemOperations.kt index 262124a..7840589 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/common/FileSystemOperations.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/common/FileSystemOperations.kt @@ -13,7 +13,7 @@ public interface FileSystemOperations { } public suspend fun fsWriteTextFile(path: String, content: String, - _meta: JsonElement? = null): WriteTextFileResponse { + _meta: JsonElement? = null): WriteTextFileResponse? { throw NotImplementedError("Must be implemented by client when advertising fs.writeTextFile capability") } } \ No newline at end of file diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/common/TerminalOperations.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/common/TerminalOperations.kt index c170456..9af9e8c 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/common/TerminalOperations.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/common/TerminalOperations.kt @@ -19,17 +19,17 @@ public interface TerminalOperations { } public suspend fun terminalRelease(terminalId: String, - _meta: JsonElement? = null): ReleaseTerminalResponse { + _meta: JsonElement? = null): ReleaseTerminalResponse? { throw NotImplementedError("Must be implemented by client when advertising terminal capability") } public suspend fun terminalWaitForExit(terminalId: String, - _meta: JsonElement? = null): WaitForTerminalExitResponse { + _meta: JsonElement? = null): WaitForTerminalExitResponse? { throw NotImplementedError("Must be implemented by client when advertising terminal capability") } public suspend fun terminalKill(terminalId: String, - _meta: JsonElement? = null): KillTerminalCommandResponse { + _meta: JsonElement? = null): KillTerminalCommandResponse? { throw NotImplementedError("Must be implemented by client when advertising terminal capability") } } \ No newline at end of file diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.extensions.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.extensions.kt index cb488de..533622e 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.extensions.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.extensions.kt @@ -7,6 +7,7 @@ import com.agentclientprotocol.model.AcpResponse import com.agentclientprotocol.rpc.ACPJson import com.agentclientprotocol.rpc.JsonRpcRequest import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement import kotlin.coroutines.AbstractCoroutineContextElement @@ -26,6 +27,16 @@ public suspend fun RpcMethodsOp return ACPJson.decodeFromJsonElement(method.responseSerializer, responseJson) } +public suspend fun RpcMethodsOperations.sendRequestNullable( + method: AcpMethod.AcpRequestResponseNullableMethod, + request: TRequest? +): TResponse? { + val params = request?.let { ACPJson.encodeToJsonElement(method.requestSerializer, request) } + val responseJson = this.sendRequestRaw(method.methodName, params) + if (responseJson is JsonNull) return null + return ACPJson.decodeFromJsonElement(method.responseSerializer, responseJson) +} + /** * Send a notification (no response expected). */ @@ -51,6 +62,21 @@ public fun RpcMethodsOperations. ACPJson.encodeToJsonElement(method.responseSerializer, responseObject) } } + +/** + * Register a handler for incoming requests. + */ +public fun RpcMethodsOperations.setRequestHandler( + method: AcpMethod.AcpRequestResponseNullableMethod, + additionalContext: CoroutineContext = EmptyCoroutineContext, + handler: suspend (TRequest) -> TResponse? +) { + this.setRequestHandlerRaw(method, additionalContext) { request -> + val requestParams = ACPJson.decodeFromJsonElement(method.requestSerializer, request.params ?: JsonNull) + val responseObject = handler(requestParams) + responseObject?.let { ACPJson.encodeToJsonElement(method.responseSerializer, responseObject) } ?: JsonNull + } +} /** * Register a handler for incoming notifications. */ @@ -69,6 +95,10 @@ public suspend operator fun AcpMe return rpc.sendRequest(this, request) } +public suspend operator fun AcpMethod.AcpRequestResponseNullableMethod.invoke(rpc: RpcMethodsOperations, request: TRequest): TResponse? { + return rpc.sendRequestNullable(this, request) +} + public operator fun AcpMethod.AcpNotificationMethod.invoke(rpc: RpcMethodsOperations, notification: TNotification) { return rpc.sendNotification(this, notification) } diff --git a/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.kt b/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.kt index b372ab5..334c9eb 100644 --- a/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.kt +++ b/acp/src/commonMain/kotlin/com/agentclientprotocol/protocol/Protocol.kt @@ -87,6 +87,12 @@ public interface RpcMethodsOperations { handler: suspend (JsonRpcRequest) -> JsonElement? ) + public fun setRequestHandlerRaw( + method: AcpMethod.AcpRequestResponseNullableMethod<*, *>, + additionalContext: CoroutineContext = EmptyCoroutineContext, + handler: suspend (JsonRpcRequest) -> JsonElement? + ) + public fun setNotificationHandlerRaw( method: AcpMethod.AcpNotificationMethod<*>, additionalContext: CoroutineContext = EmptyCoroutineContext, @@ -264,6 +270,22 @@ public class Protocol( method: AcpMethod.AcpRequestResponseMethod<*, *>, additionalContext: CoroutineContext, handler: suspend (JsonRpcRequest) -> JsonElement? + ) { + doSetRequestHandlerRaw(method, additionalContext, handler) + } + + override fun setRequestHandlerRaw( + method: AcpMethod.AcpRequestResponseNullableMethod<*, *>, + additionalContext: CoroutineContext, + handler: suspend (JsonRpcRequest) -> JsonElement? + ) { + doSetRequestHandlerRaw(method, additionalContext, handler) + } + + private fun doSetRequestHandlerRaw( + method: AcpMethod, + additionalContext: CoroutineContext, + handler: suspend (JsonRpcRequest) -> JsonElement? ) { requestHandlers.update { it.put(method.methodName) { params -> diff --git a/build.gradle.kts b/build.gradle.kts index b011328..fe7cdd7 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.10.2" +private val baseVersion = "0.10.3" allprojects { group = "com.agentclientprotocol" diff --git a/samples/kotlin-acp-client-sample/src/main/kotlin/com/agentclientprotocol/samples/SimpleAgentSupport.kt b/samples/kotlin-acp-client-sample/src/main/kotlin/com/agentclientprotocol/samples/SimpleAgentSupport.kt index a77b3a1..b3ed8a5 100644 --- a/samples/kotlin-acp-client-sample/src/main/kotlin/com/agentclientprotocol/samples/SimpleAgentSupport.kt +++ b/samples/kotlin-acp-client-sample/src/main/kotlin/com/agentclientprotocol/samples/SimpleAgentSupport.kt @@ -180,7 +180,7 @@ class SimpleAgentSession( emit(Event.SessionUpdateEvent( SessionUpdate.AgentMessageChunk( - ContentBlock.Text("\nTerminal output: ${outputResponse.output} (exit code: ${exitResponse.exitCode})") + ContentBlock.Text("\nTerminal output: ${outputResponse.output} (exit code: ${exitResponse?.exitCode})") ) )) } catch (e: Exception) {