Skip to content

Commit 558bcc5

Browse files
author
Mark Pollack
committed
Add logout and session/delete agent methods
Implement two stable spec methods across all SDK layers, following the session/close template: - logout (clears stored credentials): LogoutRequest/Response, async/sync handler interfaces, builder wiring, client methods, @Logout annotation, resolver - session/delete (permanently deletes a stored session): DeleteSession Request/Response, handler interfaces, builder wiring, client methods, @DeleteSession annotation, resolver; gated on the sessionCapabilities .delete flag Both responses are registered in the DirectResponseHandler whitelist and covered by handler-invocation and serialization tests.
1 parent 1f4b513 commit 558bcc5

13 files changed

Lines changed: 568 additions & 4 deletions

File tree

acp-agent-support/src/main/java/com/agentclientprotocol/sdk/agent/support/AcpAgentSupport.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import com.agentclientprotocol.sdk.agent.support.resolver.CancelNotificationResolver;
2929
import com.agentclientprotocol.sdk.agent.support.resolver.CapabilitiesResolver;
3030
import com.agentclientprotocol.sdk.agent.support.resolver.CloseSessionRequestResolver;
31+
import com.agentclientprotocol.sdk.agent.support.resolver.DeleteSessionRequestResolver;
3132
import com.agentclientprotocol.sdk.agent.support.resolver.ForkSessionRequestResolver;
3233
import com.agentclientprotocol.sdk.agent.support.resolver.InitializeRequestResolver;
3334
import com.agentclientprotocol.sdk.agent.support.resolver.ListSessionsRequestResolver;
3435
import com.agentclientprotocol.sdk.agent.support.resolver.LoadSessionRequestResolver;
36+
import com.agentclientprotocol.sdk.agent.support.resolver.LogoutRequestResolver;
3537
import com.agentclientprotocol.sdk.agent.support.resolver.NewSessionRequestResolver;
3638
import com.agentclientprotocol.sdk.agent.support.resolver.PromptContextResolver;
3739
import com.agentclientprotocol.sdk.agent.support.resolver.PromptRequestResolver;
@@ -42,10 +44,12 @@
4244
import com.agentclientprotocol.sdk.agent.support.resolver.SetSessionModelRequestResolver;
4345
import com.agentclientprotocol.sdk.annotation.Cancel;
4446
import com.agentclientprotocol.sdk.annotation.CloseSession;
47+
import com.agentclientprotocol.sdk.annotation.DeleteSession;
4548
import com.agentclientprotocol.sdk.annotation.ForkSession;
4649
import com.agentclientprotocol.sdk.annotation.Initialize;
4750
import com.agentclientprotocol.sdk.annotation.ListSessions;
4851
import com.agentclientprotocol.sdk.annotation.LoadSession;
52+
import com.agentclientprotocol.sdk.annotation.Logout;
4953
import com.agentclientprotocol.sdk.annotation.NewSession;
5054
import com.agentclientprotocol.sdk.annotation.Prompt;
5155
import com.agentclientprotocol.sdk.annotation.ResumeSession;
@@ -201,6 +205,12 @@ private void wireHandlers(AcpAgent.SyncAgentBuilder agentBuilder) {
201205
java.util.UUID.randomUUID().toString(), null, null));
202206
}
203207

208+
// Logout handler
209+
AcpHandlerMethod logoutHandler = handlers.get("logout");
210+
if (logoutHandler != null) {
211+
agentBuilder.logoutHandler(req -> invokeHandler(logoutHandler, req, null, null, null));
212+
}
213+
204214
// LoadSession handler
205215
AcpHandlerMethod loadSessionHandler = handlers.get("session/load");
206216
if (loadSessionHandler != null) {
@@ -241,6 +251,13 @@ private void wireHandlers(AcpAgent.SyncAgentBuilder agentBuilder) {
241251
req -> invokeHandler(closeSessionHandler, req, req.sessionId(), null, null));
242252
}
243253

254+
// DeleteSession handler
255+
AcpHandlerMethod deleteSessionHandler = handlers.get("session/delete");
256+
if (deleteSessionHandler != null) {
257+
agentBuilder.deleteSessionHandler(
258+
req -> invokeHandler(deleteSessionHandler, req, req.sessionId(), null, null));
259+
}
260+
244261
// ResumeSession handler
245262
AcpHandlerMethod resumeSessionHandler = handlers.get("session/resume");
246263
if (resumeSessionHandler != null) {
@@ -465,6 +482,10 @@ private void discoverHandlers(Class<?> agentClass, Supplier<Object> beanSupplier
465482
handlers.put("initialize", new AcpHandlerMethod(beanSupplier, method, "initialize"));
466483
log.debug("Discovered @Initialize handler: {}", method.getName());
467484
}
485+
if (method.isAnnotationPresent(Logout.class)) {
486+
handlers.put("logout", new AcpHandlerMethod(beanSupplier, method, "logout"));
487+
log.debug("Discovered @Logout handler: {}", method.getName());
488+
}
468489
if (method.isAnnotationPresent(NewSession.class)) {
469490
handlers.put("session/new", new AcpHandlerMethod(beanSupplier, method, "session/new"));
470491
log.debug("Discovered @NewSession handler: {}", method.getName());
@@ -493,6 +514,10 @@ private void discoverHandlers(Class<?> agentClass, Supplier<Object> beanSupplier
493514
handlers.put("session/close", new AcpHandlerMethod(beanSupplier, method, "session/close"));
494515
log.debug("Discovered @CloseSession handler: {}", method.getName());
495516
}
517+
if (method.isAnnotationPresent(DeleteSession.class)) {
518+
handlers.put("session/delete", new AcpHandlerMethod(beanSupplier, method, "session/delete"));
519+
log.debug("Discovered @DeleteSession handler: {}", method.getName());
520+
}
496521
if (method.isAnnotationPresent(ResumeSession.class)) {
497522
handlers.put("session/resume", new AcpHandlerMethod(beanSupplier, method, "session/resume"));
498523
log.debug("Discovered @ResumeSession handler: {}", method.getName());
@@ -517,13 +542,15 @@ private void addDefaultResolvers() {
517542
// Built-in resolvers (order matters - first match wins)
518543
// Custom resolvers added via builder go first
519544
argumentResolvers.addResolver(new InitializeRequestResolver());
545+
argumentResolvers.addResolver(new LogoutRequestResolver());
520546
argumentResolvers.addResolver(new NewSessionRequestResolver());
521547
argumentResolvers.addResolver(new LoadSessionRequestResolver());
522548
argumentResolvers.addResolver(new PromptRequestResolver());
523549
argumentResolvers.addResolver(new SetSessionModeRequestResolver());
524550
argumentResolvers.addResolver(new SetSessionModelRequestResolver());
525551
argumentResolvers.addResolver(new ListSessionsRequestResolver());
526552
argumentResolvers.addResolver(new CloseSessionRequestResolver());
553+
argumentResolvers.addResolver(new DeleteSessionRequestResolver());
527554
argumentResolvers.addResolver(new ResumeSessionRequestResolver());
528555
argumentResolvers.addResolver(new ForkSessionRequestResolver());
529556
argumentResolvers.addResolver(new SetSessionConfigOptionRequestResolver());

acp-agent-support/src/main/java/com/agentclientprotocol/sdk/agent/support/handler/DirectResponseHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import com.agentclientprotocol.sdk.agent.support.AcpInvocationContext;
88
import com.agentclientprotocol.sdk.agent.support.AcpMethodParameter;
99
import com.agentclientprotocol.sdk.spec.AcpSchema.CloseSessionResponse;
10+
import com.agentclientprotocol.sdk.spec.AcpSchema.DeleteSessionResponse;
1011
import com.agentclientprotocol.sdk.spec.AcpSchema.ForkSessionResponse;
1112
import com.agentclientprotocol.sdk.spec.AcpSchema.InitializeResponse;
1213
import com.agentclientprotocol.sdk.spec.AcpSchema.ListSessionsResponse;
1314
import com.agentclientprotocol.sdk.spec.AcpSchema.LoadSessionResponse;
15+
import com.agentclientprotocol.sdk.spec.AcpSchema.LogoutResponse;
1416
import com.agentclientprotocol.sdk.spec.AcpSchema.NewSessionResponse;
1517
import com.agentclientprotocol.sdk.spec.AcpSchema.PromptResponse;
1618
import com.agentclientprotocol.sdk.spec.AcpSchema.ResumeSessionResponse;
@@ -34,13 +36,15 @@ public class DirectResponseHandler implements ReturnValueHandler {
3436
public boolean supportsReturnType(AcpMethodParameter returnType) {
3537
Class<?> type = returnType.getParameterType();
3638
return InitializeResponse.class.isAssignableFrom(type)
39+
|| LogoutResponse.class.isAssignableFrom(type)
3740
|| NewSessionResponse.class.isAssignableFrom(type)
3841
|| LoadSessionResponse.class.isAssignableFrom(type)
3942
|| PromptResponse.class.isAssignableFrom(type)
4043
|| SetSessionModeResponse.class.isAssignableFrom(type)
4144
|| SetSessionModelResponse.class.isAssignableFrom(type)
4245
|| ListSessionsResponse.class.isAssignableFrom(type)
4346
|| CloseSessionResponse.class.isAssignableFrom(type)
47+
|| DeleteSessionResponse.class.isAssignableFrom(type)
4448
|| ResumeSessionResponse.class.isAssignableFrom(type)
4549
|| ForkSessionResponse.class.isAssignableFrom(type)
4650
|| SetSessionConfigOptionResponse.class.isAssignableFrom(type);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.agent.support.resolver;
6+
7+
import com.agentclientprotocol.sdk.agent.support.AcpInvocationContext;
8+
import com.agentclientprotocol.sdk.agent.support.AcpMethodParameter;
9+
import com.agentclientprotocol.sdk.spec.AcpSchema.DeleteSessionRequest;
10+
11+
/**
12+
* Resolves {@link DeleteSessionRequest} parameters in delete session handlers.
13+
*
14+
* @author Mark Pollack
15+
* @since 1.0.0
16+
*/
17+
public class DeleteSessionRequestResolver implements ArgumentResolver {
18+
19+
@Override
20+
public boolean supportsParameter(AcpMethodParameter parameter) {
21+
return DeleteSessionRequest.class.isAssignableFrom(parameter.getParameterType());
22+
}
23+
24+
@Override
25+
public Object resolveArgument(AcpMethodParameter parameter, AcpInvocationContext context) {
26+
Object request = context.getRequest();
27+
if (request instanceof DeleteSessionRequest) {
28+
return request;
29+
}
30+
throw new ArgumentResolutionException(
31+
"Expected DeleteSessionRequest but got: " + (request != null ? request.getClass().getName() : "null"));
32+
}
33+
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.agent.support.resolver;
6+
7+
import com.agentclientprotocol.sdk.agent.support.AcpInvocationContext;
8+
import com.agentclientprotocol.sdk.agent.support.AcpMethodParameter;
9+
import com.agentclientprotocol.sdk.spec.AcpSchema.LogoutRequest;
10+
11+
/**
12+
* Resolves {@link LogoutRequest} parameters in logout handlers.
13+
*
14+
* @author Mark Pollack
15+
* @since 1.0.0
16+
*/
17+
public class LogoutRequestResolver implements ArgumentResolver {
18+
19+
@Override
20+
public boolean supportsParameter(AcpMethodParameter parameter) {
21+
return LogoutRequest.class.isAssignableFrom(parameter.getParameterType());
22+
}
23+
24+
@Override
25+
public Object resolveArgument(AcpMethodParameter parameter, AcpInvocationContext context) {
26+
Object request = context.getRequest();
27+
if (request instanceof LogoutRequest) {
28+
return request;
29+
}
30+
throw new ArgumentResolutionException(
31+
"Expected LogoutRequest but got: " + (request != null ? request.getClass().getName() : "null"));
32+
}
33+
34+
}

acp-agent-support/src/test/java/com/agentclientprotocol/sdk/agent/support/AcpAgentSupportTest.java

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import com.agentclientprotocol.sdk.annotation.AcpAgent;
1313
import com.agentclientprotocol.sdk.annotation.Cancel;
1414
import com.agentclientprotocol.sdk.annotation.CloseSession;
15+
import com.agentclientprotocol.sdk.annotation.DeleteSession;
1516
import com.agentclientprotocol.sdk.annotation.Initialize;
1617
import com.agentclientprotocol.sdk.annotation.ListSessions;
1718
import com.agentclientprotocol.sdk.annotation.LoadSession;
19+
import com.agentclientprotocol.sdk.annotation.Logout;
1820
import com.agentclientprotocol.sdk.annotation.NewSession;
1921
import com.agentclientprotocol.sdk.annotation.Prompt;
2022
import com.agentclientprotocol.sdk.annotation.ResumeSession;
@@ -25,11 +27,15 @@
2527
import com.agentclientprotocol.sdk.spec.AcpSchema.CancelNotification;
2628
import com.agentclientprotocol.sdk.spec.AcpSchema.CloseSessionRequest;
2729
import com.agentclientprotocol.sdk.spec.AcpSchema.CloseSessionResponse;
30+
import com.agentclientprotocol.sdk.spec.AcpSchema.DeleteSessionRequest;
31+
import com.agentclientprotocol.sdk.spec.AcpSchema.DeleteSessionResponse;
2832
import com.agentclientprotocol.sdk.spec.AcpSchema.InitializeRequest;
2933
import com.agentclientprotocol.sdk.spec.AcpSchema.InitializeResponse;
3034
import com.agentclientprotocol.sdk.spec.AcpSchema.ListSessionsRequest;
3135
import com.agentclientprotocol.sdk.spec.AcpSchema.ListSessionsResponse;
3236
import com.agentclientprotocol.sdk.spec.AcpSchema.LoadSessionRequest;
37+
import com.agentclientprotocol.sdk.spec.AcpSchema.LogoutRequest;
38+
import com.agentclientprotocol.sdk.spec.AcpSchema.LogoutResponse;
3339
import com.agentclientprotocol.sdk.spec.AcpSchema.LoadSessionResponse;
3440
import com.agentclientprotocol.sdk.spec.AcpSchema.NewSessionRequest;
3541
import com.agentclientprotocol.sdk.spec.AcpSchema.NewSessionResponse;
@@ -505,6 +511,85 @@ CloseSessionResponse closeSession(CloseSessionRequest req) {
505511
assertThat(resp).isNotNull();
506512
}
507513

514+
@Test
515+
void logoutHandlerInvoked() throws Exception {
516+
AtomicReference<Boolean> loggedOut = new AtomicReference<>(false);
517+
518+
@AcpAgent
519+
class LogoutAgent {
520+
521+
@Initialize
522+
InitializeResponse init() {
523+
return InitializeResponse.ok();
524+
}
525+
526+
@Logout
527+
LogoutResponse logout(LogoutRequest req) {
528+
loggedOut.set(true);
529+
return new LogoutResponse();
530+
}
531+
532+
}
533+
534+
agentSupport = AcpAgentSupport.create(new LogoutAgent())
535+
.transport(transportPair.agentTransport())
536+
.requestTimeout(TIMEOUT)
537+
.build();
538+
539+
agentSupport.start();
540+
Thread.sleep(100);
541+
542+
client = AcpClient.async(transportPair.clientTransport())
543+
.requestTimeout(TIMEOUT)
544+
.build();
545+
546+
client.initialize(new InitializeRequest(1, null)).block(TIMEOUT);
547+
LogoutResponse resp = client.logout(new LogoutRequest()).block(TIMEOUT);
548+
549+
assertThat(loggedOut.get()).isTrue();
550+
assertThat(resp).isNotNull();
551+
}
552+
553+
@Test
554+
void deleteSessionHandlerInvoked() throws Exception {
555+
AtomicReference<String> deletedSessionId = new AtomicReference<>();
556+
557+
@AcpAgent
558+
class DeleteSessionAgent {
559+
560+
@Initialize
561+
InitializeResponse init() {
562+
return InitializeResponse.ok();
563+
}
564+
565+
@DeleteSession
566+
DeleteSessionResponse deleteSession(DeleteSessionRequest req) {
567+
deletedSessionId.set(req.sessionId());
568+
return new DeleteSessionResponse();
569+
}
570+
571+
}
572+
573+
agentSupport = AcpAgentSupport.create(new DeleteSessionAgent())
574+
.transport(transportPair.agentTransport())
575+
.requestTimeout(TIMEOUT)
576+
.build();
577+
578+
agentSupport.start();
579+
Thread.sleep(100);
580+
581+
client = AcpClient.async(transportPair.clientTransport())
582+
.requestTimeout(TIMEOUT)
583+
.build();
584+
585+
client.initialize(new InitializeRequest(1, null)).block(TIMEOUT);
586+
DeleteSessionResponse resp = client.deleteSession(new DeleteSessionRequest("session-to-delete"))
587+
.block(TIMEOUT);
588+
589+
assertThat(deletedSessionId.get()).isEqualTo("session-to-delete");
590+
assertThat(resp).isNotNull();
591+
}
592+
508593
@Test
509594
void resumeSessionHandlerInvoked() throws Exception {
510595
AtomicReference<String> resumedSessionId = new AtomicReference<>();
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2025-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.annotation;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Marks a method as the handler for deleting ACP sessions.
15+
*
16+
* <p>The annotated method handles the {@code session/delete} JSON-RPC method,
17+
* which is called when a client wants to permanently delete a stored session so
18+
* it no longer appears in {@code session/list}.
19+
*
20+
* <p>Only available if the agent advertises the {@code sessionCapabilities.delete}
21+
* capability.
22+
*
23+
* <p>The method can have the following parameters (all optional):
24+
* <ul>
25+
* <li>{@code DeleteSessionRequest} - the delete session request containing the sessionId</li>
26+
* <li>{@code @SessionId String} - the session ID being deleted</li>
27+
* </ul>
28+
*
29+
* <p>The method should return one of:
30+
* <ul>
31+
* <li>{@code DeleteSessionResponse} - the delete session response</li>
32+
* <li>{@code Mono<DeleteSessionResponse>} - for async handling</li>
33+
* <li>{@code void} - an empty response is sent automatically</li>
34+
* </ul>
35+
*
36+
* <p>Example usage:
37+
* <pre>{@code
38+
* @DeleteSession
39+
* public DeleteSessionResponse delete(DeleteSessionRequest req) {
40+
* // Permanently remove the stored session
41+
* return new DeleteSessionResponse();
42+
* }
43+
* }</pre>
44+
*
45+
* @author Mark Pollack
46+
* @since 1.0.0
47+
* @see AcpAgent
48+
* @see CloseSession
49+
*/
50+
@Target(ElementType.METHOD)
51+
@Retention(RetentionPolicy.RUNTIME)
52+
@Documented
53+
public @interface DeleteSession {
54+
55+
}

0 commit comments

Comments
 (0)