* feat(managed-agent): H4d-b session message runtime Produce the records the H4d-a contract pinned. The Hosted Shell lanes declare a managed send_message: a parent messages the child tasks it launched by task id, a child messages its parent. A running child gets a message, a completed child gets a continuation that carries its chain's history, and any other ending gets a named refusal. The routing decision shares the child funnel's writes chain with settlement, so a message either opens before the run settles and holds that settlement, or finds the run ended. A new message relay in managed-agent-server fixes each message's target at the handover, commits the receipt into the target together with the input and wake that carry it, and advances the sender as the target accepts and consumes it, with its own V61 ledger for leases, backoff and the orphaned/unknown classifications. The target reads a message as a wake turn and consumes the receipt when that turn completes. The child result relay now settles a child only once no message on its edge owes a handover and its journal holds no pending input, and takes the result from the child's newest settled turn, read from the child's own journal when a message's wake turn ran last. The parent refuses a result commit with child_messages_pending while a message to the run is undelivered. A continuation's first input is composed from the parent's committed instructions and results, bounded below the Hosted prompt limit. session_message joins the enabled domains, the continuation gate opens, the Hosted recovery allowlist admits the domain, and send_message joins the input-preview set in both languages. No public contract change. Refs #12380, #12827, #13744 (slice H4d-b) * fix(managed-agent): close the H4d-b round-1 audit findings Five independent audits of the session message runtime found these. Each one is closed here. A give-up now ends the entry on its sender before it classifies the ledger row unknown: cancelled if the entry was never handed over, accepted if its receipt exists, unknown otherwise. Undelivered now means planned or accepting in both languages, so a message the relay gave up on no longer holds the child's settlement forever. The child result relay reads the child's journal first and its edge after. Only delivered messages waiting for their turn hold the settlement, and only for 30 minutes without activity. The result is the newest settled API or message turn. The settlement names the messageCount the relay read, and the parent refuses it when a newer message exists, which closes the window between the reads and the commit. A committed result is accepted without being recomputed. The message relay decides from the entry as committed now. It pages waiting entries from its ledger index, so an accepted entry whose advance was lost is not stranded. It cancels messages to a child being stopped. A target store fault is a 503 retry, never a rejection. A closed sender's entry is classified delivered when its receipt exists. Waiting entries are reconciled through a new consume operation, which also reloads a target that a replaced Harness dropped. Senders are bounded to 8 messages in flight and 64 in all per child run and direction. A closing parent sends no messages. Refusals from the store become tool errors. A receipt for an unknown run is refused before anything publishes, and the route bounds the content it accepts. Continuation history is now XML-escaped. V61 gains a per-Session index. The migration IT lists V61. The foreground-agent sibling check no longer exempts send_message, to match #13769's recovery. Refs #12380, #12827, #13744 (slice H4d-b) * fix(managed-agent): gate message receipts like other new-work relays - Route receive and consume through the Workspace admission the automation relay runs, and re-attach a Session a prior control-plane process attached through the takeover load for every message verb. - Refuse an accepted step whose input id is not the one the message id derives, pinning the Java relay's derivation to the Hosted one. - Keep the attempt count across a forward ledger step, matching the child result relay. - Record that a failed message wake turn fails the child on purpose. * fix(managed-agent): leave the message ledger to instances with a Harness A Session Store replica runs without a Harness, and its message relay claimed entries it could only fail, counting attempts against them and pushing their real worker behind the backoff. Skip the scan there, as the turn coordinator does. * fix(managed-agent): close the H4d-b round-2 audit findings - Cross-check the edge against the journal read: a parent message the child received between the two reads holds the settlement instead of being closed away unread. - Count any journal event as activity for the message-turn wait, so a long running message turn is never cut off at 30 minutes. - Stop counting a closed child's orphaned outbox as owed, and hold the close of an active child until its own messages left. - Let a committed result or failure stand when a newer turn would re-settle the run the other way. - Read the child journal only when the child holds session messages. - Decide a give-up from the entry as committed after the failed step. - Count only messages not yet handed over against the in-flight cap. - Fail a send_message call on the store's own faults instead of answering a refusal for a message that may still commit. - Join journal text runs a thought separates with a newline, as the API turn result does. * fix(managed-agent): bound the hold on a child's own messages by the give-up A child that can take no further revision (a declined recovery) never lets the message relay's give-up land its sender step, so its outbox entries stayed planned and held the child's settlement and close forever. An entry whose ledger row is classified or past the attempt bound no longer holds either. * fix(managed-agent): fail a child whose message turn never comes Past the 30-minute wait without a journal event, a child still holding an unread message settled from its earlier turn's result, reporting the message as acted on; a turn queued behind a busy shared mount reaches that bound without any fault. Settle it as child_failed instead. * fix(managed-agent): hold a child's message for its task and refuse it once a close began A run attaches as soon as its Runtime binding exists, before the coordinator submits the child's task, so a parent's message could be received first: its wake turn ran without the task and the task then met the busy Session. The relay now hands a message to a child only once the Harness admitted the child's task (its first Turn carries the admission epoch), and cancels it when that task ended without an admission, so the child's settlement is never held by it. The message route now refuses every verb with hosted_session_closing once the Session's close began, like the channel and automation routes, and the close waits for an operation already past that check before it cancels the pending inputs, so no receipt lands after that pass. * fix(managed-agent): stop a stopped run's message turns and answer interrupted send_message calls Main's H4f stop arm decides from the child's API Turn, but a message's wake turn never becomes one, so a stopped run could settle cancelled while a message turn kept running in its child, and a stop met a child whose task had completed but whose messages were still owed as a natural completion. For a child holding session messages the stop arm now reads the child's journal: while a message input waits or runs it sends the child's message route a new `stop`, which keeps the wake pump from starting any message input, aborts a message turn in flight and, once no turn runs, settles the waiting ones cancelled with stop_requested. The natural outcome is the newest settled turn, API or message; a stop the child cannot take yet spends no attempt, and a message input no turn took within the 30-minute bound stops holding the stop. A message handed over before the stop but not yet received is rejected instead of waking the child. A send_message writes only to the journal before its answer, like a team call. Every interrupted-turn route from main's #13829, and the parked round's gap fill that runs before them, now answers it by what it committed: an outbox entry as queued, a continuation as continued, and one that committed nothing as never run where the turn resumes, so the model never sends a committed message twice. The relay's migration moves from V61 to V64: main has V62, and other open pull requests claim V61 and V63. * fix(managed-agent): carry a message stop into the load that attaches its child A stopped run's message stop went through the takeover load every message verb uses when the control plane holds no attachment for the child. That load drives recovery and kicks the child's wake pump before the stop itself arrives, so a queued message turn could start in that window, and the later stop could only abort work already begun. The stop now has its own attachment path: a child this process does not hold is loaded passively with `stopMessages`, which marks its message inputs stopped before the load kicks the pump, under the cancellation authorization a stop needs rather than the grant new work needs. A held child takes the stop directly. * fix(managed-agent): authorize a message stop by its committed request and keep stopped children unloaded The message stop's own load took the cancellation authorization, whose query needs a CANCELLING Turn. Message work never leaves one, and the stop arm skips the Turn cancel once the task ended, so a child the control plane no longer held could not be stopped at all. The load is now authorized like a persisted cancellation, by the committed stop the child result relay read: only the Session's own binding is checked, so neither the missing Turn nor a grant changed since refuses it. Other loads could still attach a stopped run's child without the stop. The stop now goes out before the arm's Turn cancel, whose own load carries none, and the message relay no longer attaches such a child for a consume reconciliation or for the child's own outbox steps; those wait for its close. A message turn that crashed in an earlier process is no longer left owed: the pump skips a stopped Session's message inputs, so the stop runs its aftermath. The Hosted load now marks a resident Session stopped only after the load passes its identity and admission checks, so a refused load changes nothing. The 30-minute message-turn bound counted a resident Session's lease renewals as activity, so it never ended a blocked child's wait; renewals no longer count. * fix(managed-agent): hold a stopped child's message steps only while its stop still settles message work The message relay held every outbox step of a stopped run's child until the child closed. When the stop met a natural end (the task completed or failed on its own, no message work owed), the child result relay settles through its ordinary walk, which waits for the child's own outbox to be handed over before it settles or closes, so the two relays waited for each other with no attempt spent and no bound to break it. The hold now lasts only while the child still owes message work the stop is settling; after that a load starts nothing, and the steps go on. A resident Session's stop flag was set before the passive load checked its tool profile, so a load refused for a profile conflict still stopped the Session's queued messages. Each load branch now sets it only after its own refusals. * fix(managed-agent): bind the claimant in the relay lease CAS and refuse a closing child's message to its parent Both relay ledgers guarded the lease takeover with the owner their read saw rather than the claimant. Two workers that both read an expired lease could therefore both win it: the first renewed it as its own, and the second still matched the stale owner. The guard now binds the claimant, so the second meets the first one's fresh lease. A child's send_message to its parent took no closing flag, so a call in flight when the Session's close began still opened an outbox entry the closing sender could only orphan, while the model was told it was queued. It now refuses like the parent-to-child arm, after a committed message's replay. * test(managed-agent): expect send_message among a Shell-lane Session's tools in the Hosted IT driver The Hosted workspace tool-turn integration driver pins the exact tool set a Shell-lane root Session advertises. Since this change that set carries the parent form of send_message beside the Agent tool, so the MySQL process gate failed on its first model call. |
||
|---|---|---|
| .. | ||
| src | ||
| .editorconfig | ||
| .gitignore | ||
| checkstyle.xml | ||
| LICENSE | ||
| pom.xml | ||
| QWEN.md | ||
| README.md | ||
| RELEASE.md | ||
Qwen Code Java SDK
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.
Requirements
- Java >= 11 for
0.1.0-alpha - Maven >= 3.9.2 when building or publishing this SDK from source
- A compatible
qwen servefor the daemon API, or qwen-code >= 0.5.0 for the legacy stdio API
Dependencies
- Logging API: org.slf4j:slf4j-api (choose an SLF4J provider in your application)
- Utilities: org.apache.commons:commons-lang3
- JSON Processing: Fastjson2 for encoding and Jackson Core for strict decoding
- Testing: JUnit 5 (org.junit.jupiter:junit-jupiter)
Installation
Add 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'
Building and Running
Build Commands
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
Real daemon E2E from source
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.
Recommended daemon API
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, restart-safe event cursor epochs from #7458, 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());
}
Callers that need to allocate the session identity before creation can pass an RFC UUID v1-v5. The SDK checks session_id_override before the mutation and reports a different returned ID as SessionCreationOutcomeUnknownException:
CreateSessionRequest request = CreateSessionRequest.builder()
.sessionId("550E8400-E29B-41D4-A716-446655440000")
.build();
try (DaemonSessionClient session = daemon.createSession(request)) {
System.out.println(session.getSession().getSessionId());
}
The daemon normalizes the ID to lowercase and creates a new thread session. This is not an idempotent attach; after an ambiguous create outcome, recover with the known ID rather than retrying creation.
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, pairs the cursor with X-Qwen-Event-Epoch when the daemon supplies an epoch, validates framing and event IDs, deduplicates replay, and reconnects only the SSE GET. It learns an epoch from the validated prompt admission or SSE response headers and fails closed if the epoch changes during prompt observation. Older daemons that omit both surfaces remain compatible but retain their numeric-only stale-cursor detection. 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 a caller supplies a session ID, the SDK additionally requires session_id_override before sending the mutation. 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.
Hosted Harness private API
HostedHarnessClient is the Java control-plane transport for qwen serve --profile hosted-harness. Construction performs capability negotiation, verifies the deployment capability digest, and pins the process bootId. Every Session request then carries protocol and generation fencing automatically. This API keeps model-loop events raw; product services remain responsible for public Session, Turn, Event, tenant, and workspace authorization state.
Map<String, Object> text = Map.of("type", "text", "text", "hello");
String payloadDigest = SubmitHarnessTurn.computePayloadDigest(List.of(text));
try (HostedHarnessClient harness = HostedHarnessClient.builder()
.baseUri(URI.create("http://127.0.0.1:4170"))
.bearerToken(System.getenv("QWEN_SERVER_TOKEN"))
.capabilityDigest(System.getenv(
"QWEN_HOSTED_HARNESS_CAPABILITY_DIGEST"))
.build()) {
HarnessSessionRef session = harness.createSession(
CreateHarnessSession.builder()
.harnessSessionId(UUID.randomUUID().toString())
.build());
PromptReceipt receipt = harness.submitTurn(
SubmitHarnessTurn.builder()
.session(session)
.promptId(UUID.randomUUID().toString())
.addContent(text)
.payloadDigest(payloadDigest)
.build());
try (HarnessEventStream events = harness.streamEvents(
StreamHarnessEvents.builder()
.session(session)
.lastEventId(receipt.getLastEventId())
.eventEpoch(receipt.getEventEpoch())
.build())) {
for (DaemonEvent event = events.next(); event != null;
event = events.next()) {
// Persist/project the raw Harness event in the Java product service.
}
}
harness.detachSession(session);
}
The client permits only one active prompt identity per Session while allowing different Sessions to run concurrently. A retry after PromptAdmissionUnknownException must reuse the same promptId, payload digest, prompt content, and deadline. SSE boot, epoch, or sequence changes fail closed for transcript/status reconciliation. close() releases local HTTP, heartbeat, timer, and stream resources only; it never destroys remote Sessions implicitly.
Legacy stdio API
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", "glob"));
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
Java 11 migration and alpha limits
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 detects a daemon event-epoch change during an observed prompt but does not automatically recover from it. 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.
Architecture
The artifact contains two isolated implementations:
- Daemon API:
DaemonClientandDaemonSessionClientuse REST mutations plus resumable SSE and own bounded HTTP, prompt, maintenance, and timer resources. - Legacy stdio API:
QwenCodeCli,Session, andProcessTransportmanage 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.
Legacy stdio features
Permission Modes
The SDK supports different permission modes for controlling tool execution:
default: Write tools are denied unless approved viacanUseToolcallback or inallowedTools. 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.auto: An LLM classifier approves tool calls.yolo: All tools execute automatically without confirmation.
Session Event Consumers and Assistant Content Consumers
The SDK provides two key interfaces for handling events and content from the CLI:
SessionEventConsumers Interface
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, returns Behavior)
AssistantContentConsumers Interface
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)
Relationship Between the Interfaces
Important Note on Event Hierarchy:
SessionEventConsumersis the high-level event processor that handles different message types (system, assistant, user, etc.)AssistantContentConsumersis 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,onOtherControlRequest
Event 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↔onControlRequestTimeout
For AssistantContentConsumers timeout methods:
onText↔onTextTimeoutonThinking↔onThinkingTimeoutonToolUse↔onToolUseTimeoutonToolResult↔onToolResultTimeoutonOtherContent↔onOtherContentTimeoutonPermissionRequest↔onPermissionRequestTimeoutonOtherControlRequest↔onOtherControlRequestTimeout
Default Timeout Values:
SessionEventSimpleConsumersdefault timeout: 180 seconds (Timeout.TIMEOUT_180_SECONDS)AssistantContentSimpleConsumersdefault timeout: 60 seconds (Timeout.TIMEOUT_60_SECONDS)
Timeout Hierarchy Requirements:
For proper operation, the following timeout relationships should be maintained:
onAssistantMessageTimeoutreturn value should be greater thanonTextTimeout,onThinkingTimeout,onToolUseTimeout,onToolResultTimeout, andonOtherContentTimeoutreturn valuesonControlRequestTimeoutreturn value should be greater thanonPermissionRequestTimeoutandonOtherControlRequestTimeoutreturn values
Transport Options
The 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 CLI
Session Control Features
- Session creation: Use
QwenCodeCli.newSession()to create a new session with custom options - Session management: The
Sessionclass provides methods to send prompts, handle responses, and manage session state - Session cleanup: Always close sessions using
session.close()to properly terminate the CLI process - Session resumption: Use
setResumeSessionId()inTransportOptionsto resume a previous session - Session interruption: Use
session.interrupt()to interrupt a currently running prompt - Dynamic model switching: Use
session.setModel()to change the model during a session - Dynamic permission mode switching: Use
session.setPermissionMode()to change the permission mode during a session
Thread Pool Configuration
The SDK uses a thread pool for managing concurrent operations with the following default configuration:
- Core Pool Size: 30 threads
- Maximum Pool Size: 100 threads
- Keep-Alive Time: 60 seconds
- Queue Capacity: 300 tasks (using LinkedBlockingQueue)
- Thread Naming: "qwen_code_cli-pool-{number}"
- Daemon Threads: true
- Rejected Execution Handler: CallerRunsPolicy
Tasks still running or queued in the default pool are abandoned when the JVM exits. Callers that require completion must wait for those tasks explicitly.
Error Handling
The SDK provides specific exception types for different error scenarios:
SessionControlException: Thrown when there's an issue with session control, including attempting to use a closed or unavailable session. Session construction andstart()can throw it directly;QwenCodeCli.newSession()wraps lower-level creation and initialization failures in aRuntimeException.SessionSendPromptException: Thrown when there's an issue sending a prompt or receiving a response
FAQ / Troubleshooting
Q: What Java versions are supported?
A: 0.1.0-alpha requires Java 11 or higher. Java 8 users must remain on 0.0.3-alpha.
Q: How do I handle long-running requests?
A: The SDK includes timeout utilities. You can configure timeouts using the Timeout class in TransportOptions.
Q: Why are some tools not executing?
A: This is likely due to permission modes. Check your permission mode settings and consider using allowedTools to pre-approve certain tools.
Q: How do I resume a previous session?
A: Use the setResumeSessionId() method in TransportOptions to resume a previous session.
Q: Can I customize the environment for the CLI process?
A: Yes, use the setEnv() method in TransportOptions to pass environment variables to the CLI process.
License
Apache-2.0 - see LICENSE for details.