Skip to content

Commit 7c1549f

Browse files
author
Mark Pollack
committed
Add PromptContext for prompt handlers and fix sync API parity
- Add PromptContext interface providing all agent capabilities to prompt handlers - Add SyncPromptContext for blocking prompt handlers - Add DefaultPromptContext and DefaultSyncPromptContext implementations - Update handler signatures to use PromptContext instead of SessionUpdateSender - Remove deprecated SessionUpdateSender and SyncSessionUpdateSender interfaces - Add getClientCapabilities() to AcpSyncAgent - Add getAgentCapabilities() to AcpSyncClient - Add unit tests for sync capability accessor methods This follows the MCP SDK's Exchange pattern where handlers receive a context object with all necessary capabilities.
1 parent 2cb8f7e commit 7c1549f

File tree

10 files changed

+610
-47
lines changed

10 files changed

+610
-47
lines changed

acp-core/src/main/java/com/agentclientprotocol/sdk/agent/AcpAgent.java

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -170,33 +170,38 @@ interface LoadSessionHandler {
170170
}
171171

172172
/**
173-
* Interface for sending session updates during prompt processing.
174-
*/
175-
interface SessionUpdateSender {
176-
177-
/**
178-
* Sends a session update notification to the client.
179-
* @param sessionId The session ID
180-
* @param update The session update to send
181-
* @return A Mono that completes when the notification is sent
182-
*/
183-
Mono<Void> sendUpdate(String sessionId, AcpSchema.SessionUpdate update);
184-
185-
}
186-
187-
/**
188-
* Functional interface for handling prompt requests with streaming updates.
173+
* Functional interface for handling prompt requests with full agent context.
174+
*
175+
* <p>
176+
* The handler receives a {@link PromptContext} that provides access to all agent
177+
* capabilities including file operations, permission requests, terminal operations,
178+
* and session updates.
179+
*
180+
* <p>Example usage:
181+
* <pre>{@code
182+
* AcpAgent.async(transport)
183+
* .promptHandler((request, context) -> {
184+
* // Read a file
185+
* var file = context.readTextFile(new ReadTextFileRequest(...)).block();
186+
*
187+
* // Send progress update
188+
* context.sendUpdate(sessionId, new AgentThoughtChunk(...));
189+
*
190+
* return Mono.just(new PromptResponse(StopReason.END_TURN));
191+
* })
192+
* .build();
193+
* }</pre>
189194
*/
190195
@FunctionalInterface
191196
interface PromptHandler {
192197

193198
/**
194-
* Handles a prompt request, optionally sending session updates during processing.
199+
* Handles a prompt request with full access to agent capabilities.
195200
* @param request The prompt request
196-
* @param updater Interface for sending session updates
201+
* @param context Context providing all agent capabilities (file ops, permissions, updates, etc.)
197202
* @return A Mono containing the prompt response
198203
*/
199-
Mono<AcpSchema.PromptResponse> handle(AcpSchema.PromptRequest request, SessionUpdateSender updater);
204+
Mono<AcpSchema.PromptResponse> handle(AcpSchema.PromptRequest request, PromptContext context);
200205

201206
}
202207

@@ -279,34 +284,38 @@ interface SyncLoadSessionHandler {
279284
}
280285

281286
/**
282-
* Synchronous interface for sending session updates during prompt processing.
283-
* Blocks until the update is sent, returns void.
284-
*/
285-
interface SyncSessionUpdateSender {
286-
287-
/**
288-
* Sends a session update notification to the client. Blocks until sent.
289-
* @param sessionId The session ID
290-
* @param update The session update to send
291-
*/
292-
void sendUpdate(String sessionId, AcpSchema.SessionUpdate update);
293-
294-
}
295-
296-
/**
297-
* Synchronous functional interface for handling prompt requests with streaming updates.
298-
* Returns a plain value instead of Mono for use with sync agents.
287+
* Synchronous functional interface for handling prompt requests with full agent context.
288+
*
289+
* <p>
290+
* The handler receives a {@link SyncPromptContext} that provides blocking access to all
291+
* agent capabilities including file operations, permission requests, terminal operations,
292+
* and session updates.
293+
*
294+
* <p>Example usage:
295+
* <pre>{@code
296+
* AcpAgent.sync(transport)
297+
* .promptHandler((request, context) -> {
298+
* // Read a file (blocks)
299+
* var file = context.readTextFile(new ReadTextFileRequest(...));
300+
*
301+
* // Send progress update (blocks)
302+
* context.sendUpdate(sessionId, new AgentThoughtChunk(...));
303+
*
304+
* return new PromptResponse(StopReason.END_TURN);
305+
* })
306+
* .build();
307+
* }</pre>
299308
*/
300309
@FunctionalInterface
301310
interface SyncPromptHandler {
302311

303312
/**
304-
* Handles a prompt request, optionally sending session updates during processing.
313+
* Handles a prompt request with full access to agent capabilities.
305314
* @param request The prompt request
306-
* @param updater Interface for sending session updates (blocking)
315+
* @param context Context providing blocking access to all agent capabilities
307316
* @return The prompt response
308317
*/
309-
AcpSchema.PromptResponse handle(AcpSchema.PromptRequest request, SyncSessionUpdateSender updater);
318+
AcpSchema.PromptResponse handle(AcpSchema.PromptRequest request, SyncPromptContext context);
310319

311320
}
312321

@@ -557,7 +566,7 @@ public SyncAgentBuilder loadSessionHandler(SyncLoadSessionHandler handler) {
557566

558567
/**
559568
* Sets the synchronous handler for prompt requests.
560-
* @param handler The sync prompt handler (returns plain value, uses SyncSessionUpdateSender)
569+
* @param handler The sync prompt handler (returns plain value, receives SyncPromptContext)
561570
* @return This builder for chaining
562571
*/
563572
public SyncAgentBuilder promptHandler(SyncPromptHandler handler) {
@@ -644,11 +653,10 @@ private static PromptHandler fromSync(SyncPromptHandler syncHandler) {
644653
if (syncHandler == null) {
645654
return null;
646655
}
647-
return (request, asyncUpdater) -> Mono.fromCallable(() -> {
648-
// Create a blocking wrapper around the async SessionUpdateSender
649-
SyncSessionUpdateSender syncUpdater = (sessionId, update) -> asyncUpdater.sendUpdate(sessionId, update)
650-
.block();
651-
return syncHandler.handle(request, syncUpdater);
656+
return (request, asyncContext) -> Mono.fromCallable(() -> {
657+
// Create a blocking wrapper around the async PromptContext
658+
SyncPromptContext syncContext = new DefaultSyncPromptContext(asyncContext);
659+
return syncHandler.handle(request, syncContext);
652660
}).subscribeOn(SYNC_HANDLER_SCHEDULER);
653661
}
654662

acp-core/src/main/java/com/agentclientprotocol/sdk/agent/AcpSyncAgent.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ public AcpSchema.KillTerminalCommandResponse killTerminal(AcpSchema.KillTerminal
170170
return asyncAgent.killTerminal(request).block(blockTimeout);
171171
}
172172

173+
/**
174+
* Returns the capabilities negotiated with the client during initialization.
175+
*
176+
* <p>
177+
* This method returns null if initialization has not been completed yet.
178+
* Use this to check what features the client supports before calling
179+
* methods like {@link #readTextFile} or {@link #createTerminal}.
180+
* </p>
181+
* @return the negotiated client capabilities, or null if not initialized
182+
*/
183+
public com.agentclientprotocol.sdk.capabilities.NegotiatedCapabilities getClientCapabilities() {
184+
return asyncAgent.getClientCapabilities();
185+
}
186+
173187
/**
174188
* Returns the underlying async agent.
175189
* @return The async agent

acp-core/src/main/java/com/agentclientprotocol/sdk/agent/DefaultAcpAsyncAgent.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,15 @@ public Mono<Void> start() {
130130
});
131131
}
132132

133-
// Prompt handler - uses the session update sender
133+
// Prompt handler - provides full PromptContext with all agent capabilities
134134
if (promptHandler != null) {
135135
requestHandlers.put(AcpSchema.METHOD_SESSION_PROMPT, params -> {
136136
AcpSchema.PromptRequest request = transport.unmarshalFrom(params,
137137
new TypeRef<AcpSchema.PromptRequest>() {
138138
});
139-
return promptHandler.handle(request, (sessionId, update) -> sendSessionUpdate(sessionId, update))
139+
// Create PromptContext that wraps this agent, giving handler access to all capabilities
140+
PromptContext context = new DefaultPromptContext(this);
141+
return promptHandler.handle(request, context)
140142
.cast(Object.class);
141143
});
142144
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.agent;
6+
7+
import com.agentclientprotocol.sdk.capabilities.NegotiatedCapabilities;
8+
import com.agentclientprotocol.sdk.spec.AcpSchema;
9+
import reactor.core.publisher.Mono;
10+
11+
/**
12+
* Default implementation of {@link PromptContext} that delegates to an {@link AcpAsyncAgent}.
13+
*
14+
* <p>
15+
* This class is created internally by {@link DefaultAcpAsyncAgent} and passed to prompt handlers.
16+
* It provides a clean interface for handlers to access all agent capabilities without needing
17+
* a direct reference to the agent instance.
18+
*
19+
* @author Mark Pollack
20+
* @since 0.9.1
21+
*/
22+
class DefaultPromptContext implements PromptContext {
23+
24+
private final AcpAsyncAgent agent;
25+
26+
/**
27+
* Creates a new prompt context wrapping the given agent.
28+
* @param agent The agent to delegate to
29+
*/
30+
DefaultPromptContext(AcpAsyncAgent agent) {
31+
this.agent = agent;
32+
}
33+
34+
@Override
35+
public Mono<Void> sendUpdate(String sessionId, AcpSchema.SessionUpdate update) {
36+
return agent.sendSessionUpdate(sessionId, update);
37+
}
38+
39+
@Override
40+
public Mono<AcpSchema.ReadTextFileResponse> readTextFile(AcpSchema.ReadTextFileRequest request) {
41+
return agent.readTextFile(request);
42+
}
43+
44+
@Override
45+
public Mono<AcpSchema.WriteTextFileResponse> writeTextFile(AcpSchema.WriteTextFileRequest request) {
46+
return agent.writeTextFile(request);
47+
}
48+
49+
@Override
50+
public Mono<AcpSchema.RequestPermissionResponse> requestPermission(AcpSchema.RequestPermissionRequest request) {
51+
return agent.requestPermission(request);
52+
}
53+
54+
@Override
55+
public Mono<AcpSchema.CreateTerminalResponse> createTerminal(AcpSchema.CreateTerminalRequest request) {
56+
return agent.createTerminal(request);
57+
}
58+
59+
@Override
60+
public Mono<AcpSchema.TerminalOutputResponse> getTerminalOutput(AcpSchema.TerminalOutputRequest request) {
61+
return agent.getTerminalOutput(request);
62+
}
63+
64+
@Override
65+
public Mono<AcpSchema.ReleaseTerminalResponse> releaseTerminal(AcpSchema.ReleaseTerminalRequest request) {
66+
return agent.releaseTerminal(request);
67+
}
68+
69+
@Override
70+
public Mono<AcpSchema.WaitForTerminalExitResponse> waitForTerminalExit(
71+
AcpSchema.WaitForTerminalExitRequest request) {
72+
return agent.waitForTerminalExit(request);
73+
}
74+
75+
@Override
76+
public Mono<AcpSchema.KillTerminalCommandResponse> killTerminal(AcpSchema.KillTerminalCommandRequest request) {
77+
return agent.killTerminal(request);
78+
}
79+
80+
@Override
81+
public NegotiatedCapabilities getClientCapabilities() {
82+
return agent.getClientCapabilities();
83+
}
84+
85+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.agent;
6+
7+
import com.agentclientprotocol.sdk.capabilities.NegotiatedCapabilities;
8+
import com.agentclientprotocol.sdk.spec.AcpSchema;
9+
10+
/**
11+
* Default implementation of {@link SyncPromptContext} that wraps an async {@link PromptContext}
12+
* and provides blocking methods.
13+
*
14+
* <p>
15+
* This class is created internally by the sync-to-async handler converter in {@link AcpAgent.SyncAgentBuilder}.
16+
*
17+
* @author Mark Pollack
18+
* @since 0.9.1
19+
*/
20+
class DefaultSyncPromptContext implements SyncPromptContext {
21+
22+
private final PromptContext asyncContext;
23+
24+
/**
25+
* Creates a new sync context wrapping the given async context.
26+
* @param asyncContext The async context to wrap
27+
*/
28+
DefaultSyncPromptContext(PromptContext asyncContext) {
29+
this.asyncContext = asyncContext;
30+
}
31+
32+
@Override
33+
public void sendUpdate(String sessionId, AcpSchema.SessionUpdate update) {
34+
asyncContext.sendUpdate(sessionId, update).block();
35+
}
36+
37+
@Override
38+
public AcpSchema.ReadTextFileResponse readTextFile(AcpSchema.ReadTextFileRequest request) {
39+
return asyncContext.readTextFile(request).block();
40+
}
41+
42+
@Override
43+
public AcpSchema.WriteTextFileResponse writeTextFile(AcpSchema.WriteTextFileRequest request) {
44+
return asyncContext.writeTextFile(request).block();
45+
}
46+
47+
@Override
48+
public AcpSchema.RequestPermissionResponse requestPermission(AcpSchema.RequestPermissionRequest request) {
49+
return asyncContext.requestPermission(request).block();
50+
}
51+
52+
@Override
53+
public AcpSchema.CreateTerminalResponse createTerminal(AcpSchema.CreateTerminalRequest request) {
54+
return asyncContext.createTerminal(request).block();
55+
}
56+
57+
@Override
58+
public AcpSchema.TerminalOutputResponse getTerminalOutput(AcpSchema.TerminalOutputRequest request) {
59+
return asyncContext.getTerminalOutput(request).block();
60+
}
61+
62+
@Override
63+
public AcpSchema.ReleaseTerminalResponse releaseTerminal(AcpSchema.ReleaseTerminalRequest request) {
64+
return asyncContext.releaseTerminal(request).block();
65+
}
66+
67+
@Override
68+
public AcpSchema.WaitForTerminalExitResponse waitForTerminalExit(AcpSchema.WaitForTerminalExitRequest request) {
69+
return asyncContext.waitForTerminalExit(request).block();
70+
}
71+
72+
@Override
73+
public AcpSchema.KillTerminalCommandResponse killTerminal(AcpSchema.KillTerminalCommandRequest request) {
74+
return asyncContext.killTerminal(request).block();
75+
}
76+
77+
@Override
78+
public NegotiatedCapabilities getClientCapabilities() {
79+
return asyncContext.getClientCapabilities();
80+
}
81+
82+
}

0 commit comments

Comments
 (0)