Skip to content

Commit 0718837

Browse files
author
Mark Pollack
committed
Add elicitation/create and elicitation/complete (unstable)
Implement ACP elicitation methods — agent-to-client structured user input via forms (JSON Schema) or URLs (out-of-band OAuth-style). Schema types (all @UnstableAcpApi): CreateElicitationRequest/Response, CompleteElicitationNotification, ElicitationSchema with 5 property variants (string, number, integer, boolean, multi-select), EnumOption, ElicitationCapabilities on ClientCapabilities. Agent side: createElicitation() and completeElicitation() on AcpAsyncAgent, PromptContext, and SyncPromptContext with capability check. Client side: createElicitationHandler() on both AsyncSpec and SyncSpec builders. Adds acp-annotations as dependency of acp-core (zero-dep module) so @UnstableAcpApi can be used on protocol types directly. Tests: 6 serialization tests (schema round-trip, property type polymorphism, request/response variants, capabilities) and 1 end-to-end integration test (agent sends form elicitation to client, client accepts with structured response).
1 parent 1822a8c commit 0718837

12 files changed

Lines changed: 680 additions & 3 deletions

File tree

acp-core/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
<description>Core ACP implementation with stdio transport - zero external dependencies beyond Reactor</description>
1818

1919
<dependencies>
20+
<!-- ACP annotations (@UnstableAcpApi, etc.) - zero-dependency module -->
21+
<dependency>
22+
<groupId>com.agentclientprotocol</groupId>
23+
<artifactId>acp-annotations</artifactId>
24+
<version>${project.version}</version>
25+
</dependency>
26+
2027
<!-- Reactor for async operations -->
2128
<dependency>
2229
<groupId>io.projectreactor</groupId>

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,20 @@ public interface AcpAsyncAgent {
125125
*/
126126
Mono<AcpSchema.KillTerminalCommandResponse> killTerminal(AcpSchema.KillTerminalCommandRequest request);
127127

128+
/**
129+
* Requests structured user input from the client via a form or URL.
130+
* @param request The elicitation request
131+
* @return A Mono containing the user's response
132+
*/
133+
Mono<AcpSchema.CreateElicitationResponse> createElicitation(AcpSchema.CreateElicitationRequest request);
134+
135+
/**
136+
* Notifies the client that a URL-mode elicitation has completed.
137+
* @param notification The completion notification
138+
* @return A Mono that completes when the notification is sent
139+
*/
140+
Mono<Void> completeElicitation(AcpSchema.CompleteElicitationNotification notification);
141+
128142
/**
129143
* Closes the agent gracefully, allowing pending operations to complete.
130144
* @return A Mono that completes when the agent is closed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,29 @@ public Mono<AcpSchema.KillTerminalCommandResponse> killTerminal(AcpSchema.KillTe
339339
});
340340
}
341341

342+
@Override
343+
public Mono<AcpSchema.CreateElicitationResponse> createElicitation(
344+
AcpSchema.CreateElicitationRequest request) {
345+
if (session == null) {
346+
return Mono.error(new IllegalStateException("Agent not started"));
347+
}
348+
NegotiatedCapabilities caps = clientCapabilities.get();
349+
if (caps != null && !caps.supportsElicitation()) {
350+
return Mono.error(new AcpCapabilityException("elicitation"));
351+
}
352+
return session.sendRequest(AcpSchema.METHOD_ELICITATION_CREATE, request,
353+
new TypeRef<AcpSchema.CreateElicitationResponse>() {
354+
});
355+
}
356+
357+
@Override
358+
public Mono<Void> completeElicitation(AcpSchema.CompleteElicitationNotification notification) {
359+
if (session == null) {
360+
return Mono.error(new IllegalStateException("Agent not started"));
361+
}
362+
return session.sendNotification(AcpSchema.METHOD_ELICITATION_COMPLETE, notification);
363+
}
364+
342365
@Override
343366
public Mono<Void> closeGracefully() {
344367
return Mono.defer(() -> {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ public Mono<AcpSchema.KillTerminalCommandResponse> killTerminal(AcpSchema.KillTe
106106
return agent.killTerminal(request);
107107
}
108108

109+
@Override
110+
public Mono<AcpSchema.CreateElicitationResponse> createElicitation(
111+
AcpSchema.CreateElicitationRequest request) {
112+
return agent.createElicitation(request);
113+
}
114+
115+
@Override
116+
public Mono<Void> completeElicitation(AcpSchema.CompleteElicitationNotification notification) {
117+
return agent.completeElicitation(notification);
118+
}
119+
109120
@Override
110121
public NegotiatedCapabilities getClientCapabilities() {
111122
return agent.getClientCapabilities();

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ public AcpSchema.KillTerminalCommandResponse killTerminal(AcpSchema.KillTerminal
8080
return asyncContext.killTerminal(request).block();
8181
}
8282

83+
@Override
84+
public AcpSchema.CreateElicitationResponse createElicitation(
85+
AcpSchema.CreateElicitationRequest request) {
86+
return asyncContext.createElicitation(request).block();
87+
}
88+
89+
@Override
90+
public void completeElicitation(AcpSchema.CompleteElicitationNotification notification) {
91+
asyncContext.completeElicitation(notification).block();
92+
}
93+
8394
@Override
8495
public NegotiatedCapabilities getClientCapabilities() {
8596
return asyncContext.getClientCapabilities();

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@ public interface PromptContext {
136136
*/
137137
Mono<AcpSchema.KillTerminalCommandResponse> killTerminal(AcpSchema.KillTerminalCommandRequest request);
138138

139+
// ========================================================================
140+
// Elicitation
141+
// ========================================================================
142+
143+
/**
144+
* Requests structured user input from the client via a form or URL.
145+
* @param request The elicitation request
146+
* @return A Mono containing the user's response
147+
*/
148+
Mono<AcpSchema.CreateElicitationResponse> createElicitation(AcpSchema.CreateElicitationRequest request);
149+
150+
/**
151+
* Notifies the client that a URL-mode elicitation has completed.
152+
* @param notification The completion notification
153+
* @return A Mono that completes when the notification is sent
154+
*/
155+
Mono<Void> completeElicitation(AcpSchema.CompleteElicitationNotification notification);
156+
139157
// ========================================================================
140158
// Client Capabilities
141159
// ========================================================================

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,24 @@ public interface SyncPromptContext {
133133
*/
134134
AcpSchema.KillTerminalCommandResponse killTerminal(AcpSchema.KillTerminalCommandRequest request);
135135

136+
// ========================================================================
137+
// Elicitation
138+
// ========================================================================
139+
140+
/**
141+
* Requests structured user input from the client via a form or URL.
142+
* Blocks until the user responds.
143+
* @param request The elicitation request
144+
* @return The user's response
145+
*/
146+
AcpSchema.CreateElicitationResponse createElicitation(AcpSchema.CreateElicitationRequest request);
147+
148+
/**
149+
* Notifies the client that a URL-mode elicitation has completed.
150+
* @param notification The completion notification
151+
*/
152+
void completeElicitation(AcpSchema.CompleteElicitationNotification notification);
153+
136154
// ========================================================================
137155
// Client Capabilities
138156
// ========================================================================

acp-core/src/main/java/com/agentclientprotocol/sdk/capabilities/NegotiatedCapabilities.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.agentclientprotocol.sdk.spec.AcpSchema.FileSystemCapability;
1111
import com.agentclientprotocol.sdk.spec.AcpSchema.McpCapabilities;
1212
import com.agentclientprotocol.sdk.spec.AcpSchema.PromptCapabilities;
13+
import com.agentclientprotocol.sdk.spec.AcpSchema.ElicitationCapabilities;
1314
import com.agentclientprotocol.sdk.spec.AcpSchema.SessionCapabilities;
1415

1516
/**
@@ -60,6 +61,13 @@ public final class NegotiatedCapabilities {
6061

6162
private final boolean terminal;
6263

64+
// Client elicitation capabilities
65+
private final boolean elicitation;
66+
67+
private final boolean elicitationForm;
68+
69+
private final boolean elicitationUrl;
70+
6371
// Agent capabilities (what agent offers to client)
6472
private final boolean loadSession;
6573

@@ -83,6 +91,9 @@ private NegotiatedCapabilities(Builder builder) {
8391
this.readTextFile = builder.readTextFile;
8492
this.writeTextFile = builder.writeTextFile;
8593
this.terminal = builder.terminal;
94+
this.elicitation = builder.elicitation;
95+
this.elicitationForm = builder.elicitationForm;
96+
this.elicitationUrl = builder.elicitationUrl;
8697
this.loadSession = builder.loadSession;
8798
this.listSessions = builder.listSessions;
8899
this.closeSession = builder.closeSession;
@@ -114,6 +125,13 @@ public static NegotiatedCapabilities fromClient(ClientCapabilities caps) {
114125

115126
builder.terminal(Boolean.TRUE.equals(caps.terminal()));
116127

128+
ElicitationCapabilities elicit = caps.elicitation();
129+
if (elicit != null) {
130+
builder.elicitation(true);
131+
builder.elicitationForm(elicit.form() != null);
132+
builder.elicitationUrl(elicit.url() != null);
133+
}
134+
117135
return builder.build();
118136
}
119137

@@ -211,6 +229,40 @@ public void requireTerminal() {
211229
}
212230
}
213231

232+
/**
233+
* Returns true if the client supports elicitation (any mode).
234+
* @return true if elicitation capability was advertised
235+
*/
236+
public boolean supportsElicitation() {
237+
return elicitation;
238+
}
239+
240+
/**
241+
* Returns true if the client supports form-based elicitation.
242+
* @return true if elicitation.form capability was advertised
243+
*/
244+
public boolean supportsElicitationForm() {
245+
return elicitationForm;
246+
}
247+
248+
/**
249+
* Returns true if the client supports URL-based elicitation.
250+
* @return true if elicitation.url capability was advertised
251+
*/
252+
public boolean supportsElicitationUrl() {
253+
return elicitationUrl;
254+
}
255+
256+
/**
257+
* Requires elicitation capability, throwing if not supported.
258+
* @throws AcpCapabilityException if the client doesn't support elicitation
259+
*/
260+
public void requireElicitation() {
261+
if (!elicitation) {
262+
throw new AcpCapabilityException("elicitation");
263+
}
264+
}
265+
214266
// --------------------------
215267
// Agent Capability Checks (for clients)
216268
// --------------------------
@@ -350,7 +402,9 @@ public void requireAudioContent() {
350402
@Override
351403
public String toString() {
352404
return "NegotiatedCapabilities{" + "readTextFile=" + readTextFile + ", writeTextFile=" + writeTextFile
353-
+ ", terminal=" + terminal + ", loadSession=" + loadSession + ", listSessions=" + listSessions
405+
+ ", terminal=" + terminal + ", elicitation=" + elicitation + ", elicitationForm="
406+
+ elicitationForm + ", elicitationUrl=" + elicitationUrl + ", loadSession=" + loadSession
407+
+ ", listSessions=" + listSessions
354408
+ ", closeSession=" + closeSession + ", resumeSession=" + resumeSession + ", imageContent="
355409
+ imageContent + ", audioContent=" + audioContent + ", embeddedContext=" + embeddedContext
356410
+ ", mcpHttp=" + mcpHttp + ", mcpSse=" + mcpSse + '}';
@@ -367,6 +421,12 @@ public static class Builder {
367421

368422
private boolean terminal = false;
369423

424+
private boolean elicitation = false;
425+
426+
private boolean elicitationForm = false;
427+
428+
private boolean elicitationUrl = false;
429+
370430
private boolean loadSession = false;
371431

372432
private boolean listSessions = false;
@@ -400,6 +460,21 @@ public Builder terminal(boolean value) {
400460
return this;
401461
}
402462

463+
public Builder elicitation(boolean value) {
464+
this.elicitation = value;
465+
return this;
466+
}
467+
468+
public Builder elicitationForm(boolean value) {
469+
this.elicitationForm = value;
470+
return this;
471+
}
472+
473+
public Builder elicitationUrl(boolean value) {
474+
this.elicitationUrl = value;
475+
return this;
476+
}
477+
403478
public Builder loadSession(boolean value) {
404479
this.loadSession = value;
405480
return this;

acp-core/src/main/java/com/agentclientprotocol/sdk/client/AcpClient.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,26 @@ public AsyncSpec killTerminalHandler(
452452
return this;
453453
}
454454

455+
/**
456+
* Adds a typed handler for elicitation requests from the agent.
457+
* The agent uses elicitation to request structured user input via forms.
458+
*
459+
* @param handler The typed handler function that processes elicitation requests
460+
* @return This builder instance for method chaining
461+
* @throws IllegalArgumentException if handler is null
462+
*/
463+
public AsyncSpec createElicitationHandler(
464+
Function<AcpSchema.CreateElicitationRequest, Mono<AcpSchema.CreateElicitationResponse>> handler) {
465+
Assert.notNull(handler, "Create elicitation handler must not be null");
466+
AcpClientSession.RequestHandler<AcpSchema.CreateElicitationResponse> rawHandler = params -> {
467+
AcpSchema.CreateElicitationRequest request = transport.unmarshalFrom(params,
468+
new TypeRef<AcpSchema.CreateElicitationRequest>() {});
469+
return handler.apply(request);
470+
};
471+
this.requestHandlers.put(AcpSchema.METHOD_ELICITATION_CREATE, rawHandler);
472+
return this;
473+
}
474+
455475
/**
456476
* Adds a consumer to be notified when session update notifications are received
457477
* from the agent. Session updates include agent thoughts, message chunks, and
@@ -833,6 +853,27 @@ public SyncSpec killTerminalHandler(
833853
return this;
834854
}
835855

856+
/**
857+
* Adds a typed handler for elicitation requests from the agent.
858+
* The agent uses elicitation to request structured user input via forms.
859+
*
860+
* @param handler The typed handler function that processes elicitation requests
861+
* @return This builder instance for method chaining
862+
* @throws IllegalArgumentException if handler is null
863+
*/
864+
public SyncSpec createElicitationHandler(
865+
Function<AcpSchema.CreateElicitationRequest, AcpSchema.CreateElicitationResponse> handler) {
866+
Assert.notNull(handler, "Create elicitation handler must not be null");
867+
SyncRequestHandler<AcpSchema.CreateElicitationResponse> rawHandler = params -> {
868+
logger.debug("createElicitation request params: {}", params);
869+
AcpSchema.CreateElicitationRequest request = transport.unmarshalFrom(params,
870+
new TypeRef<AcpSchema.CreateElicitationRequest>() {});
871+
return handler.apply(request);
872+
};
873+
this.requestHandlers.put(AcpSchema.METHOD_ELICITATION_CREATE, fromSync(rawHandler));
874+
return this;
875+
}
876+
836877
/**
837878
* Adds a synchronous consumer to be notified when session update notifications
838879
* are received from the agent. This is the preferred method for sync clients.

0 commit comments

Comments
 (0)