docs/developers/sdk-java.md
The Qwen Code Java SDK provides a recommended daemon transport for qwen serve and retains the experimental legacy stdio API for compatibility. Both APIs ship in the same com.alibaba:qwencode-sdk artifact.
0.1.0-alphaqwen serve for the daemon API, or qwen-code >= 0.5.0 for the legacy stdio APIAdd the following dependency to your Maven pom.xml:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<version>0.1.0-alpha</version>
</dependency>
Or if using Gradle, add to your build.gradle:
implementation 'com.alibaba:qwencode-sdk:0.1.0-alpha'
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
Run the real-daemon Java integration tests from the repository root after building both the workspaces and the root CLI bundle:
npm run build
npm run bundle
npx tsx scripts/run-java-daemon-sdk-e2e.ts
npm run build alone does not refresh dist/cli.js; the E2E harness launches that bundle and fails with an explicit prerequisite error when it is missing.
Start qwen serve, then create an independent thread-scoped session. promptText returns only after a matching turn_complete; incomplete streams fail with PromptOutcomeIndeterminateException rather than returning partial text as success.
For the lifecycle guarantees assumed by 0.1.0-alpha, use the qwen-code build released from the same source revision as the SDK. The daemon must contain the idempotent per-client detach ledger from #7386, the per-epoch terminal guarantee from #7400, and this release's acknowledged admission cancellation plus FIFO cancel-drain fence. The #7400 commit alone is not sufficient: a same-wire daemon can acknowledge cancel before agent dispatch without stopping the admitted prompt, or let an unacknowledged session-scoped cancel reach a queued successor. The bundled ACP child uses one acknowledged admission-aware cancellation handshake; a custom standards-compliant ACP child without that extension receives one standard session/cancel notification. Feature negotiation cannot distinguish older same-wire daemon builds, so the SDK fails closed rather than reporting partial output as success.
The bundled cancellation handshake deliberately waits for the targeted prompt call to settle before the daemon dispatches its queued successor. It has no timeout that merely acknowledges cancellation: doing so could let a late session-scoped cancel reach the next prompt. If a provider, tool, or custom integration ignores its AbortSignal indefinitely, the cancel mutation can therefore remain outcome-unknown and that session must not be reused. Treat a formal prompt terminal received within the caller's observation boundary as authoritative; otherwise close or destroy the session after observation fails. Recovering a wedged shared ACP child without disturbing its sibling sessions requires stronger runtime isolation and is outside this alpha contract.
import com.alibaba.qwen.code.daemon.DaemonClient;
import com.alibaba.qwen.code.daemon.DaemonSessionClient;
import com.alibaba.qwen.code.daemon.PromptTextResult;
import java.net.URI;
try (DaemonClient daemon = DaemonClient.builder()
.baseUri(URI.create("http://127.0.0.1:4170"))
.build();
DaemonSessionClient session = daemon.createSession()) {
PromptTextResult result = session.promptText("Explain this repository");
System.out.println(result.getText());
}
If qwen serve requires authentication, add
.bearerToken(System.getenv("QWEN_SERVER_TOKEN")) to the DaemonClient
builder. The SDK sends the bearer on REST and SSE requests and never puts it in
the URL.
Use startPrompt with a PromptObserver when you need ordered text, thought, tool, usage, permission, and raw event callbacks. Its acceptanceFuture() and completionFuture() views separately expose daemon admission and the reliable turn terminal. respondToPermission() returns false when the request was already resolved or no longer pending. Cancelling the future views does not cancel the daemon prompt; use cancelActivePrompt() for the session-level daemon cancel operation and still wait for the matching terminal. A cooperative cancellation completes with turn_complete and stopReason=cancelled; promptText() returns its PromptTextResult, so callers that distinguish cancellation must inspect result.getTerminal().getStopReason(). If the agent or provider fails while cancelling, the daemon can instead publish turn_error, which makes promptText() throw PromptTurnException.
When cancellation, deadline, teardown, or agent settlement race, the daemon's exactly-once latch publishes the first formal terminal and suppresses later candidates. Always branch on the received terminal itself; the last control mutation sent by the client does not determine the terminal kind or error code.
The SSE transport sends Accept-Encoding: identity and Last-Event-ID, validates framing and event IDs, deduplicates replay, and reconnects only the SSE GET. Prompt and other mutation requests are never retried automatically. HTTP 408 and 5xx responses to prompt admission, session creation, permission, cancel, heartbeat, detach, or delete are reported as outcome-unknown because they do not prove that the daemon rejected the mutation. Finite response bodies and SSE observation have independent deadlines.
Creation-time model selection is intentionally not exposed by the Java daemon SDK API in this alpha. The daemon reports a rejected modelServiceId only as an SSE event emitted before the create response, while this SDK opens its stream from the later prompt-admission watermark. Until the daemon returns a definitive create result or the SDK owns a separate session-event subscription from Last-Event-ID: 0, use the daemon's configured default model.
PromptRequest.Builder.deadline(Duration) requests a daemon-enforced prompt deadline and is accepted only when the daemon advertises prompt_absolute_deadline; otherwise the SDK fails before sending the prompt. The value must be between 1 and 2,147,483,647 milliseconds, matching the daemon's Node timer range. This is separate from observationTimeout(Duration), which only bounds local SSE observation and never sends a cancel mutation.
Before creating a session, the SDK requires the daemon to advertise the REST transport and session_scope_override; this prevents an older daemon from silently ignoring the requested thread scope and attaching the client to a shared session. When client_heartbeat is advertised, an open session sends a fresh heartbeat every minute so the daemon does not reap an otherwise idle client. Set heartbeatInterval(Duration.ZERO) on the DaemonClient builder to disable this behavior, or choose a different positive interval. A heartbeat is never retried; the next scheduled heartbeat is a separate keepalive. Prompt observation is bounded to 32 concurrent prompts per client by default and can be adjusted with maximumConcurrentPrompts. Admission and terminal future callbacks run away from transport workers; callbacks that remain blocked consume bounded publication capacity. SSE stream cleanup is also bounded, and a close that remains blocked retains its cleanup reservation. Either condition can cause a later startPrompt to fail with DaemonClientCapacityException rather than dropping a timeout close or growing threads and queued work without limit.
An indeterminate completion is an outcome boundary, not a session-reuse boundary. After PromptAdmissionUnknownException or PromptOutcomeIndeterminateException, that DaemonSessionClient permanently rejects further prompts even if local stream cleanup later succeeds; close or destroy the session instead. An observation timeout is published without waiting forever for a blocked stream close, while cleanup continues asynchronously and retains bounded client capacity until it finishes.
The existing com.alibaba.qwen.code.cli API remains available:
public static void runSimpleExample() {
List<String> result = QwenCodeCli.simpleQuery("hello world");
result.forEach(logger::info);
}
For more advanced usage with custom transport options:
public static void runTransportOptionsExample() {
TransportOptions options = new TransportOptions()
.setModel("qwen3-coder-flash")
.setPermissionMode(PermissionMode.AUTO_EDIT)
.setCwd("./")
.setEnv(new HashMap<String, String>() {{put("CUSTOM_VAR", "value");}})
.setIncludePartialMessages(true)
.setTurnTimeout(new Timeout(120L, TimeUnit.SECONDS))
.setMessageTimeout(new Timeout(90L, TimeUnit.SECONDS))
.setAllowedTools(Arrays.asList("read_file", "write_file", "list_directory"));
List<String> result = QwenCodeCli.simpleQuery("who are you, what are your capabilities?", options);
result.forEach(logger::info);
}
For streaming content handling with custom content consumers:
public static void runStreamingExample() {
QwenCodeCli.simpleQuery("who are you, what are your capabilities?",
new TransportOptions().setMessageTimeout(new Timeout(10L, TimeUnit.SECONDS)), new AssistantContentSimpleConsumers() {
@Override
public void onText(Session session, TextAssistantContent textAssistantContent) {
logger.info("Text content received: {}", textAssistantContent.getText());
}
@Override
public void onThinking(Session session, ThinkingAssistantContent thinkingAssistantContent) {
logger.info("Thinking content received: {}", thinkingAssistantContent.getThinking());
}
@Override
public void onToolUse(Session session, ToolUseAssistantContent toolUseContent) {
logger.info("Tool use content received: {} with arguments: {}",
toolUseContent, toolUseContent.getInput());
}
@Override
public void onToolResult(Session session, ToolResultAssistantContent toolResultContent) {
logger.info("Tool result content received: {}", toolResultContent.getContent());
}
@Override
public void onOtherContent(Session session, AssistantContent<?> other) {
logger.info("Other content received: {}", other);
}
@Override
public void onUsage(Session session, AssistantUsage assistantUsage) {
logger.info("Usage information received: Input tokens: {}, Output tokens: {}",
assistantUsage.getUsage().getInputTokens(), assistantUsage.getUsage().getOutputTokens());
}
}.setDefaultPermissionOperation(Operation.allow));
logger.info("Streaming example completed.");
}
other examples see src/test/java/com/alibaba/qwen/code/cli/example
0.1.0-alpha raises the minimum Java version for the whole artifact from 8 to 11. Java 8 applications must remain on 0.0.3-alpha. Logback is no longer a runtime dependency; add the SLF4J provider your application uses.
This alpha deliberately fails closed when it cannot prove a prompt terminal. It does not guarantee exactly-once execution across daemon restarts, automatic epoch recovery, snapshot/resync, persisted cursors, or true prompt-ID-targeted cancellation. prompt_cancelled and queue events are advisory; only matching turn_complete and turn_error are terminal.
If session creation has an ambiguous transport outcome, the daemon may retain a session whose ID never reached the caller. The SDK does not retry creation and cannot detach that unknown session; daemon-side lifecycle reaping is the recovery boundary.
The artifact contains two isolated implementations:
DaemonClient and DaemonSessionClient use REST mutations plus resumable SSE and own bounded HTTP, prompt, maintenance, and timer resources.QwenCodeCli, Session, and ProcessTransport manage a child CLI process using the existing CLI protocol DTOs and utilities.The daemon implementation does not reuse the legacy process transport, session model, DTOs, or global executor.
The SDK supports different permission modes for controlling tool execution:
default: Write tools are denied unless approved via canUseTool callback or in allowedTools. Read-only tools execute without confirmation.plan: Blocks all write tools, instructing AI to present a plan first.auto-edit: Auto-approve edit tools (edit, write_file, notebook_edit) while other tools require confirmation.yolo: All tools execute automatically without confirmation.The SDK provides two key interfaces for handling events and content from the CLI:
The SessionEventConsumers interface provides callbacks for different types of messages during a session:
onSystemMessage: Handles system messages from the CLI (receives Session and SDKSystemMessage)onResultMessage: Handles result messages from the CLI (receives Session and SDKResultMessage)onAssistantMessage: Handles assistant messages (AI responses) (receives Session and SDKAssistantMessage)onPartialAssistantMessage: Handles partial assistant messages during streaming (receives Session and SDKPartialAssistantMessage)onUserMessage: Handles user messages (receives Session and SDKUserMessage)onOtherMessage: Handles other types of messages (receives Session and String message)onControlResponse: Handles control responses (receives Session and CLIControlResponse)onControlRequest: Handles control requests (receives Session and CLIControlRequest, returns CLIControlResponse)onPermissionRequest: Handles permission requests (receives Session and CLIControlRequest<CLIControlPermissionRequest>, returns Behavior)The AssistantContentConsumers interface handles different types of content within assistant messages:
onText: Handles text content (receives Session and TextAssistantContent)onThinking: Handles thinking content (receives Session and ThinkingAssistantContent)onToolUse: Handles tool use content (receives Session and ToolUseAssistantContent)onToolResult: Handles tool result content (receives Session and ToolResultAssistantContent)onOtherContent: Handles other content types (receives Session and AssistantContent)onUsage: Handles usage information (receives Session and AssistantUsage)onPermissionRequest: Handles permission requests (receives Session and CLIControlPermissionRequest, returns Behavior)onOtherControlRequest: Handles other control requests (receives Session and ControlRequestPayload, returns ControlResponsePayload)Important Note on Event Hierarchy:
SessionEventConsumers is the high-level event processor that handles different message types (system, assistant, user, etc.)AssistantContentConsumers is the low-level content processor that handles different types of content within assistant messages (text, tools, thinking, etc.)Processor Relationship:
SessionEventConsumers → AssistantContentConsumers (SessionEventConsumers uses AssistantContentConsumers to process content within assistant messages)Event Derivation Relationships:
onAssistantMessage → onText, onThinking, onToolUse, onToolResult, onOtherContent, onUsageonPartialAssistantMessage → onText, onThinking, onToolUse, onToolResult, onOtherContentonControlRequest → onPermissionRequest, onOtherControlRequestEvent Timeout Relationships:
Each event handler method has a corresponding timeout method that allows customizing the timeout behavior for that specific event:
onSystemMessage ↔ onSystemMessageTimeoutonResultMessage ↔ onResultMessageTimeoutonAssistantMessage ↔ onAssistantMessageTimeoutonPartialAssistantMessage ↔ onPartialAssistantMessageTimeoutonUserMessage ↔ onUserMessageTimeoutonOtherMessage ↔ onOtherMessageTimeoutonControlResponse ↔ onControlResponseTimeoutonControlRequest ↔ onControlRequestTimeoutFor AssistantContentConsumers timeout methods:
onText ↔ onTextTimeoutonThinking ↔ onThinkingTimeoutonToolUse ↔ onToolUseTimeoutonToolResult ↔ onToolResultTimeoutonOtherContent ↔ onOtherContentTimeoutonPermissionRequest ↔ onPermissionRequestTimeoutonOtherControlRequest ↔ onOtherControlRequestTimeoutDefault Timeout Values:
SessionEventSimpleConsumers default timeout: 180 seconds (Timeout.TIMEOUT_180_SECONDS)AssistantContentSimpleConsumers default timeout: 60 seconds (Timeout.TIMEOUT_60_SECONDS)Timeout Hierarchy Requirements:
For proper operation, the following timeout relationships should be maintained:
onAssistantMessageTimeout return value should be greater than onTextTimeout, onThinkingTimeout, onToolUseTimeout, onToolResultTimeout, and onOtherContentTimeout return valuesonControlRequestTimeout return value should be greater than onPermissionRequestTimeout and onOtherControlRequestTimeout return valuesThe TransportOptions class allows configuration of how the SDK communicates with the Qwen Code CLI:
pathToQwenExecutable: Path to the Qwen Code CLI executablecwd: Working directory for the CLI processmodel: AI model to use for the sessionpermissionMode: Permission mode that controls tool executionenv: Environment variables to pass to the CLI processmaxSessionTurns: Limits the number of conversation turns in a sessioncoreTools: List of core tools that should be available to the AIexcludeTools: List of tools to exclude from being available to the AIallowedTools: List of tools that are pre-approved for use without additional confirmationauthType: Authentication type to use for the sessionincludePartialMessages: Enables receiving partial messages during streaming responsesturnTimeout: Timeout for a complete turn of conversationmessageTimeout: Timeout for individual messages within a turnresumeSessionId: ID of a previous session to resumeotherOptions: Additional command-line options to pass to the CLIQwenCodeCli.newSession() to create a new session with custom optionsSession class provides methods to send prompts, handle responses, and manage session statesession.close() to properly terminate the CLI processsetResumeSessionId() in TransportOptions to resume a previous sessionsession.interrupt() to interrupt a currently running promptsession.setModel() to change the model during a sessionsession.setPermissionMode() to change the permission mode during a sessionThe SDK uses a thread pool for managing concurrent operations with the following default configuration:
The SDK provides specific exception types for different error scenarios:
SessionControlException: Thrown when there's an issue with session control (creation, initialization, etc.)SessionSendPromptException: Thrown when there's an issue sending a prompt or receiving a responseSessionClosedException: Thrown when attempting to use a closed sessionA: Yes. The daemon API requires a compatible qwen serve; the legacy stdio
API requires qwen-code 0.5.0 or higher.
A: 0.1.0-alpha requires Java 11 or higher. Java 8 users must remain on 0.0.3-alpha.
A: The SDK includes timeout utilities. You can configure timeouts using the Timeout class in TransportOptions.
A: This is likely due to permission modes. Check your permission mode settings and consider using allowedTools to pre-approve certain tools.
A: Use the setResumeSessionId() method in TransportOptions to resume a previous session.
A: Yes, use the setEnv() method in TransportOptions to pass environment variables to the CLI process.
Apache-2.0 - see LICENSE for details.