Skip to content

Commit 0d8ca76

Browse files
author
Mark Pollack
committed
Add convenience API for PromptContext with reduced boilerplate
1 parent b994d60 commit 0d8ca76

File tree

19 files changed

+1729
-16
lines changed

19 files changed

+1729
-16
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.agent;
6+
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
/**
12+
* Builder for terminal command execution via the convenience API.
13+
*
14+
* <p>
15+
* This record allows configuring command execution with options like
16+
* working directory, environment variables, and output limits.
17+
*
18+
* <p>
19+
* Example usage:
20+
* <pre>{@code
21+
* // Simple command
22+
* CommandResult result = context.execute("echo", "hello");
23+
*
24+
* // Command with options
25+
* CommandResult result = context.execute(
26+
* Command.of("make", "build")
27+
* .cwd("/workspace")
28+
* .env(Map.of("DEBUG", "true"))
29+
* .outputLimit(10000));
30+
* }</pre>
31+
*
32+
* @param executable The command to execute
33+
* @param args The arguments to pass to the command
34+
* @param cwd The working directory (null for default)
35+
* @param env Environment variables to set (null for default)
36+
* @param outputByteLimit Maximum bytes of output to capture (null for default)
37+
* @author Mark Pollack
38+
* @since 0.9.2
39+
* @see SyncPromptContext#execute(Command)
40+
* @see PromptContext#execute(Command)
41+
*/
42+
public record Command(
43+
String executable,
44+
List<String> args,
45+
String cwd,
46+
Map<String, String> env,
47+
Long outputByteLimit
48+
) {
49+
50+
/**
51+
* Creates a Command from command-line arguments.
52+
* The first argument is the executable, remaining arguments are passed as args.
53+
* @param commandAndArgs The command and its arguments
54+
* @return A new Command instance
55+
*/
56+
public static Command of(String... commandAndArgs) {
57+
if (commandAndArgs == null || commandAndArgs.length == 0) {
58+
throw new IllegalArgumentException("At least one argument (the command) is required");
59+
}
60+
return new Command(
61+
commandAndArgs[0],
62+
commandAndArgs.length > 1
63+
? Arrays.asList(commandAndArgs).subList(1, commandAndArgs.length)
64+
: List.of(),
65+
null, null, null);
66+
}
67+
68+
/**
69+
* Returns a new Command with the specified working directory.
70+
* @param cwd The working directory
71+
* @return A new Command with the working directory set
72+
*/
73+
public Command cwd(String cwd) {
74+
return new Command(executable, args, cwd, env, outputByteLimit);
75+
}
76+
77+
/**
78+
* Returns a new Command with the specified environment variables.
79+
* @param env The environment variables
80+
* @return A new Command with the environment variables set
81+
*/
82+
public Command env(Map<String, String> env) {
83+
return new Command(executable, args, cwd, env, outputByteLimit);
84+
}
85+
86+
/**
87+
* Returns a new Command with the specified output byte limit.
88+
* @param limit The maximum bytes of output to capture
89+
* @return A new Command with the output byte limit set
90+
*/
91+
public Command outputByteLimit(long limit) {
92+
return new Command(executable, args, cwd, env, limit);
93+
}
94+
95+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.agent;
6+
7+
/**
8+
* Result of executing a terminal command via the convenience API.
9+
*
10+
* <p>
11+
* This record wraps the output and exit code from a terminal command execution,
12+
* providing a clean interface for the common case of running a command and
13+
* checking its result.
14+
*
15+
* <p>
16+
* Example usage:
17+
* <pre>{@code
18+
* CommandResult result = context.execute("make", "build");
19+
* if (result.exitCode() == 0) {
20+
* context.sendMessage("Build succeeded!");
21+
* } else {
22+
* context.sendMessage("Build failed: " + result.output());
23+
* }
24+
* }</pre>
25+
*
26+
* @param output The combined stdout/stderr output from the command
27+
* @param exitCode The exit code (0 typically means success)
28+
* @param timedOut Whether the command was terminated due to timeout
29+
* @author Mark Pollack
30+
* @since 0.9.2
31+
* @see SyncPromptContext#execute(String...)
32+
* @see PromptContext#execute(String...)
33+
*/
34+
public record CommandResult(
35+
String output,
36+
int exitCode,
37+
boolean timedOut
38+
) {
39+
40+
/**
41+
* Creates a CommandResult with the given output and exit code.
42+
* Sets timedOut to false.
43+
* @param output The command output
44+
* @param exitCode The exit code
45+
*/
46+
public CommandResult(String output, int exitCode) {
47+
this(output, exitCode, false);
48+
}
49+
50+
/**
51+
* Returns true if the command completed successfully (exit code 0).
52+
* @return true if exit code is 0 and command did not time out
53+
*/
54+
public boolean success() {
55+
return exitCode == 0 && !timedOut;
56+
}
57+
58+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public Mono<Void> start() {
137137
new TypeRef<AcpSchema.PromptRequest>() {
138138
});
139139
// Create PromptContext that wraps this agent, giving handler access to all capabilities
140-
PromptContext context = new DefaultPromptContext(this);
140+
PromptContext context = new DefaultPromptContext(this, request.sessionId());
141141
return promptHandler.handle(request, context)
142142
.cast(Object.class);
143143
});

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

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,29 @@
44

55
package com.agentclientprotocol.sdk.agent;
66

7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.UUID;
10+
711
import com.agentclientprotocol.sdk.capabilities.NegotiatedCapabilities;
812
import com.agentclientprotocol.sdk.spec.AcpSchema;
13+
import com.agentclientprotocol.sdk.spec.AcpSchema.AgentMessageChunk;
14+
import com.agentclientprotocol.sdk.spec.AcpSchema.AgentThoughtChunk;
15+
import com.agentclientprotocol.sdk.spec.AcpSchema.CreateTerminalRequest;
16+
import com.agentclientprotocol.sdk.spec.AcpSchema.EnvVariable;
17+
import com.agentclientprotocol.sdk.spec.AcpSchema.PermissionOption;
18+
import com.agentclientprotocol.sdk.spec.AcpSchema.PermissionOptionKind;
19+
import com.agentclientprotocol.sdk.spec.AcpSchema.PermissionSelected;
20+
import com.agentclientprotocol.sdk.spec.AcpSchema.ReadTextFileRequest;
21+
import com.agentclientprotocol.sdk.spec.AcpSchema.ReleaseTerminalRequest;
22+
import com.agentclientprotocol.sdk.spec.AcpSchema.RequestPermissionRequest;
23+
import com.agentclientprotocol.sdk.spec.AcpSchema.TerminalOutputRequest;
24+
import com.agentclientprotocol.sdk.spec.AcpSchema.TextContent;
25+
import com.agentclientprotocol.sdk.spec.AcpSchema.ToolCallStatus;
26+
import com.agentclientprotocol.sdk.spec.AcpSchema.ToolCallUpdate;
27+
import com.agentclientprotocol.sdk.spec.AcpSchema.ToolKind;
28+
import com.agentclientprotocol.sdk.spec.AcpSchema.WaitForTerminalExitRequest;
29+
import com.agentclientprotocol.sdk.spec.AcpSchema.WriteTextFileRequest;
930
import reactor.core.publisher.Mono;
1031

1132
/**
@@ -23,14 +44,22 @@ class DefaultPromptContext implements PromptContext {
2344

2445
private final AcpAsyncAgent agent;
2546

47+
private final String sessionId;
48+
2649
/**
2750
* Creates a new prompt context wrapping the given agent.
2851
* @param agent The agent to delegate to
52+
* @param sessionId The session ID for this prompt invocation
2953
*/
30-
DefaultPromptContext(AcpAsyncAgent agent) {
54+
DefaultPromptContext(AcpAsyncAgent agent, String sessionId) {
3155
this.agent = agent;
56+
this.sessionId = sessionId;
3257
}
3358

59+
// ========================================================================
60+
// Low-Level API
61+
// ========================================================================
62+
3463
@Override
3564
public Mono<Void> sendUpdate(String sessionId, AcpSchema.SessionUpdate update) {
3665
return agent.sendSessionUpdate(sessionId, update);
@@ -82,4 +111,109 @@ public NegotiatedCapabilities getClientCapabilities() {
82111
return agent.getClientCapabilities();
83112
}
84113

114+
// ========================================================================
115+
// Convenience API
116+
// ========================================================================
117+
118+
@Override
119+
public String getSessionId() {
120+
return sessionId;
121+
}
122+
123+
@Override
124+
public Mono<Void> sendMessage(String text) {
125+
return sendUpdate(sessionId, new AgentMessageChunk("agent_message_chunk", new TextContent(text)));
126+
}
127+
128+
@Override
129+
public Mono<Void> sendThought(String text) {
130+
return sendUpdate(sessionId, new AgentThoughtChunk("agent_thought_chunk", new TextContent(text)));
131+
}
132+
133+
@Override
134+
public Mono<String> readFile(String path) {
135+
return readFile(path, null, null);
136+
}
137+
138+
@Override
139+
public Mono<String> readFile(String path, Integer startLine, Integer lineCount) {
140+
return readTextFile(new ReadTextFileRequest(sessionId, path, startLine, lineCount))
141+
.map(AcpSchema.ReadTextFileResponse::content);
142+
}
143+
144+
@Override
145+
public Mono<Void> writeFile(String path, String content) {
146+
return writeTextFile(new WriteTextFileRequest(sessionId, path, content)).then();
147+
}
148+
149+
@Override
150+
public Mono<Boolean> askPermission(String action) {
151+
ToolCallUpdate toolCall = new ToolCallUpdate(
152+
UUID.randomUUID().toString(), action, ToolKind.EDIT, ToolCallStatus.PENDING,
153+
null, null, null, null);
154+
155+
List<PermissionOption> options = List.of(
156+
new PermissionOption("allow", "Allow", PermissionOptionKind.ALLOW_ONCE),
157+
new PermissionOption("deny", "Deny", PermissionOptionKind.REJECT_ONCE));
158+
159+
return requestPermission(new RequestPermissionRequest(sessionId, toolCall, options))
160+
.map(response -> response.outcome() instanceof PermissionSelected s
161+
&& "allow".equals(s.optionId()));
162+
}
163+
164+
@Override
165+
public Mono<String> askChoice(String question, String... options) {
166+
if (options == null || options.length < 2) {
167+
return Mono.error(new IllegalArgumentException("At least 2 options are required"));
168+
}
169+
170+
List<PermissionOption> permOptions = new ArrayList<>();
171+
for (int i = 0; i < options.length; i++) {
172+
permOptions.add(new PermissionOption(
173+
String.valueOf(i), options[i], PermissionOptionKind.ALLOW_ONCE));
174+
}
175+
176+
ToolCallUpdate toolCall = new ToolCallUpdate(
177+
UUID.randomUUID().toString(), question, ToolKind.OTHER,
178+
ToolCallStatus.PENDING, null, null, null, null);
179+
180+
return requestPermission(new RequestPermissionRequest(sessionId, toolCall, permOptions))
181+
.map(response -> {
182+
if (response.outcome() instanceof PermissionSelected s) {
183+
int idx = Integer.parseInt(s.optionId());
184+
return options[idx];
185+
}
186+
return null;
187+
});
188+
}
189+
190+
@Override
191+
public Mono<CommandResult> execute(String... commandAndArgs) {
192+
return execute(Command.of(commandAndArgs));
193+
}
194+
195+
@Override
196+
public Mono<CommandResult> execute(Command command) {
197+
// Convert env map to list of EnvVariable
198+
List<EnvVariable> envList = null;
199+
if (command.env() != null) {
200+
envList = command.env().entrySet().stream()
201+
.map(e -> new EnvVariable(e.getKey(), e.getValue()))
202+
.toList();
203+
}
204+
205+
return createTerminal(new CreateTerminalRequest(
206+
sessionId, command.executable(), command.args(),
207+
command.cwd(), envList, command.outputByteLimit()))
208+
.flatMap(createResp -> {
209+
String terminalId = createResp.terminalId();
210+
211+
return waitForTerminalExit(new WaitForTerminalExitRequest(sessionId, terminalId))
212+
.flatMap(exitResp -> getTerminalOutput(new TerminalOutputRequest(sessionId, terminalId))
213+
.map(outputResp -> new CommandResult(outputResp.output(), exitResp.exitCode(), false)))
214+
.doFinally(signal -> releaseTerminal(new ReleaseTerminalRequest(sessionId, terminalId))
215+
.subscribe());
216+
});
217+
}
218+
85219
}

0 commit comments

Comments
 (0)