* feat(managed-agent): H4 workflow child runtime (#13803)
Issue #13803's four exit checks land in one change because the
`managed-child_run` kind gate fires on every revision commitment: the
relay and cascade verbs on a workflow row would answer 409 until the
gate admits the kind, so the query widening (K2) and the gate flip
(K4) cannot be separated.
K1 — the ChildResultRelay discovery now selects `workflow` rows, and
creation branches on the record kind: a workflow launch mints exactly
one child Session under the parent's Workspace binding and lineage,
whose first Turn carries a `workflow_launch` block (pinned definition,
script, args) instead of a prompt. K3 — every produced launch pins
its definition from revision 1: the pin's digest is the sha256 of the
exact script bytes (revision 1 by convention, id `workflow/<name>`),
and the child's first Turn re-verifies the digest before spending
anything.
The daemon gains a `HOSTED_WORKFLOW_TOOL` declaration that mirrors
the Agent tool's admission behind its own kind reader; a workflow
child Session runs its first Turn deterministically in-process via
WorkflowRunner (its agent() calls are in-process AgentHeadless
subagents under ordinary caps, never nested managed grandchildren).
Wake, close, and consumption predicates treat both child Session
kinds through one funnel; quotas already count the two kinds
together.
One cross-language contract consequence: a workflow launch block is
the first prompt ever to carry fractional `args` across the daemon's
`sha256(JSON.stringify(prompt))` admission digest, so the SDK's JSON
digest substrate is canonicalized to ECMAScript byte-fidelity —
otherwise a fractional arg wedged the first Turn with a
deterministic `invalid_hosted_prompt`.
Physical acceptance in #13532's shape ran against the packaged stack:
end-to-end foreground and background workflow children with result
delivery verified on a real host; workspace-close cancellation is
blocked-with-reason on macOS (host capability, re-confirmed on the
final tree); a tampered pin refuses execution; the mid-interruption
re-claim path is partially observed and filed as issue #13838.
* fix(cli): refuse nested saved-workflow resolution outside the pinned slice (#13803)
The managed workflow child pins its script from the launch, but a
model-authored script's own workflow() call resolved saved workflows
the pin never examined, including bytes from the daemon host's own
~/.qwen/workflows (user tier). Admission now refuses any script whose
masked source invokes the injected workflow global, with the same
model-correctable text that names name, scriptPath, resumeFromRunId
and sourceRef as the deferred reference scope. The hosted runner
passage also passes restrictNestedScriptPaths as a second line, and
the declaration no longer advertises workflow() in this profile.
Witnesses: both workflow('x') and workflow({scriptPath}) forms refuse
without committing a child_run, the same text inside strings or
comments admits, and removing the admission gate turns both refusals
red.
* fix(sdk-java): render big numbers through the double the script peer holds (#13803)
The canonical writer computed its 2^53 exact-integer guard on a
value already narrowed through longValue(): Long.MIN_VALUE stayed
negative under Math.abs, and any BigInteger at or past 2^63 was
truncated -- sometimes sign-flipped -- before the check, printing
digits JSON.stringify never produces while both sides digest the
same corrupted text and therefore agree. Long and BigInteger now
flow through the shared double path, and the BigDecimal arm renders
through the double the parser could hold except in the subnormal
band, where the exact digits are the ECMAScript short form ('5e-324')
that a blunt double round-trip would print as '4.9e-324'.
Golden vectors pin Long.MIN_VALUE, 2^63, 2^64 and the decimal
examples (12345678901234567890.5 prints 12345678901234567000;
1E-400 prints 0; 1E+400 is refused); a 38-case parity probe matches
Node's JSON.stringify byte-for-byte, and removing each guard turns
the named witnesses red.
* fix(cli): close the review's pin-, admission-, preview- and doc-record gaps across the workflow child lane (#13803)
* fix(cli): adopt main's mount-refusal taxonomy in the workflow child admission path (#13803)
* fix(integration-tests): teach the hosted workspace tool-turn fixture the H4c workflow advertisement (#13803)
* fix(cli): harden the workflow child runtime lane against the review's round-1 findings (#13803)
Real-stack verification round 1 found the lexical admission's nested
workflow scan bypassable by indirection, and the foreground workflow
wait lost to any Harness restart. This change closes both:
- WorkflowRunner gains refuseNestedWorkflows: when set, every nested
workflow() resolution — by name or by path, aliased or through the
global — is refused, since any of them resolves bytes the launch pin
never examined. The managed workflow Turn passes it; the lexical
pre-launch scan stays as the model-friendly early refusal.
- acceptChildWorkflow's foreground arm parks #13708's durable
commitAwaitAgent checkpoint before it waits, exactly as the child
agent arm does, so a restart re-enters the wait from the journal
instead of declining the parent's Turn unresolved_after_settle.
Merge-integration cleanups ride along: the member-chain walk narrows
by kind where the roster view needs ChildAgentRun, and the recovery
gap fill and the shared ended text name the run's kind honestly.
Witnesses: indirection forms now settle failed with the named refusal;
the park commit is asserted with the exact checkpoint evidence; the
canonical-args refusal of the existing pre-commit gate is pinned
end to end (top-level and nested subnormals, nothing committed).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): settle a registry-cancelled workflow start as cancelled, not failed (#13803)
The real stack showed a WorkflowStartCancelledError — cancelStarting or
abortAll reaching the run's controller while the Turn's own signal stays
live — settling the child as child_failed because only the abort signal
classified the outcome. The turn now reclassifies that cause to
cancelled/cancelled, the same reading the Workflow tool gives for the
identical error; deadline_exceeded keeps its name, and error keeps its
name — only the generic error outcome yields to the cause.
Witness: the harness session settles with a cancelled turn_result when
the workflow start is torn down registry-side.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk-java): normalize lone surrogates before the digest and the insert, and keep protocol messages' field paths (#13803)
Two digest-honesty findings from the real stack and the reviews:
- The canonical encoder escapes an unpaired surrogate in the admission
digest, while no store column keeps the character; re-encoding the
read-back input then never matches the payloadDigest, and the Turn
retries forever (payloadDigest does not match promptContent). Input
admission now trades unpaired surrogates for U+FFFD before the payload
digest, the replay digest and the insert, on the API path and on the
child-creation path, so all three describe one representation.
- Four DaemonProtocolException messages that named only the context lost
their field path in this PR's digest work; they name the field again,
pinned by a witness that reads all four texts.
Witnesses: the child-creation and user-text admissions normalize before
the captured digest and insert, with valid pairs and non-strings left
untouched; the helper-rejection tests pass unchanged.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* docs(design): retire the stale closed-gate claims and record the runtime slice's settled honesty contract (#13803)
The H4c design's items 5 and 6 still named the kind gated and the Java
paths child-agent-only; the kind doc now names them closed only in its
own slice and opened with this one, in both languages. The runtime
design gains the behaviors the review rounds settled: the runner's
refuseNestedWorkflows line behind the lexical scan, the waiter arm's
durable park, the registry-side cancel settling cancelled through
WorkflowStartCancelledError, and lone-surrogate normalization at
input admission keeping the digest and the column one representation.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk-java): satisfy Checkstyle NeedBraces on the surrogate-normalization helpers (#13803)
The helpers added in
|
||
|---|---|---|
| .. | ||
| 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: A non-empty allowlist of core tools. An omitted or empty list is treated as unspecified; an empty list does not disable tools. MCP and other non-core tools have separate registration and permission rules. UseexcludeToolsto exclude named tools explicitly;allowedToolscontrols approval rather than registration.excludeTools: 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.