Skip to content

Commit 1f4b513

Browse files
author
Mark Pollack
committed
Add additionalDirectories and per-chunk messageId fields
Catch up to ACP spec v0.13.6 stable additions: - additionalDirectories (List<String>) on NewSessionRequest, LoadSessionRequest, ResumeSessionRequest, ForkSessionRequest, and SessionInfo - messageId on UserMessageChunk, AgentMessageChunk, AgentThoughtChunk - delete and additionalDirectories capabilities on SessionCapabilities, wired through NegotiatedCapabilities (supports*/require* accessors) Existing convenience constructors are preserved, so no caller changes are required.
1 parent f681eab commit 1f4b513

4 files changed

Lines changed: 202 additions & 15 deletions

File tree

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ public final class NegotiatedCapabilities {
7777

7878
private final boolean resumeSession;
7979

80+
private final boolean deleteSession;
81+
82+
private final boolean additionalDirectories;
83+
8084
private final boolean forkSession;
8185

8286
private final boolean imageContent;
@@ -100,6 +104,8 @@ private NegotiatedCapabilities(Builder builder) {
100104
this.listSessions = builder.listSessions;
101105
this.closeSession = builder.closeSession;
102106
this.resumeSession = builder.resumeSession;
107+
this.deleteSession = builder.deleteSession;
108+
this.additionalDirectories = builder.additionalDirectories;
103109
this.forkSession = builder.forkSession;
104110
this.imageContent = builder.imageContent;
105111
this.audioContent = builder.audioContent;
@@ -156,6 +162,8 @@ public static NegotiatedCapabilities fromAgent(AgentCapabilities caps) {
156162
builder.listSessions(sc.list() != null);
157163
builder.closeSession(sc.close() != null);
158164
builder.resumeSession(sc.resume() != null);
165+
builder.deleteSession(sc.delete() != null);
166+
builder.additionalDirectories(sc.additionalDirectories() != null);
159167
builder.forkSession(sc.fork() != null);
160168
}
161169

@@ -303,6 +311,23 @@ public boolean supportsResumeSession() {
303311
return resumeSession;
304312
}
305313

314+
/**
315+
* Returns true if the agent supports deleting sessions.
316+
* @return true if sessionCapabilities.delete was advertised
317+
*/
318+
public boolean supportsDeleteSession() {
319+
return deleteSession;
320+
}
321+
322+
/**
323+
* Returns true if the agent supports additional workspace directories on session
324+
* lifecycle requests (session/new, session/load, session/resume).
325+
* @return true if sessionCapabilities.additionalDirectories was advertised
326+
*/
327+
public boolean supportsAdditionalDirectories() {
328+
return additionalDirectories;
329+
}
330+
306331
/**
307332
* Returns true if the agent supports forking sessions.
308333
* @return true if sessionCapabilities.fork was advertised
@@ -391,6 +416,26 @@ public void requireResumeSession() {
391416
}
392417
}
393418

419+
/**
420+
* Requires delete session capability, throwing if not supported.
421+
* @throws AcpCapabilityException if the agent doesn't support this capability
422+
*/
423+
public void requireDeleteSession() {
424+
if (!deleteSession) {
425+
throw new AcpCapabilityException("sessionCapabilities.delete");
426+
}
427+
}
428+
429+
/**
430+
* Requires additional directories capability, throwing if not supported.
431+
* @throws AcpCapabilityException if the agent doesn't support this capability
432+
*/
433+
public void requireAdditionalDirectories() {
434+
if (!additionalDirectories) {
435+
throw new AcpCapabilityException("sessionCapabilities.additionalDirectories");
436+
}
437+
}
438+
394439
public void requireForkSession() {
395440
if (!forkSession) {
396441
throw new AcpCapabilityException("sessionCapabilities.fork");
@@ -423,7 +468,8 @@ public String toString() {
423468
+ ", terminal=" + terminal + ", elicitation=" + elicitation + ", elicitationForm="
424469
+ elicitationForm + ", elicitationUrl=" + elicitationUrl + ", loadSession=" + loadSession
425470
+ ", listSessions=" + listSessions
426-
+ ", closeSession=" + closeSession + ", resumeSession=" + resumeSession + ", forkSession="
471+
+ ", closeSession=" + closeSession + ", resumeSession=" + resumeSession + ", deleteSession="
472+
+ deleteSession + ", additionalDirectories=" + additionalDirectories + ", forkSession="
427473
+ forkSession + ", imageContent="
428474
+ imageContent + ", audioContent=" + audioContent + ", embeddedContext=" + embeddedContext
429475
+ ", mcpHttp=" + mcpHttp + ", mcpSse=" + mcpSse + '}';
@@ -454,6 +500,10 @@ public static class Builder {
454500

455501
private boolean resumeSession = false;
456502

503+
private boolean deleteSession = false;
504+
505+
private boolean additionalDirectories = false;
506+
457507
private boolean forkSession = false;
458508

459509
private boolean imageContent = false;
@@ -516,6 +566,16 @@ public Builder resumeSession(boolean value) {
516566
return this;
517567
}
518568

569+
public Builder deleteSession(boolean value) {
570+
this.deleteSession = value;
571+
return this;
572+
}
573+
574+
public Builder additionalDirectories(boolean value) {
575+
this.additionalDirectories = value;
576+
return this;
577+
}
578+
519579
public Builder forkSession(boolean value) {
520580
this.forkSession = value;
521581
return this;

acp-core/src/main/java/com/agentclientprotocol/sdk/spec/AcpSchema.java

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,14 @@ public record AuthenticateResponse() {
273273
@JsonInclude(JsonInclude.Include.NON_NULL)
274274
public record NewSessionRequest(@JsonProperty("cwd") String cwd,
275275
@JsonProperty("mcpServers") List<McpServer> mcpServers,
276+
@JsonProperty("additionalDirectories") List<String> additionalDirectories,
276277
@JsonProperty("_meta") Map<String, Object> meta) {
277278
public NewSessionRequest(String cwd, List<McpServer> mcpServers) {
278-
this(cwd, mcpServers, null);
279+
this(cwd, mcpServers, null, null);
280+
}
281+
282+
public NewSessionRequest(String cwd, List<McpServer> mcpServers, List<String> additionalDirectories) {
283+
this(cwd, mcpServers, additionalDirectories, null);
279284
}
280285
}
281286

@@ -299,9 +304,15 @@ public NewSessionResponse(String sessionId, SessionModeState modes, SessionModel
299304
@JsonInclude(JsonInclude.Include.NON_NULL)
300305
public record LoadSessionRequest(@JsonProperty("sessionId") String sessionId, @JsonProperty("cwd") String cwd,
301306
@JsonProperty("mcpServers") List<McpServer> mcpServers,
307+
@JsonProperty("additionalDirectories") List<String> additionalDirectories,
302308
@JsonProperty("_meta") Map<String, Object> meta) {
303309
public LoadSessionRequest(String sessionId, String cwd, List<McpServer> mcpServers) {
304-
this(sessionId, cwd, mcpServers, null);
310+
this(sessionId, cwd, mcpServers, null, null);
311+
}
312+
313+
public LoadSessionRequest(String sessionId, String cwd, List<McpServer> mcpServers,
314+
List<String> additionalDirectories) {
315+
this(sessionId, cwd, mcpServers, additionalDirectories, null);
305316
}
306317
}
307318

@@ -482,9 +493,15 @@ public CloseSessionResponse() {
482493
@JsonInclude(JsonInclude.Include.NON_NULL)
483494
public record ResumeSessionRequest(@JsonProperty("sessionId") String sessionId, @JsonProperty("cwd") String cwd,
484495
@JsonProperty("mcpServers") List<McpServer> mcpServers,
496+
@JsonProperty("additionalDirectories") List<String> additionalDirectories,
485497
@JsonProperty("_meta") Map<String, Object> meta) {
486498
public ResumeSessionRequest(String sessionId, String cwd, List<McpServer> mcpServers) {
487-
this(sessionId, cwd, mcpServers, null);
499+
this(sessionId, cwd, mcpServers, null, null);
500+
}
501+
502+
public ResumeSessionRequest(String sessionId, String cwd, List<McpServer> mcpServers,
503+
List<String> additionalDirectories) {
504+
this(sessionId, cwd, mcpServers, additionalDirectories, null);
488505
}
489506
}
490507

@@ -510,9 +527,15 @@ public ResumeSessionResponse(SessionModeState modes, SessionModelState models) {
510527
public record ForkSessionRequest(@JsonProperty("sessionId") String sessionId,
511528
@JsonProperty("cwd") String cwd,
512529
@JsonProperty("mcpServers") List<McpServer> mcpServers,
530+
@JsonProperty("additionalDirectories") List<String> additionalDirectories,
513531
@JsonProperty("_meta") Map<String, Object> meta) {
514532
public ForkSessionRequest(String sessionId, String cwd, List<McpServer> mcpServers) {
515-
this(sessionId, cwd, mcpServers, null);
533+
this(sessionId, cwd, mcpServers, null, null);
534+
}
535+
536+
public ForkSessionRequest(String sessionId, String cwd, List<McpServer> mcpServers,
537+
List<String> additionalDirectories) {
538+
this(sessionId, cwd, mcpServers, additionalDirectories, null);
516539
}
517540
}
518541

@@ -1034,10 +1057,15 @@ public AgentCapabilities(Boolean loadSession, McpCapabilities mcpCapabilities,
10341057
@JsonIgnoreProperties(ignoreUnknown = true)
10351058
@JsonInclude(JsonInclude.Include.NON_NULL)
10361059
public record SessionCapabilities(@JsonProperty("list") Object list, @JsonProperty("close") Object close,
1037-
@JsonProperty("resume") Object resume,
1060+
@JsonProperty("resume") Object resume, @JsonProperty("delete") Object delete,
1061+
@JsonProperty("additionalDirectories") Object additionalDirectories,
10381062
@UnstableAcpApi @JsonProperty("fork") Object fork) {
10391063
public SessionCapabilities(Object list, Object close, Object resume) {
1040-
this(list, close, resume, null);
1064+
this(list, close, resume, null, null, null);
1065+
}
1066+
1067+
public SessionCapabilities(Object list, Object close, Object resume, Object fork) {
1068+
this(list, close, resume, null, null, fork);
10411069
}
10421070
}
10431071

@@ -1075,9 +1103,10 @@ public PromptCapabilities() {
10751103
@JsonInclude(JsonInclude.Include.NON_NULL)
10761104
public record SessionInfo(@JsonProperty("sessionId") String sessionId, @JsonProperty("cwd") String cwd,
10771105
@JsonProperty("title") String title, @JsonProperty("updatedAt") String updatedAt,
1106+
@JsonProperty("additionalDirectories") List<String> additionalDirectories,
10781107
@JsonProperty("_meta") Map<String, Object> meta) {
10791108
public SessionInfo(String sessionId, String cwd) {
1080-
this(sessionId, cwd, null, null, null);
1109+
this(sessionId, cwd, null, null, null, null);
10811110
}
10821111
}
10831112

@@ -1334,10 +1363,14 @@ public interface SessionUpdate {
13341363
@JsonIgnoreProperties(ignoreUnknown = true)
13351364
@JsonInclude(JsonInclude.Include.NON_NULL)
13361365
public record UserMessageChunk(@JsonProperty("sessionUpdate") String sessionUpdate,
1337-
@JsonProperty("content") ContentBlock content,
1366+
@JsonProperty("content") ContentBlock content, @JsonProperty("messageId") String messageId,
13381367
@JsonProperty("_meta") Map<String, Object> meta) implements SessionUpdate {
13391368
public UserMessageChunk(String sessionUpdate, ContentBlock content) {
1340-
this(sessionUpdate, content, null);
1369+
this(sessionUpdate, content, null, null);
1370+
}
1371+
1372+
public UserMessageChunk(String sessionUpdate, ContentBlock content, String messageId) {
1373+
this(sessionUpdate, content, messageId, null);
13411374
}
13421375
}
13431376

@@ -1347,10 +1380,14 @@ public UserMessageChunk(String sessionUpdate, ContentBlock content) {
13471380
@JsonIgnoreProperties(ignoreUnknown = true)
13481381
@JsonInclude(JsonInclude.Include.NON_NULL)
13491382
public record AgentMessageChunk(@JsonProperty("sessionUpdate") String sessionUpdate,
1350-
@JsonProperty("content") ContentBlock content,
1383+
@JsonProperty("content") ContentBlock content, @JsonProperty("messageId") String messageId,
13511384
@JsonProperty("_meta") Map<String, Object> meta) implements SessionUpdate {
13521385
public AgentMessageChunk(String sessionUpdate, ContentBlock content) {
1353-
this(sessionUpdate, content, null);
1386+
this(sessionUpdate, content, null, null);
1387+
}
1388+
1389+
public AgentMessageChunk(String sessionUpdate, ContentBlock content, String messageId) {
1390+
this(sessionUpdate, content, messageId, null);
13541391
}
13551392
}
13561393

@@ -1360,10 +1397,14 @@ public AgentMessageChunk(String sessionUpdate, ContentBlock content) {
13601397
@JsonIgnoreProperties(ignoreUnknown = true)
13611398
@JsonInclude(JsonInclude.Include.NON_NULL)
13621399
public record AgentThoughtChunk(@JsonProperty("sessionUpdate") String sessionUpdate,
1363-
@JsonProperty("content") ContentBlock content,
1400+
@JsonProperty("content") ContentBlock content, @JsonProperty("messageId") String messageId,
13641401
@JsonProperty("_meta") Map<String, Object> meta) implements SessionUpdate {
13651402
public AgentThoughtChunk(String sessionUpdate, ContentBlock content) {
1366-
this(sessionUpdate, content, null);
1403+
this(sessionUpdate, content, null, null);
1404+
}
1405+
1406+
public AgentThoughtChunk(String sessionUpdate, ContentBlock content, String messageId) {
1407+
this(sessionUpdate, content, messageId, null);
13671408
}
13681409
}
13691410

acp-core/src/test/java/com/agentclientprotocol/sdk/capabilities/NegotiatedCapabilitiesTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,40 @@ void fromAgentWithPartialCapabilities() {
159159
assertThat(caps.supportsMcpSse()).isFalse();
160160
}
161161

162+
@Test
163+
void fromAgentExtractsDeleteAndAdditionalDirectoriesSessionCapabilities() {
164+
// list, close, resume, delete, additionalDirectories, fork
165+
AcpSchema.SessionCapabilities sessionCaps = new AcpSchema.SessionCapabilities(
166+
java.util.Collections.emptyMap(), null, null, java.util.Collections.emptyMap(),
167+
java.util.Collections.emptyMap(), null);
168+
AcpSchema.AgentCapabilities agentCaps = new AcpSchema.AgentCapabilities(true, sessionCaps,
169+
new AcpSchema.McpCapabilities(), new AcpSchema.PromptCapabilities(), null);
170+
171+
NegotiatedCapabilities caps = NegotiatedCapabilities.fromAgent(agentCaps);
172+
173+
assertThat(caps.supportsListSessions()).isTrue();
174+
assertThat(caps.supportsCloseSession()).isFalse();
175+
assertThat(caps.supportsDeleteSession()).isTrue();
176+
assertThat(caps.supportsAdditionalDirectories()).isTrue();
177+
assertThat(caps.supportsForkSession()).isFalse();
178+
}
179+
180+
@Test
181+
void requireDeleteSessionThrowsWhenNotSupported() {
182+
NegotiatedCapabilities caps = NegotiatedCapabilities.fromAgent(null);
183+
184+
assertThatThrownBy(caps::requireDeleteSession).isInstanceOf(AcpCapabilityException.class)
185+
.hasMessageContaining("sessionCapabilities.delete");
186+
}
187+
188+
@Test
189+
void requireAdditionalDirectoriesThrowsWhenNotSupported() {
190+
NegotiatedCapabilities caps = NegotiatedCapabilities.fromAgent(null);
191+
192+
assertThatThrownBy(caps::requireAdditionalDirectories).isInstanceOf(AcpCapabilityException.class)
193+
.hasMessageContaining("sessionCapabilities.additionalDirectories");
194+
}
195+
162196
@Test
163197
void requireLoadSessionThrowsWhenNotSupported() {
164198
NegotiatedCapabilities caps = NegotiatedCapabilities.fromAgent(null);

acp-core/src/test/java/com/agentclientprotocol/sdk/spec/AcpSchemaSerializationTest.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ void agentCapabilitiesWithMetaSerialization() throws IOException {
365365
void sessionUpdateWithMetaSerialization() throws IOException {
366366
Map<String, Object> meta = Map.of("custom/field", "value");
367367
AcpSchema.AgentMessageChunk update = new AcpSchema.AgentMessageChunk("agent_message_chunk",
368-
new AcpSchema.TextContent("Hello"), meta);
368+
new AcpSchema.TextContent("Hello"), null, meta);
369369

370370
String json = jsonMapper.writeValueAsString(update);
371371
assertThat(json).contains("\"_meta\"");
@@ -378,6 +378,58 @@ void sessionUpdateWithMetaSerialization() throws IOException {
378378
assertThat(deserialized.meta().get("custom/field")).isEqualTo("value");
379379
}
380380

381+
@Test
382+
void messageChunkWithMessageIdSerialization() throws IOException {
383+
AcpSchema.AgentMessageChunk update = new AcpSchema.AgentMessageChunk("agent_message_chunk",
384+
new AcpSchema.TextContent("Hello"), "msg-42");
385+
386+
String json = jsonMapper.writeValueAsString(update);
387+
assertThat(json).contains("\"messageId\":\"msg-42\"");
388+
389+
AcpSchema.AgentMessageChunk deserialized = jsonMapper.readValue(json,
390+
new TypeRef<AcpSchema.AgentMessageChunk>() {
391+
});
392+
393+
assertThat(deserialized.messageId()).isEqualTo("msg-42");
394+
}
395+
396+
@Test
397+
void messageChunkWithoutMessageIdOmitsField() throws IOException {
398+
AcpSchema.AgentMessageChunk update = new AcpSchema.AgentMessageChunk("agent_message_chunk",
399+
new AcpSchema.TextContent("Hello"));
400+
401+
String json = jsonMapper.writeValueAsString(update);
402+
assertThat(json).doesNotContain("messageId");
403+
}
404+
405+
@Test
406+
void newSessionRequestWithAdditionalDirectoriesSerialization() throws IOException {
407+
AcpSchema.NewSessionRequest request = new AcpSchema.NewSessionRequest("/workspace", Collections.emptyList(),
408+
List.of("/extra/one", "/extra/two"));
409+
410+
String json = jsonMapper.writeValueAsString(request);
411+
assertThat(json).contains("\"additionalDirectories\"");
412+
413+
AcpSchema.NewSessionRequest deserialized = jsonMapper.readValue(json,
414+
new TypeRef<AcpSchema.NewSessionRequest>() {
415+
});
416+
417+
assertThat(deserialized.additionalDirectories()).containsExactly("/extra/one", "/extra/two");
418+
}
419+
420+
@Test
421+
void sessionInfoWithAdditionalDirectoriesSerialization() throws IOException {
422+
AcpSchema.SessionInfo info = new AcpSchema.SessionInfo("session-1", "/workspace", null, null,
423+
List.of("/extra"), null);
424+
425+
String json = jsonMapper.writeValueAsString(info);
426+
427+
AcpSchema.SessionInfo deserialized = jsonMapper.readValue(json, new TypeRef<AcpSchema.SessionInfo>() {
428+
});
429+
430+
assertThat(deserialized.additionalDirectories()).containsExactly("/extra");
431+
}
432+
381433
// ---------------------------
382434
// Elicitation Serialization
383435
// ---------------------------

0 commit comments

Comments
 (0)