diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCli.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCli.java index 065ff9e73..0471ab692 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCli.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCli.java @@ -31,12 +31,12 @@ public class QwenCli { try { session.sendPrompt(prompt, new SessionEventSimpleConsumers() { @Override - public void onSystemMessage(SDKSystemMessage systemMessage) { + public void onSystemMessage(Session session, SDKSystemMessage systemMessage) { response.add(systemMessage); } @Override - public void onAssistantMessage(SDKAssistantMessage assistantMessage) { + public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) { response.add(assistantMessage); } }); diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/PermissionMode.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/PermissionMode.java similarity index 92% rename from packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/PermissionMode.java rename to packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/PermissionMode.java index 3db5782c6..d960a396e 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/PermissionMode.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/PermissionMode.java @@ -1,4 +1,4 @@ -package com.alibaba.qwen.code.cli.transport; +package com.alibaba.qwen.code.cli.protocol.data; public enum PermissionMode { DEFAULT("default"), diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Allow.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Allow.java new file mode 100644 index 000000000..14adf7a2f --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Allow.java @@ -0,0 +1,23 @@ +package com.alibaba.qwen.code.cli.protocol.data.behavior; + +import java.util.Map; + +import com.alibaba.fastjson2.annotation.JSONType; + +@JSONType(typeKey = "operation", typeName = "allow") +public class Allow extends Behavior { + public Allow() { + super(); + this.behavior = Operation.allow; + } + Map updatedInput; + + public Map getUpdatedInput() { + return updatedInput; + } + + public Allow setUpdatedInput(Map updatedInput) { + this.updatedInput = updatedInput; + return this; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Behavior.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Behavior.java new file mode 100644 index 000000000..1f54f2341 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Behavior.java @@ -0,0 +1,25 @@ +package com.alibaba.qwen.code.cli.protocol.data.behavior; + +import com.alibaba.fastjson2.annotation.JSONType; + +@JSONType(typeKey = "operation", typeName = "Behavior", seeAlso = {Allow.class, Deny.class}) +public class Behavior { + Operation behavior; + + public Operation getBehavior() { + return behavior; + } + + public void setBehavior(Operation behavior) { + this.behavior = behavior; + } + + public enum Operation { + allow, + deny + } + + public static Behavior defaultBehavior() { + return new Deny().setMessage("Default Behavior Permission denied"); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Deny.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Deny.java new file mode 100644 index 000000000..17d37ca05 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Deny.java @@ -0,0 +1,22 @@ +package com.alibaba.qwen.code.cli.protocol.data.behavior; + +import com.alibaba.fastjson2.annotation.JSONType; + +@JSONType(typeKey = "operation", typeName = "deny") +public class Deny extends Behavior { + public Deny() { + super(); + this.behavior = Operation.deny; + } + + String message; + + public String getMessage() { + return message; + } + + public Deny setMessage(String message) { + this.message = message; + return this; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlInterruptRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlInterruptRequest.java new file mode 100644 index 000000000..f4a052697 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlInterruptRequest.java @@ -0,0 +1,13 @@ +package com.alibaba.qwen.code.cli.protocol.message.control; + +public class CLIControlInterruptRequest { + String subtype = "interrupt"; + + public String getSubtype() { + return subtype; + } + + public void setSubtype(String subtype) { + this.subtype = subtype; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlPermissionRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlPermissionRequest.java new file mode 100644 index 000000000..ac3e43e79 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlPermissionRequest.java @@ -0,0 +1,112 @@ +package com.alibaba.qwen.code.cli.protocol.message.control; + +import java.util.List; +import java.util.Map; + +import com.alibaba.fastjson2.annotation.JSONField; + +public class CLIControlPermissionRequest { + private String subtype; + + @JSONField(name = "tool_name") + private String toolName; + + @JSONField(name = "tool_use_id") + private String toolUseId; + + private Map input; + + @JSONField(name = "permission_suggestions") + private List permissionSuggestions; + + @JSONField(name = "blocked_path") + private String blockedPath; + + public String getSubtype() { + return subtype; + } + + public void setSubtype(String subtype) { + this.subtype = subtype; + } + + public String getToolName() { + return toolName; + } + + public void setToolName(String toolName) { + this.toolName = toolName; + } + + public String getToolUseId() { + return toolUseId; + } + + public void setToolUseId(String toolUseId) { + this.toolUseId = toolUseId; + } + + public Map getInput() { + return input; + } + + public void setInput(Map input) { + this.input = input; + } + + public List getPermissionSuggestions() { + return permissionSuggestions; + } + + public void setPermissionSuggestions( + List permissionSuggestions) { + this.permissionSuggestions = permissionSuggestions; + } + + public String getBlockedPath() { + return blockedPath; + } + + public void setBlockedPath(String blockedPath) { + this.blockedPath = blockedPath; + } + + public static class PermissionSuggestion { + private String type; // 'allow' | 'deny' | 'modify' + private String label; + private String description; + private Object modifiedInput; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Object getModifiedInput() { + return modifiedInput; + } + + public void setModifiedInput(Object modifiedInput) { + this.modifiedInput = modifiedInput; + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlPermissionResponse.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlPermissionResponse.java new file mode 100644 index 000000000..66c199632 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlPermissionResponse.java @@ -0,0 +1,28 @@ +package com.alibaba.qwen.code.cli.protocol.message.control; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; + +public class CLIControlPermissionResponse { + private String subtype = "can_use_tool"; + + @JSONField(unwrapped = true) + Behavior behavior; + + public String getSubtype() { + return subtype; + } + + public void setSubtype(String subtype) { + this.subtype = subtype; + } + + public Behavior getBehavior() { + return behavior; + } + + public CLIControlPermissionResponse setBehavior(Behavior behavior) { + this.behavior = behavior; + return this; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlResponse.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlResponse.java index 5e193e2cd..bce0c03cc 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlResponse.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlResponse.java @@ -21,34 +21,43 @@ public class CLIControlResponse extends MessageBase { this.response = response; } + public Response createResponse() { + Response response = new Response<>(); + this.setResponse(response); + return response; + } + public static class Response { @JSONField(name = "request_id") private String requestId; - private String subtype; + private String subtype = "success"; R response; public String getRequestId() { return requestId; } - public void setRequestId(String requestId) { + public Response setRequestId(String requestId) { this.requestId = requestId; + return this; } public String getSubtype() { return subtype; } - public void setSubtype(String subtype) { + public Response setSubtype(String subtype) { this.subtype = subtype; + return this; } public R getResponse() { return response; } - public void setResponse(R response) { + public Response setResponse(R response) { this.response = response; + return this; } } } diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlSetModelRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlSetModelRequest.java new file mode 100644 index 000000000..d93a6fb6d --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlSetModelRequest.java @@ -0,0 +1,22 @@ +package com.alibaba.qwen.code.cli.protocol.message.control; + +public class CLIControlSetModelRequest { + String subtype = "set_model"; + String model; + + public String getSubtype() { + return subtype; + } + + public void setSubtype(String subtype) { + this.subtype = subtype; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlSetPermissionModeRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlSetPermissionModeRequest.java new file mode 100644 index 000000000..ea1ad9698 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlSetPermissionModeRequest.java @@ -0,0 +1,23 @@ +package com.alibaba.qwen.code.cli.protocol.message.control; + +public class CLIControlSetPermissionModeRequest { + String subtype = "set_permission_mode"; + + String mode; + + public String getSubtype() { + return subtype; + } + + public void setSubtype(String subtype) { + this.subtype = subtype; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/Session.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/Session.java index 346b0f1f2..79a210742 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/Session.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/Session.java @@ -1,59 +1,141 @@ package com.alibaba.qwen.code.cli.session; +import java.io.IOException; +import java.util.Optional; + import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.JSONReader.Feature; import com.alibaba.fastjson2.TypeReference; import com.alibaba.qwen.code.cli.protocol.data.Capabilities; +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Allow; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlInitializeRequest; import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlInitializeResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlInterruptRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlPermissionRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlPermissionResponse; import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlSetModelRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlSetPermissionModeRequest; import com.alibaba.qwen.code.cli.session.event.SessionEventConsumers; -import com.alibaba.qwen.code.cli.session.exception.SessionCloseException; +import com.alibaba.qwen.code.cli.session.exception.SessionControlException; import com.alibaba.qwen.code.cli.session.exception.SessionSendPromptException; -import com.alibaba.qwen.code.cli.session.exception.SessionStartException; import com.alibaba.qwen.code.cli.transport.Transport; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Session { private final Transport transport; - private Capabilities capabilities; + private CLIControlInitializeResponse lastCliControlInitializeResponse; + private SDKSystemMessage lastSdkSystemMessage; private static final Logger log = LoggerFactory.getLogger(Session.class); - public Session(Transport transport) throws SessionStartException { + public Session(Transport transport) throws SessionControlException { if (transport == null || !transport.isAvailable()) { - throw new SessionStartException("Transport is not available"); + throw new SessionControlException("Transport is not available"); } this.transport = transport; start(); } - private void start() throws SessionStartException { + public void start() throws SessionControlException { try { + if (!transport.isAvailable()) { + transport.start(); + } String response = transport.inputWaitForOneLine(CLIControlRequest.create(new CLIControlInitializeRequest()).toString()); - CLIControlResponse cliControlResponse = JSON.parseObject(response, new TypeReference>() {}); - this.capabilities = cliControlResponse.getResponse().getResponse().getCapabilities(); + CLIControlResponse cliControlResponse = JSON.parseObject(response, + new TypeReference>() {}); + this.lastCliControlInitializeResponse = cliControlResponse.getResponse().getResponse(); } catch (Exception e) { - throw new SessionStartException("Failed to initialize the session", e); + throw new SessionControlException("Failed to initialize the session", e); } } - public void close() throws SessionCloseException { + public void interrupt() throws SessionControlException { + if (!isAvailable()) { + throw new SessionControlException("Session is not available"); + } + + try { + transport.inputNoWaitResponse( + new CLIControlRequest().setRequest(new CLIControlInterruptRequest()).toString()); + } catch (Exception e) { + throw new SessionControlException("Failed to interrupt the session", e); + } + } + + public void setModel(String modelName) throws SessionControlException { + if (!isAvailable()) { + throw new SessionControlException("Session is not available"); + } + + CLIControlSetModelRequest cliControlSetModelRequest = new CLIControlSetModelRequest(); + cliControlSetModelRequest.setModel(modelName); + try { + transport.inputNoWaitResponse(new CLIControlRequest().setRequest(cliControlSetModelRequest).toString()); + } catch (Exception e) { + throw new SessionControlException("Failed to set model", e); + } + } + + public void setPermissionMode(PermissionMode permissionMode) throws SessionControlException { + if (!isAvailable()) { + throw new SessionControlException("Session is not available"); + } + + CLIControlSetPermissionModeRequest cliControlSetPermissionModeRequest = new CLIControlSetPermissionModeRequest(); + cliControlSetPermissionModeRequest.setMode(permissionMode.getValue()); + try { + transport.inputNoWaitResponse( + new CLIControlRequest().setRequest(cliControlSetPermissionModeRequest).toString()); + } catch (Exception e) { + throw new SessionControlException("Failed to set model", e); + } + } + + public void continueSession() throws SessionControlException { + resumeSession(getSessionId()); + } + + public void resumeSession(String sessionId) throws SessionControlException { + if (!isAvailable()) { + throw new SessionControlException("Session is not available"); + } + + if (StringUtils.isNotBlank(sessionId)) { + transport.getTransportOptions().setResumeSessionId(sessionId); + } + this.start(); + } + + public String getSessionId() { + return Optional.ofNullable(lastSdkSystemMessage).map(SDKSystemMessage::getSessionId).orElse(null); + } + + public void close() throws SessionControlException { try { transport.close(); } catch (Exception e) { - throw new SessionCloseException("Failed to close the session", e); + throw new SessionControlException("Failed to close the session", e); } } + public boolean isAvailable() { + return transport.isAvailable(); + } + public Capabilities getCapabilities() { - return capabilities; + return Optional.ofNullable(lastCliControlInitializeResponse).map(CLIControlInitializeResponse::getCapabilities).orElse(new Capabilities()); } public void sendPrompt(String prompt, SessionEventConsumers sessionEventConsumers) throws SessionSendPromptException { @@ -65,20 +147,33 @@ public class Session { transport.inputWaitForMultiLine(new SDKUserMessage().setContent(prompt).toString(), (line) -> { log.debug("read a message from agent {}", line); JSONObject jsonObject = JSON.parseObject(line); - String messageType = jsonObject.getString("type"); if ("system".equals(messageType)) { - sessionEventConsumers.onSystemMessage(JSON.parseObject(line, SDKSystemMessage.class)); + lastSdkSystemMessage = jsonObject.to(SDKSystemMessage.class); + sessionEventConsumers.onSystemMessage(this, lastSdkSystemMessage); return false; } else if ("assistant".equals(messageType)) { - sessionEventConsumers.onAssistantMessage(JSON.parseObject(line, SDKAssistantMessage.class)); + sessionEventConsumers.onAssistantMessage(this, jsonObject.to(SDKAssistantMessage.class)); + return false; + } else if ("user".equals(messageType)) { + sessionEventConsumers.onUserMessage(this, jsonObject.to(SDKUserMessage.class, Feature.FieldBased)); return false; } else if ("result".equals(messageType)) { - sessionEventConsumers.onResultMessage(JSON.parseObject(line, SDKResultMessage.class)); + sessionEventConsumers.onResultMessage(this, jsonObject.to(SDKResultMessage.class)); return true; + } else if ("control_response".equals(messageType)) { + sessionEventConsumers.onControlResponse(this, jsonObject.to(CLIControlResponse.class)); + if (!"error".equals(jsonObject.getString("subtype"))) { + return false; + } else { + log.info("control_response error: {}", jsonObject.toJSONString()); + return "error".equals(jsonObject.getString("subtype")); + } + } else if ("control_request".equals(messageType)) { + return processControlRequest(jsonObject, sessionEventConsumers); } else { log.warn("unknown message type: {}", messageType); - sessionEventConsumers.onOtherMessage(line); + sessionEventConsumers.onOtherMessage(this, line); return false; } }); @@ -86,4 +181,53 @@ public class Session { throw new SessionSendPromptException("Failed to send prompt", e); } } + + private boolean processControlRequest(JSONObject jsonObject, SessionEventConsumers sessionEventConsumers) { + String subType = Optional.of(jsonObject) + .map(cr -> cr.getJSONObject("request")) + .map(r -> r.getString("subtype")) + .orElse(""); + if ("can_use_tool".equals(subType)) { + try { + return processPermissionResponse(jsonObject, sessionEventConsumers); + } catch (IOException e) { + log.error("Failed to process permission response", e); + return false; + } + } else { + CLIControlResponse cliControlResponse = sessionEventConsumers.onControlRequest(this, + jsonObject.to(new TypeReference>() {})); + if (cliControlResponse != null) { + try { + transport.inputNoWaitResponse(cliControlResponse.toString()); + } catch (Exception e) { + log.error("Failed to process control response", e); + return false; + } + } + return false; + } + } + + private boolean processPermissionResponse(JSONObject jsonObject, SessionEventConsumers sessionEventConsumers) throws IOException { + CLIControlRequest permissionRequest = jsonObject.to(new TypeReference>() {}); + Behavior behavior = Optional.ofNullable(sessionEventConsumers.onPermissionRequest(this, permissionRequest)) + .map(b -> { + if (b instanceof Allow) { + Allow allow = (Allow) b; + if (allow.getUpdatedInput() == null) { + allow.setUpdatedInput(permissionRequest.getRequest().getInput()); + } + } + return b; + }) + .orElse(Behavior.defaultBehavior()); + CLIControlResponse permissionResponse = new CLIControlResponse<>(); + permissionResponse.createResponse().setResponse(new CLIControlPermissionResponse().setBehavior(behavior)).setRequestId(permissionRequest.getRequestId()); + String permissionMessage = permissionResponse.toString(); + log.debug("send permission message to agent: {}", permissionMessage); + transport.inputNoWaitResponse(permissionMessage); + + return false; + } } diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventConsumers.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventConsumers.java index 9f9bb6fc1..e2100b5cc 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventConsumers.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventConsumers.java @@ -1,15 +1,29 @@ package com.alibaba.qwen.code.cli.session.event; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlPermissionRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.session.Session; public interface SessionEventConsumers { - void onSystemMessage(SDKSystemMessage systemMessage); + void onSystemMessage(Session session, SDKSystemMessage systemMessage); - void onResultMessage(SDKResultMessage resultMessage); + void onResultMessage(Session session, SDKResultMessage resultMessage); - void onAssistantMessage(SDKAssistantMessage assistantMessage); + void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage); - void onOtherMessage(String message); + void onUserMessage(Session session, SDKUserMessage userMessage); + + void onOtherMessage(Session session, String message); + + void onControlResponse(Session session, CLIControlResponse cliControlResponse); + + CLIControlResponse onControlRequest(Session session, CLIControlRequest cliControlRequest); + + Behavior onPermissionRequest(Session session, CLIControlRequest permissionRequest); } diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventSimpleConsumers.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventSimpleConsumers.java index 584354d43..9c685e755 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventSimpleConsumers.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/SessionEventSimpleConsumers.java @@ -1,23 +1,47 @@ package com.alibaba.qwen.code.cli.session.event; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlPermissionRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.session.Session; public class SessionEventSimpleConsumers implements SessionEventConsumers { @Override - public void onSystemMessage(SDKSystemMessage systemMessage) { + public void onSystemMessage(Session session, SDKSystemMessage systemMessage) { } @Override - public void onResultMessage(SDKResultMessage resultMessage) { + public void onResultMessage(Session session, SDKResultMessage resultMessage) { } @Override - public void onAssistantMessage(SDKAssistantMessage assistantMessage) { + public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) { } @Override - public void onOtherMessage(String message) { + public void onUserMessage(Session session, SDKUserMessage userMessage) { + } + + @Override + public void onOtherMessage(Session session, String message) { + } + + @Override + public void onControlResponse(Session session, CLIControlResponse cliControlResponse) { + } + + @Override + public CLIControlResponse onControlRequest(Session session, CLIControlRequest cliControlRequest) { + return new CLIControlResponse<>(); + } + + @Override + public Behavior onPermissionRequest(Session session, CLIControlRequest permissionRequest) { + return Behavior.defaultBehavior(); } } diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionCloseException.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionCloseException.java deleted file mode 100644 index 3db39e793..000000000 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionCloseException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.alibaba.qwen.code.cli.session.exception; - -public class SessionCloseException extends Exception { - public SessionCloseException() { - } - - public SessionCloseException(String message) { - super(message); - } - - public SessionCloseException(String message, Throwable cause) { - super(message, cause); - } - - public SessionCloseException(Throwable cause) { - super(cause); - } - - public SessionCloseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionControlException.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionControlException.java new file mode 100644 index 000000000..770d5982c --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionControlException.java @@ -0,0 +1,22 @@ +package com.alibaba.qwen.code.cli.session.exception; + +public class SessionControlException extends Exception { + public SessionControlException() { + } + + public SessionControlException(String message) { + super(message); + } + + public SessionControlException(String message, Throwable cause) { + super(message, cause); + } + + public SessionControlException(Throwable cause) { + super(cause); + } + + public SessionControlException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionStartException.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionStartException.java deleted file mode 100644 index 9d30f2367..000000000 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionStartException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.alibaba.qwen.code.cli.session.exception; - -public class SessionStartException extends Exception { - public SessionStartException() { - } - - public SessionStartException(String message) { - super(message); - } - - public SessionStartException(String message, Throwable cause) { - super(message, cause); - } - - public SessionStartException(Throwable cause) { - super(cause); - } - - public SessionStartException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/Transport.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/Transport.java index 5b66cbc90..b3d69ee28 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/Transport.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/Transport.java @@ -6,6 +6,10 @@ import java.util.concurrent.TimeoutException; import java.util.function.Function; public interface Transport { + TransportOptions getTransportOptions(); + + void start() throws IOException; + void close() throws IOException; boolean isAvailable(); diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/TransportOptions.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/TransportOptions.java index b64df2ec6..b5e6ada6f 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/TransportOptions.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/TransportOptions.java @@ -3,6 +3,8 @@ package com.alibaba.qwen.code.cli.transport; import java.util.List; import java.util.Map; +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; + public class TransportOptions implements Cloneable { private String pathToQwenExecutable; private String cwd; @@ -17,6 +19,7 @@ public class TransportOptions implements Cloneable { private Boolean includePartialMessages; private Long turnTimeoutMs; private Long messageTimeoutMs; + private String resumeSessionId; public String getPathToQwenExecutable() { return pathToQwenExecutable; @@ -122,6 +125,14 @@ public class TransportOptions implements Cloneable { this.messageTimeoutMs = messageTimeoutMs; } + public String getResumeSessionId() { + return resumeSessionId; + } + + public void setResumeSessionId(String resumeSessionId) { + this.resumeSessionId = resumeSessionId; + } + @Override public TransportOptions clone() { try { diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransport.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransport.java index b7d62b5d7..14a0eb0ff 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransport.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransport.java @@ -22,10 +22,9 @@ import java.util.function.Function; public class ProcessTransport implements Transport { private static final Logger log = LoggerFactory.getLogger(ProcessTransport.class); - TransportOptionsAdapter transportOptionsAdapter; - - protected final Long turnTimeoutMs; - protected final Long messageTimeoutMs; + private final TransportOptions transportOptions; + protected Long turnTimeoutMs; + protected Long messageTimeoutMs; protected Process process; protected BufferedWriter processInput; @@ -37,13 +36,21 @@ public class ProcessTransport implements Transport { } public ProcessTransport(TransportOptions transportOptions) throws IOException { - this.transportOptionsAdapter = new TransportOptionsAdapter(transportOptions); - turnTimeoutMs = transportOptionsAdapter.getHandledTransportOptions().getTurnTimeoutMs(); - messageTimeoutMs = transportOptionsAdapter.getHandledTransportOptions().getMessageTimeoutMs(); + this.transportOptions = transportOptions; start(); } - protected void start() throws IOException { + @Override + public TransportOptions getTransportOptions() { + return transportOptions; + } + + @Override + public void start() throws IOException { + TransportOptionsAdapter transportOptionsAdapter = new TransportOptionsAdapter(transportOptions); + this.turnTimeoutMs = transportOptionsAdapter.getHandledTransportOptions().getTurnTimeoutMs(); + this.messageTimeoutMs = transportOptionsAdapter.getHandledTransportOptions().getMessageTimeoutMs(); + String[] commandArgs = transportOptionsAdapter.buildCommandArgs(); log.debug("trans to command args: {}", transportOptionsAdapter); diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/TransportOptionsAdapter.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/TransportOptionsAdapter.java index 4c6b7d48d..1f179f93e 100644 --- a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/TransportOptionsAdapter.java +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/TransportOptionsAdapter.java @@ -72,6 +72,11 @@ class TransportOptionsAdapter { if (transportOptions.getIncludePartialMessages() != null && transportOptions.getIncludePartialMessages()) { args.add("--include-partial-messages"); } + + if (StringUtils.isNotBlank(transportOptions.getResumeSessionId())) { + args.add("--resume"); + args.add(transportOptions.getResumeSessionId()); + } return args.toArray(new String[] {}); } diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/session/SessionTest.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/session/SessionTest.java index 69898b948..51c37c6c8 100644 --- a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/session/SessionTest.java +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/session/SessionTest.java @@ -3,17 +3,23 @@ package com.alibaba.qwen.code.cli.session; import java.io.IOException; import com.alibaba.fastjson2.JSON; +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Allow; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlPermissionRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; import com.alibaba.qwen.code.cli.session.event.SessionEventConsumers; import com.alibaba.qwen.code.cli.session.event.SessionEventSimpleConsumers; -import com.alibaba.qwen.code.cli.session.exception.SessionCloseException; +import com.alibaba.qwen.code.cli.session.exception.SessionControlException; import com.alibaba.qwen.code.cli.session.exception.SessionSendPromptException; -import com.alibaba.qwen.code.cli.session.exception.SessionStartException; import com.alibaba.qwen.code.cli.transport.Transport; import com.alibaba.qwen.code.cli.transport.process.ProcessTransport; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,34 +27,111 @@ import org.slf4j.LoggerFactory; class SessionTest { private static final Logger log = LoggerFactory.getLogger(SessionTest.class); + @Test - void sendPrompt() throws IOException, SessionStartException, SessionSendPromptException, SessionCloseException { + void setPermissionModeSuccessfully() throws IOException, SessionControlException, SessionSendPromptException { Transport transport = new ProcessTransport(); Session session = new Session(transport); - session.sendPrompt("hello world", new SessionEventSimpleConsumers() { + + session.setPermissionMode(PermissionMode.YOLO); + session.sendPrompt("in the dir src/test/temp/, create file empty file test.touch", new SessionEventSimpleConsumers()); + + session.setPermissionMode(PermissionMode.PLAN); + session.sendPrompt("rename test.touch to test_rename.touch", new SessionEventSimpleConsumers()); + + session.setPermissionMode(PermissionMode.AUTO_EDIT); + session.sendPrompt("rename test.touch to test_rename.touch", new SessionEventSimpleConsumers()); + + session.sendPrompt("rename test.touch to test_rename.touch again user will allow", new SessionEventSimpleConsumers() { + public Behavior onPermissionRequest(Session session, CLIControlRequest permissionRequest) { + log.info("permissionRequest: {}", permissionRequest); + return new Allow().setUpdatedInput(permissionRequest.getRequest().getInput()); + } + }); + + session.close(); + } + + @Test + void sendPromptAndSetModelSuccessfully() throws IOException, SessionControlException, SessionSendPromptException { + Transport transport = new ProcessTransport(); + Session session = new Session(transport); + + session.setModel("qwen3-coder-flash"); + writeSplitLine("setModel 1 end"); + + session.sendPrompt("hello world", new SessionEventSimpleConsumers()); + writeSplitLine("prompt 1 end"); + + session.setModel("qwen3-coder-plus"); + writeSplitLine("setModel 1 end"); + + session.sendPrompt("查看下当前目录有多少个文件", new SessionEventSimpleConsumers()); + writeSplitLine("prompt 2 end"); + + session.setModel("qwen3-max"); + writeSplitLine("setModel 1 end"); + + session.sendPrompt("查看下当前目录有多少个xml文件", new SessionEventSimpleConsumers()); + writeSplitLine("prompt 3 end"); + + session.close(); + } + + @Test + void sendPromptAndInterruptContinueSuccessfully() throws IOException, SessionControlException, SessionSendPromptException { + Transport transport = new ProcessTransport(); + Session session = new Session(transport); + + SessionEventConsumers sessionEventConsumers = new SessionEventSimpleConsumers() { @Override - public void onSystemMessage(SDKSystemMessage systemMessage) { + public void onSystemMessage(Session session, SDKSystemMessage systemMessage) { log.info("systemMessage: {}", systemMessage); } @Override - public void onResultMessage(SDKResultMessage resultMessage) { + public void onResultMessage(Session session, SDKResultMessage resultMessage) { log.info("resultMessage: {}", resultMessage); } @Override - public void onAssistantMessage(SDKAssistantMessage assistantMessage) { + public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) { log.info("assistantMessage: {}", assistantMessage); + try { + session.interrupt(); + } catch (SessionControlException e) { + log.error("interrupt error", e); + } } @Override - public void onOtherMessage(String message) { + public void onControlResponse(Session session, CLIControlResponse cliControlResponse) { + log.info("cliControlResponse: {}", cliControlResponse); + } + + @Override + public void onOtherMessage(Session session, String message) { log.info("otherMessage: {}", message); } - }); + }; + session.sendPrompt("查看下当前目录有多少个文件", sessionEventConsumers); + writeSplitLine("prompt 1 end"); + + session.continueSession(); + session.sendPrompt("hello world", sessionEventConsumers); + writeSplitLine("prompt 2 end"); + + session.continueSession(); + session.sendPrompt("当前目录有多少个java文件", sessionEventConsumers); + writeSplitLine("prompt 3 end"); + session.close(); } + public void writeSplitLine(String line) { + log.info("{} {}",line, StringUtils.repeat("=", 300)); + } + @Test void testJSON() { String json = "{\"type\":\"assistant\",\"uuid\":\"ed8374fe-a4eb-4fc0-9780-9bd2fd831cda\",\"session_id\":\"166badc0-e6d3-4978-ae47-4ccd51c468ef\",\"message\":{\"content\":[{\"text\":\"Hello! How can I help you with the Qwen Code SDK for Java today?\",\"type\":\"text\"}],\"id\":\"ed8374fe-a4eb-4fc0-9780-9bd2fd831cda\",\"model\":\"qwen3-coder-plus\",\"role\":\"assistant\",\"type\":\"message\",\"usage\":{\"cache_read_input_tokens\":12766,\"input_tokens\":12770,\"output_tokens\":17,\"total_tokens\":12787}}}"; diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java index 7707c5fda..97e6fe0d1 100644 --- a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java @@ -1,5 +1,7 @@ package com.alibaba.qwen.code.cli.transport; +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; + import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/packages/sdk-java/todo b/packages/sdk-java/todo new file mode 100644 index 000000000..656489715 --- /dev/null +++ b/packages/sdk-java/todo @@ -0,0 +1,6 @@ +1、event timeout +2、mcp servers +3、errorHandle +4、review QwenCli +https://github.com/QwenLM/qwen-code/tree/main/packages/sdk-typescript#custom-permission-handler +