Compare commits

..

92 Commits

Author SHA1 Message Date
github-actions[bot]
a142433c7e chore(release): v0.6.1-nightly.20260108.570ec432 2026-01-08 00:13:10 +00:00
tanzhenxin
570ec432af Merge pull request #1282 from BlockHand/fix-sandbox-ideInstall
feat: Optimize the issue where an error message indicating unfriendli…
2026-01-07 17:40:45 +08:00
tanzhenxin
bfc3bbfa9c update user messages 2026-01-07 17:25:27 +08:00
tanzhenxin
91af9bf6c8 Merge branch 'main' into fix-sandbox-ideInstall 2026-01-07 17:12:22 +08:00
tanzhenxin
f6771c0858 Merge pull request #1393 from Weaxs/main
[OpenaiContentGenerate] convertOpenAIResponseToGemini record thoughtsTokenCount
2026-01-07 17:05:28 +08:00
tanzhenxin
2c8be05029 Merge pull request #1415 from QwenLM/fix/openai-reasoning-config
fix(core): don’t force reasoning/topP defaults for OpenAI-compatible APIs
2026-01-07 16:59:26 +08:00
tanzhenxin
4744af1ea8 Merge pull request #1406 from QwenLM/fix/non-interactive-tool-permission
fix(cli,core): honor `tools.core` / `tools.allowed` in non-interactive runs
2026-01-07 16:59:19 +08:00
Mingholy
2c285394c7 Merge pull request #1423 from QwenLM/chore/release-v0.6.1
chore: bump version to 0.6.1
2026-01-07 16:55:45 +08:00
mingholy.lmh
f2d941e469 chore: bump version to 0.6.1 2026-01-07 16:25:14 +08:00
tanzhenxin
9b2dfe1e06 Merge pull request #1374 from QwenLM/fix/resume-command-broken-after-new-chat
Fix resume command broken after new chat
2026-01-07 16:21:47 +08:00
tanzhenxin
3e695cd82b Merge pull request #1146 from QwenLM/fix/windows-background-terminal-execute-x
fix: improve windows background process handling and cleanup
2026-01-07 16:21:14 +08:00
xwj02155382
177a91f1d5 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2026-01-07 15:50:03 +08:00
Weaxs
870d207f18 revert enable_thinking & thinking_budget 2026-01-07 10:25:34 +08:00
tanzhenxin
3f512528cb Merge pull request #1391 from tt-a1i/feat/approval-mode-direct-arg
feat(cli): add direct argument support for /approval-mode command
2026-01-07 09:39:33 +08:00
Tu Shaokun
0878ee4cbd fix: handle setApprovalMode error in untrusted folders
Add try/catch to gracefully handle errors when setting privileged
modes (yolo/auto-edit) in untrusted folders, returning an error
message instead of throwing.
2026-01-06 22:04:43 +08:00
Tu Shaokun
bfe7298858 refactor: apply session-only approval mode per review feedback
- Remove persistence to user settings (no setValue call)
- Only use config.setApprovalMode() for session scope
- Remove autocomplete feature for simplicity
- Align with Shift+Tab behavior
2026-01-06 21:57:21 +08:00
Tu Shaokun
2f2937aafe test: add explicit assertions for setApprovalMode argument
Verify the exact mode value passed to config.setApprovalMode to catch
potential regressions in settings merge/update mechanism.
2026-01-06 21:54:33 +08:00
Tu Shaokun
8fcdd86b91 feat(cli): add direct argument support for /approval-mode command
Allow users to set approval mode directly via argument instead of
opening the dialog. For example:
- /approval-mode plan
- /approval-mode yolo
- /approval-mode auto-edit
- /approval-mode default

If no argument is provided, the dialog opens as before.
If an invalid argument is provided, an error message shows valid options.

Also adds tab completion for mode arguments.

Fixes #1353
2026-01-06 21:54:33 +08:00
tanzhenxin
d7d7bf0c39 fix default values of reasoning config for openai compatible api 2026-01-06 19:39:28 +08:00
gwinthis
b95d9a8d2d Merge pull request #1414 from QwenLM/doc/qwencode-java
Doc/qwencode java
2026-01-06 17:54:47 +08:00
顾盼
6f39ae120c Merge pull request #1355 from QwenLM/feat/stable-acp-flag
feat: graduate `--experimental-acp` to stable `--acp` flag
2026-01-06 17:51:43 +08:00
顾盼
627857621a Merge pull request #1365 from QwenLM/fix/missing-whitespaces
fix: preserve whitespace in thinking content for stream-json output format
2026-01-06 17:51:26 +08:00
顾盼
65c7cf5d8f Merge pull request #1376 from QwenLM/fix/missing-error-throw-nonInteractive
fix: exit with non-zero code on API errors in text mode
2026-01-06 17:51:13 +08:00
顾盼
7a823060ac Merge pull request #1383 from QwenLM/fix/tool-result-text-mode
fix: improve tool execution feedback in non-interactive mode
2026-01-06 17:50:58 +08:00
xwj02155382
2c88ea6dc1 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2026-01-06 17:26:01 +08:00
skyfire
ad3086f7dd add qwencode-sdk java doc 2026-01-06 17:18:41 +08:00
skyfire
8f3bbef575 add qwencode-sdk java doc 2026-01-06 17:11:47 +08:00
xwj02155382
e2d6ab9b7e refactor: simplify background shell command handling
- Remove ineffective error detection for background processes
  (stdio is detached/ignored, so cumulativeOutput is always empty)
- Add kill command hints for both Windows and macOS/Linux
- Simplify code from 40 lines to 12 lines with clearer logic
- Add explanatory comment about why startup errors cannot be reliably detected
2026-01-06 16:46:56 +08:00
Weaxs
35bf5ef4d0 remove duplicate reasoning_content handle 2026-01-06 14:38:01 +08:00
Weaxs
1d16513e27 remove duplicate reasoning_content handle && remove extra_body.enable_thinking 2026-01-06 14:38:01 +08:00
Weaxs
731fd99800 remove duplicate reasoning_content handle && remove extra_body.enable_thinking 2026-01-06 14:21:42 +08:00
skyfire
c6ae0a8be7 for alpha stage 2026-01-06 11:16:47 +08:00
gwinthis
49892a8e17 Merge pull request #1412 from QwenLM/feat/javasdk
Feat/javasdk
2026-01-06 09:49:36 +08:00
skyfire
d1a3e828b7 add license 2026-01-06 09:21:58 +08:00
pomelo
b19bb6cb20 Merge pull request #1378 from afarber/add-german-language-support
feat(i18n): add German language support
2026-01-05 22:14:00 +08:00
skyfire
e8625658ba publish 0.0.1-alpha 2026-01-05 20:27:37 +08:00
tanzhenxin
19f8f631b4 Respect 'tools.core' and 'tools.allowed' settings in non-interactive mode, for both agent execution and custom command 2026-01-05 19:28:52 +08:00
skyfire
a4eb3adea8 for pom 2026-01-05 19:22:50 +08:00
skyfire
7dc7c6380d for pom 2026-01-05 18:14:40 +08:00
skyfire
d2d2b845c5 for README.md 2026-01-05 18:12:48 +08:00
skyfire
96080f84a6 for README.md 2026-01-05 18:00:38 +08:00
skyfire
2b6218e564 for README.md 2026-01-05 17:49:43 +08:00
skyfire
24edf32da8 for README.md 2026-01-05 17:46:18 +08:00
skyfire
51b08f700c for examples 2026-01-05 17:44:07 +08:00
tanzhenxin
58eac7f595 Merge pull request #1397 from liqiongyu/fix/1304-disable-update-nag
fix(cli): skip update check when disableUpdateNag is true
2026-01-05 14:19:13 +08:00
skyfire
32e8b01cf0 for javadoc 2026-01-04 19:39:00 +08:00
skyfire
db9d5cb45d add javadoc 2026-01-04 18:07:56 +08:00
liqoingyu
473cb7b951 fix(cli): skip update check when disableUpdateNag is true 2026-01-04 14:32:38 +08:00
Weaxs
e5cced8813 buildRequest add thinking config && convert Handle reasoning content 2026-01-02 18:59:23 +08:00
skyfire
73848d3867 fix arg 2026-01-01 01:30:58 +08:00
skyfire
6a62167f79 for README.md 2025-12-31 23:36:17 +08:00
skyfire
6ff437671e for README.md 2025-12-31 23:26:20 +08:00
skyfire
30f9e9c782 for README.md 2025-12-31 22:57:20 +08:00
skyfire
e4caa7a856 for partial message processing and event timeout processing 2025-12-31 20:15:51 +08:00
LaZzyMan
aaa66b3172 fix: add tool result and deny warning in text mode 2025-12-31 17:38:33 +08:00
Alexander Farber
0ae59b900c Add German umlauts 2025-12-30 16:50:23 +01:00
Alexander Farber
5a5dae1987 Add German language support and remove a misleading witty phrase 2025-12-30 16:35:34 +01:00
skyfire
ac7ba95d65 add permission 2025-12-30 20:08:05 +08:00
LaZzyMan
15912892f2 fix: missing error throw in non-Interactive mode 2025-12-30 19:40:24 +08:00
cris
e3c20b03bd reslove blank 2025-12-30 16:03:11 +08:00
cris
4db50d4158 fix resume unwork on windows 2025-12-30 16:00:55 +08:00
skyfire
4154493640 message and session use 2025-12-29 21:44:02 +08:00
Mingholy
105ad743fa Merge pull request #1284 from tt-a1i/fix/boolean-string-coercion
fix(core): coerce string boolean values in schema validation
2025-12-29 18:27:36 +08:00
mingholy.lmh
ac3f7cb8c8 fix: ts erros in test file 2025-12-29 17:20:25 +08:00
xuewenjie
98c043bf50 test: update tests for detached process changes 2025-12-29 11:37:54 +08:00
cris
f610133660 improve ad hoc method for windows background terminal task 2025-12-28 22:14:16 +08:00
LaZzyMan
fe7ff5b148 feat: stable-acp-flag 2025-12-26 17:09:16 +08:00
xuewenjie
5417de4219 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2025-12-25 16:44:30 +08:00
skyfire
422998d7f0 add ProcessTransport unitTest and fix bug 2025-12-24 21:20:47 +08:00
skyfire
68628bf952 add ProcessTransport 2025-12-24 20:45:17 +08:00
skyfire
e5efad89e0 Merge branch 'feat/javasdk' of github.com:QwenLM/qwen-code into feat/javasdk 2025-12-24 10:01:28 +08:00
skyfire
e09bb5f5c0 modify junit version to 5 and add org developers 2025-12-23 20:14:11 +08:00
乾离
24d11179d8 modify junit version to 5 and add org developers 2025-12-23 20:04:58 +08:00
乾离
2ef8b6f350 ProcessTransport stru init 2025-12-23 17:44:28 +08:00
乾离
5779f7ab1d project initialize 2025-12-23 17:20:12 +08:00
刘伟光
43e0815def feat: 修改链接ide之前的判断逻辑,检测是否安装过ide扩展 2025-12-22 11:22:51 +08:00
刘伟光
0c14f4ce08 Merge branch 'main' into fix-sandbox-ideInstall 2025-12-22 11:00:01 +08:00
刘伟光
34d8dbf9b2 feat: 兼容宿主机在不同ide上的instal提示 2025-12-19 11:07:33 +08:00
刘伟光
b3b2bc6ad5 feat: 兼容宿主机在不同ide上的instal提示 2025-12-19 10:39:05 +08:00
Tu Shaokun
7b01b26ff5 perf(core): avoid recompiling schema on retry 2025-12-17 16:27:42 +08:00
Tu Shaokun
0f3e97ea1c fix(core): coerce string boolean values in schema validation
Self-hosted LLMs sometimes return "true"/"false" strings instead of
actual boolean values for tool parameters like `is_background`. This
causes schema validation to fail with type errors.

Fixes #1267
2025-12-17 16:14:02 +08:00
刘伟光
6ca54beba2 feat: Optimize the issue where an error message indicating unfriendliness occurs after executing the ideinstall command in the sandbox environment 2025-12-17 13:38:38 +08:00
xuewenjie
8673426d5c fix(core): use current chunk for shell output update instead of cumulative 2025-12-16 10:26:20 +08:00
xuewenjie
b272ac0119 Fix: Make cleanup strategy dynamic to support testing mocks 2025-12-12 17:47:03 +08:00
xuewenjie
574d89da14 Refactor ShellExecutionService cleanup to use strategy pattern 2025-12-12 17:03:04 +08:00
xuewenjie
16939c0bc8 Refactor ShellTool: remove ping hack and timeout, optimize cleanup 2025-12-10 13:49:51 +08:00
xuewenjie
6fc09a82fb fix: use && for windows background keep-alive ping and add test 2025-12-09 13:33:42 +08:00
xuewenjie
d622f8d1bf Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2025-12-09 11:32:17 +08:00
xuewenjie
28d178b5c1 fix: handle windows background execution errors and add tests 2025-12-09 11:24:30 +08:00
xuewenjie
4c69d536ac test: fix shell tool tests by updating pid expectation and AbortSignal matching 2025-12-05 10:47:06 +08:00
xuewenjie
403fd06117 chore: update .gitignore 2025-12-04 15:55:17 +08:00
xuewenjie
d9928eab66 fix: improve windows background process handling and cleanup 2025-12-04 15:55:11 +08:00
119 changed files with 11949 additions and 134 deletions

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ package-lock.json
.idea
*.iml
.cursor
.qoder
# OS metadata
.DS_Store

View File

@@ -11,6 +11,7 @@ export default {
type: 'separator',
},
'sdk-typescript': 'Typescript SDK',
'sdk-java': 'Java SDK(alpha)',
'Dive Into Qwen Code': {
title: 'Dive Into Qwen Code',
type: 'separator',

312
docs/developers/sdk-java.md Normal file
View File

@@ -0,0 +1,312 @@
# Qwen Code Java SDK
The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
## Requirements
- Java >= 1.8
- Maven >= 3.6.0 (for building from source)
- qwen-code >= 0.5.0
### Dependencies
- **Logging**: ch.qos.logback:logback-classic
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: com.alibaba.fastjson2:fastjson2
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)
## Installation
Add the following dependency to your Maven `pom.xml`:
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<version>{$version}</version>
</dependency>
```
Or if using Gradle, add to your `build.gradle`:
```gradle
implementation 'com.alibaba:qwencode-sdk:{$version}'
```
## Building and Running
### Build Commands
```bash
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
```
## Quick Start
The simplest way to use the SDK is through the `QwenCodeCli.simpleQuery()` method:
```java
public static void runSimpleExample() {
List<String> result = QwenCodeCli.simpleQuery("hello world");
result.forEach(logger::info);
}
```
For more advanced usage with custom transport options:
```java
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:
```java
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, ThingkingAssistantContent thingkingAssistantContent) {
logger.info("Thinking content received: {}", thingkingAssistantContent.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
## Architecture
The SDK follows a layered architecture:
- **API Layer**: Provides the main entry points through `QwenCodeCli` class with simple static methods for basic usage
- **Session Layer**: Manages communication sessions with the Qwen Code CLI through the `Session` class
- **Transport Layer**: Handles the communication mechanism between the SDK and CLI process (currently using process transport via `ProcessTransport`)
- **Protocol Layer**: Defines data structures for communication based on the CLI protocol
- **Utils**: Common utilities for concurrent execution, timeout handling, and error management
## Key Features
### Permission Modes
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) while other tools require confirmation.
- **`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<CLIControlPermissionRequest>, 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 ThingkingAssistantContent)
- `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:**
- `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`, `onUsage`
- `onPartialAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest``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``onSystemMessageTimeout`
- `onResultMessage``onResultMessageTimeout`
- `onAssistantMessage``onAssistantMessageTimeout`
- `onPartialAssistantMessage``onPartialAssistantMessageTimeout`
- `onUserMessage``onUserMessageTimeout`
- `onOtherMessage``onOtherMessageTimeout`
- `onControlResponse``onControlResponseTimeout`
- `onControlRequest``onControlRequestTimeout`
For AssistantContentConsumers timeout methods:
- `onText``onTextTimeout`
- `onThinking``onThinkingTimeout`
- `onToolUse``onToolUseTimeout`
- `onToolResult``onToolResultTimeout`
- `onOtherContent``onOtherContentTimeout`
- `onPermissionRequest``onPermissionRequestTimeout`
- `onOtherControlRequest``onOtherControlRequestTimeout`
**Default 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 values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return 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 executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: List of core tools that should be available to the AI
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `skillsEnable`: Enables or disables skills functionality for the session
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: 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 `Session` class 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()` in `TransportOptions` to 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**: false
- **Rejected Execution Handler**: CallerRunsPolicy
## Error Handling
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 response
- `SessionClosedException`: Thrown when attempting to use a closed session
## FAQ / Troubleshooting
### Q: Do I need to install the Qwen CLI separately?
A: yes, requires Qwen CLI 0.5.5 or higher.
### Q: What Java versions are supported?
A: The SDK requires Java 1.8 or higher.
### 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](./LICENSE) for details.

View File

@@ -381,7 +381,7 @@ Arguments passed directly when running the CLI can override other configurations
| `--telemetry-otlp-protocol` | | Sets the OTLP protocol for telemetry (`grpc` or `http`). | | Defaults to `grpc`. See [telemetry](../../developers/development/telemetry) for more information. |
| `--telemetry-log-prompts` | | Enables logging of prompts for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
| `--checkpointing` | | Enables [checkpointing](../features/checkpointing). | | |
| `--experimental-acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Experimental. |
| `--acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Stable. Replaces the deprecated `--experimental-acp` flag. |
| `--experimental-skills` | | Enables experimental [Agent Skills](../features/skills) (registers the `skill` tool and loads Skills from `.qwen/skills/` and `~/.qwen/skills/`). | | Experimental. |
| `--extensions` | `-e` | Specifies a list of extensions to use for the session. | Extension names | If not provided, all available extensions are used. Use the special term `qwen -e none` to disable all extensions. Example: `qwen -e my-extension -e my-other-extension` |
| `--list-extensions` | `-l` | Lists all available extensions and exits. | | |

View File

@@ -32,7 +32,7 @@
"Qwen Code": {
"type": "custom",
"command": "qwen",
"args": ["--experimental-acp"],
"args": ["--acp"],
"env": {}
}
```

View File

@@ -80,10 +80,11 @@ type PermissionHandler = (
/**
* Sets up an ACP test environment with all necessary utilities.
* @param useNewFlag - If true, uses --acp; if false, uses --experimental-acp (for backward compatibility testing)
*/
function setupAcpTest(
rig: TestRig,
options?: { permissionHandler?: PermissionHandler },
options?: { permissionHandler?: PermissionHandler; useNewFlag?: boolean },
) {
const pending = new Map<number, PendingRequest>();
let nextRequestId = 1;
@@ -95,9 +96,13 @@ function setupAcpTest(
const permissionHandler =
options?.permissionHandler ?? (() => ({ optionId: 'proceed_once' }));
// Use --acp by default, but allow testing with --experimental-acp for backward compatibility
const acpFlag =
options?.useNewFlag !== false ? '--acp' : '--experimental-acp';
const agent = spawn(
'node',
[rig.bundlePath, '--experimental-acp', '--no-chat-recording'],
[rig.bundlePath, acpFlag, '--no-chat-recording'],
{
cwd: rig.testDir!,
stdio: ['pipe', 'pipe', 'pipe'],
@@ -621,3 +626,99 @@ function setupAcpTest(
}
});
});
(IS_SANDBOX ? describe.skip : describe)(
'acp flag backward compatibility',
() => {
it('should work with deprecated --experimental-acp flag and show warning', async () => {
const rig = new TestRig();
rig.setup('acp backward compatibility');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig, {
useNewFlag: false,
});
try {
const initResult = await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
expect(initResult).toBeDefined();
// Verify deprecation warning is shown
const stderrOutput = stderr.join('');
expect(stderrOutput).toContain('--experimental-acp is deprecated');
expect(stderrOutput).toContain('Please use --acp instead');
await sendRequest('authenticate', { methodId: 'openai' });
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Verify functionality still works
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [{ type: 'text', text: 'Say hello.' }],
});
expect(promptResult).toBeDefined();
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
it('should work with new --acp flag without warnings', async () => {
const rig = new TestRig();
rig.setup('acp new flag');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig, {
useNewFlag: true,
});
try {
const initResult = await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
expect(initResult).toBeDefined();
// Verify no deprecation warning is shown
const stderrOutput = stderr.join('');
expect(stderrOutput).not.toContain('--experimental-acp is deprecated');
await sendRequest('authenticate', { methodId: 'openai' });
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Verify functionality works
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [{ type: 'text', text: 'Say hello.' }],
});
expect(promptResult).toBeDefined();
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
},
);

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"workspaces": [
"packages/*"
],
@@ -17316,7 +17316,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"dependencies": {
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -17953,7 +17953,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"hasInstallScript": true,
"dependencies": {
"@anthropic-ai/sdk": "^0.36.1",
@@ -21413,7 +21413,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -21425,7 +21425,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.1-nightly.20260108.570ec432"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.1-nightly.20260108.570ec432"
},
"dependencies": {
"@google/genai": "1.30.0",

View File

@@ -1597,6 +1597,58 @@ describe('Approval mode tool exclusion logic', () => {
expect(excludedTools).toContain(WriteFileTool.Name);
});
it('should not exclude a tool explicitly allowed in tools.allowed', async () => {
process.argv = ['node', 'script.js', '-p', 'test'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
tools: {
allowed: [ShellTool.Name],
},
};
const extensions: Extension[] = [];
const config = await loadCliConfig(
settings,
extensions,
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
argv,
);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(ShellTool.Name);
expect(excludedTools).toContain(EditTool.Name);
expect(excludedTools).toContain(WriteFileTool.Name);
});
it('should not exclude a tool explicitly allowed in tools.core', async () => {
process.argv = ['node', 'script.js', '-p', 'test'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
tools: {
core: [ShellTool.Name],
},
};
const extensions: Extension[] = [];
const config = await loadCliConfig(
settings,
extensions,
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
argv,
);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(ShellTool.Name);
expect(excludedTools).toContain(EditTool.Name);
expect(excludedTools).toContain(WriteFileTool.Name);
});
it('should exclude only shell tools in non-interactive mode with auto-edit approval mode', async () => {
process.argv = [
'node',

View File

@@ -10,22 +10,24 @@ import {
Config,
DEFAULT_QWEN_EMBEDDING_MODEL,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
EditTool,
FileDiscoveryService,
getCurrentGeminiMdFilename,
loadServerHierarchicalMemory,
setGeminiMdFilename as setServerGeminiMdFilename,
ShellTool,
WriteFileTool,
resolveTelemetrySettings,
FatalConfigError,
Storage,
InputFormat,
OutputFormat,
isToolEnabled,
SessionService,
type ResumedSessionData,
type FileFilteringOptions,
type MCPServerConfig,
type ToolName,
EditTool,
ShellTool,
WriteFileTool,
} from '@qwen-code/qwen-code-core';
import { extensionsCommand } from '../commands/extensions.js';
import type { Settings } from './settings.js';
@@ -111,6 +113,7 @@ export interface CliArgs {
telemetryOutfile: string | undefined;
allowedMcpServerNames: string[] | undefined;
allowedTools: string[] | undefined;
acp: boolean | undefined;
experimentalAcp: boolean | undefined;
experimentalSkills: boolean | undefined;
extensions: string[] | undefined;
@@ -304,10 +307,16 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
description: 'Enables checkpointing of file edits',
default: false,
})
.option('experimental-acp', {
.option('acp', {
type: 'boolean',
description: 'Starts the agent in ACP mode',
})
.option('experimental-acp', {
type: 'boolean',
description:
'Starts the agent in ACP mode (deprecated, use --acp instead)',
hidden: true,
})
.option('experimental-skills', {
type: 'boolean',
description: 'Enable experimental Skills feature',
@@ -589,8 +598,19 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
// Apply ACP fallback: if experimental-acp is present but no explicit --channel, treat as ACP
if (result['experimentalAcp'] && !result['channel']) {
// Handle deprecated --experimental-acp flag
if (result['experimentalAcp']) {
console.warn(
'\x1b[33m⚠ Warning: --experimental-acp is deprecated and will be removed in a future release. Please use --acp instead.\x1b[0m',
);
// Map experimental-acp to acp if acp is not explicitly set
if (!result['acp']) {
(result as Record<string, unknown>)['acp'] = true;
}
}
// Apply ACP fallback: if acp or experimental-acp is present but no explicit --channel, treat as ACP
if ((result['acp'] || result['experimentalAcp']) && !result['channel']) {
(result as Record<string, unknown>)['channel'] = 'ACP';
}
@@ -818,6 +838,28 @@ export async function loadCliConfig(
// However, if stream-json input is used, control can be requested via JSON messages,
// so tools should not be excluded in that case.
const extraExcludes: string[] = [];
const resolvedCoreTools = argv.coreTools || settings.tools?.core || [];
const resolvedAllowedTools =
argv.allowedTools || settings.tools?.allowed || [];
const isExplicitlyEnabled = (toolName: ToolName): boolean => {
if (resolvedCoreTools.length > 0) {
if (isToolEnabled(toolName, resolvedCoreTools, [])) {
return true;
}
}
if (resolvedAllowedTools.length > 0) {
if (isToolEnabled(toolName, resolvedAllowedTools, [])) {
return true;
}
}
return false;
};
const excludeUnlessExplicit = (toolName: ToolName): void => {
if (!isExplicitlyEnabled(toolName)) {
extraExcludes.push(toolName);
}
};
if (
!interactive &&
!argv.experimentalAcp &&
@@ -826,12 +868,15 @@ export async function loadCliConfig(
switch (approvalMode) {
case ApprovalMode.PLAN:
case ApprovalMode.DEFAULT:
// In default non-interactive mode, all tools that require approval are excluded.
extraExcludes.push(ShellTool.Name, EditTool.Name, WriteFileTool.Name);
// In default non-interactive mode, all tools that require approval are excluded,
// unless explicitly enabled via coreTools/allowedTools.
excludeUnlessExplicit(ShellTool.Name as ToolName);
excludeUnlessExplicit(EditTool.Name as ToolName);
excludeUnlessExplicit(WriteFileTool.Name as ToolName);
break;
case ApprovalMode.AUTO_EDIT:
// In auto-edit non-interactive mode, only tools that still require a prompt are excluded.
extraExcludes.push(ShellTool.Name);
excludeUnlessExplicit(ShellTool.Name as ToolName);
break;
case ApprovalMode.YOLO:
// No extra excludes for YOLO mode.
@@ -981,7 +1026,7 @@ export async function loadCliConfig(
sessionTokenLimit: settings.model?.sessionTokenLimit ?? -1,
maxSessionTurns:
argv.maxSessionTurns ?? settings.model?.maxSessionTurns ?? -1,
experimentalZedIntegration: argv.experimentalAcp || false,
experimentalZedIntegration: argv.acp || argv.experimentalAcp || false,
experimentalSkills: argv.experimentalSkills || false,
listExtensions: argv.listExtensions || false,
extensions: allExtensions,

View File

@@ -202,6 +202,7 @@ const SETTINGS_SCHEMA = {
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文 (Chinese)' },
{ value: 'ru', label: 'Русский (Russian)' },
{ value: 'de', label: 'Deutsch (German)' },
],
},
terminalBell: {

View File

@@ -460,6 +460,7 @@ describe('gemini.tsx main function kitty protocol', () => {
telemetryOutfile: undefined,
allowedMcpServerNames: undefined,
allowedTools: undefined,
acp: undefined,
experimentalAcp: undefined,
experimentalSkills: undefined,
extensions: undefined,
@@ -639,4 +640,37 @@ describe('startInteractiveUI', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
expect(checkForUpdates).toHaveBeenCalledTimes(1);
});
it('should not check for updates when update nag is disabled', async () => {
const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
const settingsWithUpdateNagDisabled = {
merged: {
general: {
disableUpdateNag: true,
},
ui: {
hideWindowTitle: false,
},
},
} as LoadedSettings;
await startInteractiveUI(
mockConfig,
settingsWithUpdateNagDisabled,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(checkForUpdates).not.toHaveBeenCalled();
});
});

View File

@@ -183,16 +183,18 @@ export async function startInteractiveUI(
},
);
checkForUpdates()
.then((info) => {
handleAutoUpdate(info, settings, config.getProjectRoot());
})
.catch((err) => {
// Silently ignore update check errors.
if (config.getDebugMode()) {
console.error('Update check failed:', err);
}
});
if (!settings.merged.general?.disableUpdateNag) {
checkForUpdates()
.then((info) => {
handleAutoUpdate(info, settings, config.getProjectRoot());
})
.catch((err) => {
// Silently ignore update check errors.
if (config.getDebugMode()) {
console.error('Update check failed:', err);
}
});
}
registerCleanup(() => instance.unmount());
}

File diff suppressed because it is too large Load Diff

View File

@@ -89,6 +89,9 @@ export default {
'No tools available': 'No tools available',
'View or change the approval mode for tool usage':
'View or change the approval mode for tool usage',
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}':
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}',
'Approval mode set to "{{mode}}"': 'Approval mode set to "{{mode}}"',
'View or change the language setting': 'View or change the language setting',
'change the theme': 'change the theme',
'Select Theme': 'Select Theme',
@@ -1037,7 +1040,6 @@ export default {
'Applying percussive maintenance...',
'Searching for the correct USB orientation...',
'Ensuring the magic smoke stays inside the wires...',
'Rewriting in Rust for no particular reason...',
'Trying to exit Vim...',
'Spinning up the hamster wheel...',
"That's not a bug, it's an undocumented feature...",

View File

@@ -89,6 +89,10 @@ export default {
'No tools available': 'Нет доступных инструментов',
'View or change the approval mode for tool usage':
'Просмотр или изменение режима подтверждения для использования инструментов',
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}':
'Недопустимый режим подтверждения "{{arg}}". Допустимые режимы: {{modes}}',
'Approval mode set to "{{mode}}"':
'Режим подтверждения установлен на "{{mode}}"',
'View or change the language setting':
'Просмотр или изменение настроек языка',
'change the theme': 'Изменение темы',
@@ -1056,7 +1060,6 @@ export default {
'Провожу настройку методом тыка...',
'Ищем, какой стороной вставлять флешку...',
'Следим, чтобы волшебный дым не вышел из проводов...',
'Переписываем всё на Rust без особой причины...',
'Пытаемся выйти из Vim...',
'Раскручиваем колесо для хомяка...',
'Это не баг, а фича...',

View File

@@ -88,6 +88,9 @@ export default {
'No tools available': '没有可用工具',
'View or change the approval mode for tool usage':
'查看或更改工具使用的审批模式',
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}':
'无效的审批模式 "{{arg}}"。有效模式:{{modes}}',
'Approval mode set to "{{mode}}"': '审批模式已设置为 "{{mode}}"',
'View or change the language setting': '查看或更改语言设置',
'change the theme': '更改主题',
'Select Theme': '选择主题',

View File

@@ -298,7 +298,9 @@ describe('runNonInteractive', () => {
mockConfig,
expect.objectContaining({ name: 'testTool' }),
expect.any(AbortSignal),
undefined,
expect.objectContaining({
outputUpdateHandler: expect.any(Function),
}),
);
// Verify first call has isContinuation: false
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
@@ -771,6 +773,52 @@ describe('runNonInteractive', () => {
);
});
it('should handle API errors in text mode and exit with error code', async () => {
(mockConfig.getOutputFormat as Mock).mockReturnValue(OutputFormat.TEXT);
setupMetricsMock();
// Simulate an API error event (like 401 unauthorized)
const apiErrorEvent: ServerGeminiStreamEvent = {
type: GeminiEventType.Error,
value: {
error: {
message: '401 Incorrect API key provided',
status: 401,
},
},
};
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents([apiErrorEvent]),
);
let thrownError: Error | null = null;
try {
await runNonInteractive(
mockConfig,
mockSettings,
'Test input',
'prompt-id-api-error',
);
// Should not reach here
expect.fail('Expected error to be thrown');
} catch (error) {
thrownError = error as Error;
}
// Should throw with the API error message
expect(thrownError).toBeTruthy();
expect(thrownError?.message).toContain('401');
expect(thrownError?.message).toContain('Incorrect API key provided');
// Verify error was written to stderr
expect(processStderrSpy).toHaveBeenCalled();
const stderrCalls = processStderrSpy.mock.calls;
const errorOutput = stderrCalls.map((call) => call[0]).join('');
expect(errorOutput).toContain('401');
expect(errorOutput).toContain('Incorrect API key provided');
});
it('should handle FatalInputError with custom exit code in JSON format', async () => {
(mockConfig.getOutputFormat as Mock).mockReturnValue(OutputFormat.JSON);
setupMetricsMock();
@@ -1777,4 +1825,84 @@ describe('runNonInteractive', () => {
{ isContinuation: false },
);
});
it('should print tool output to console in text mode (non-Task tools)', async () => {
// Test that tool output is printed to stdout in text mode
const toolCallEvent: ServerGeminiStreamEvent = {
type: GeminiEventType.ToolCallRequest,
value: {
callId: 'tool-1',
name: 'run_in_terminal',
args: { command: 'npm outdated' },
isClientInitiated: false,
prompt_id: 'prompt-id-tool-output',
},
};
// Mock tool execution with outputUpdateHandler being called
mockCoreExecuteToolCall.mockImplementation(
async (_config, _request, _signal, options) => {
// Simulate tool calling outputUpdateHandler with output chunks
if (options?.outputUpdateHandler) {
options.outputUpdateHandler('tool-1', 'Package outdated\n');
options.outputUpdateHandler('tool-1', 'npm@1.0.0 -> npm@2.0.0\n');
}
return {
responseParts: [
{
functionResponse: {
id: 'tool-1',
name: 'run_in_terminal',
response: {
output: 'Package outdated\nnpm@1.0.0 -> npm@2.0.0',
},
},
},
],
};
},
);
const firstCallEvents: ServerGeminiStreamEvent[] = [
toolCallEvent,
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
},
];
const secondCallEvents: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Dependencies checked' },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 3 } },
},
];
mockGeminiClient.sendMessageStream
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
await runNonInteractive(
mockConfig,
mockSettings,
'Check dependencies',
'prompt-id-tool-output',
);
// Verify that executeToolCall was called with outputUpdateHandler
expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
mockConfig,
expect.objectContaining({ name: 'run_in_terminal' }),
expect.any(AbortSignal),
expect.objectContaining({
outputUpdateHandler: expect.any(Function),
}),
);
// Verify tool output was written to stdout
expect(processStdoutSpy).toHaveBeenCalledWith('Package outdated\n');
expect(processStdoutSpy).toHaveBeenCalledWith('npm@1.0.0 -> npm@2.0.0\n');
expect(processStdoutSpy).toHaveBeenCalledWith('Dependencies checked');
});
});

View File

@@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config, ToolCallRequestInfo } from '@qwen-code/qwen-code-core';
import type {
Config,
ToolCallRequestInfo,
ToolResultDisplay,
} from '@qwen-code/qwen-code-core';
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
@@ -308,6 +312,8 @@ export async function runNonInteractive(
config.getContentGeneratorConfig()?.authType,
);
process.stderr.write(`${errorText}\n`);
// Throw error to exit with non-zero code
throw new Error(errorText);
}
}
}
@@ -333,7 +339,7 @@ export async function runNonInteractive(
? options.controlService.permission.getToolCallUpdateCallback()
: undefined;
// Only pass outputUpdateHandler for Task tool
// Create output handler for Task tool (for subagent execution)
const isTaskTool = finalRequestInfo.name === 'task';
const taskToolProgress = isTaskTool
? createTaskToolProgressHandler(
@@ -343,20 +349,41 @@ export async function runNonInteractive(
)
: undefined;
const taskToolProgressHandler = taskToolProgress?.handler;
// Create output handler for non-Task tools in text mode (for console output)
const nonTaskOutputHandler =
!isTaskTool && !adapter
? (callId: string, outputChunk: ToolResultDisplay) => {
// Print tool output to console in text mode
if (typeof outputChunk === 'string') {
process.stdout.write(outputChunk);
} else if (
outputChunk &&
typeof outputChunk === 'object' &&
'ansiOutput' in outputChunk
) {
// Handle ANSI output - just print as string for now
process.stdout.write(String(outputChunk.ansiOutput));
}
}
: undefined;
// Combine output handlers
const outputUpdateHandler =
taskToolProgressHandler || nonTaskOutputHandler;
const toolResponse = await executeToolCall(
config,
finalRequestInfo,
abortController.signal,
isTaskTool && taskToolProgressHandler
outputUpdateHandler || toolCallUpdateCallback
? {
outputUpdateHandler: taskToolProgressHandler,
onToolCallsUpdate: toolCallUpdateCallback,
}
: toolCallUpdateCallback
? {
...(outputUpdateHandler && { outputUpdateHandler }),
...(toolCallUpdateCallback && {
onToolCallsUpdate: toolCallUpdateCallback,
}
: undefined,
}),
}
: undefined,
);
// Note: In JSON mode, subagent messages are automatically added to the main

View File

@@ -72,6 +72,7 @@ describe('ShellProcessor', () => {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getShouldUseNodePtyShell: vi.fn().mockReturnValue(false),
getShellExecutionConfig: vi.fn().mockReturnValue({}),
getAllowedTools: vi.fn().mockReturnValue([]),
};
context = createMockCommandContext({
@@ -196,6 +197,35 @@ describe('ShellProcessor', () => {
);
});
it('should NOT throw ConfirmationRequiredError when a command matches allowedTools', async () => {
const processor = new ShellProcessor('test-command');
const prompt: PromptPipelineContent = createPromptPipelineContent(
'Do something dangerous: !{rm -rf /}',
);
mockCheckCommandPermissions.mockReturnValue({
allAllowed: false,
disallowedCommands: ['rm -rf /'],
});
(mockConfig.getAllowedTools as Mock).mockReturnValue([
'ShellTool(rm -rf /)',
]);
mockShellExecute.mockReturnValue({
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }),
});
const result = await processor.process(prompt, context);
expect(mockShellExecute).toHaveBeenCalledWith(
'rm -rf /',
expect.any(String),
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);
expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]);
});
it('should NOT throw ConfirmationRequiredError if a command is not allowed but approval mode is YOLO', async () => {
const processor = new ShellProcessor('test-command');
const prompt: PromptPipelineContent = createPromptPipelineContent(

View File

@@ -7,11 +7,13 @@
import {
ApprovalMode,
checkCommandPermissions,
doesToolInvocationMatch,
escapeShellArg,
getShellConfiguration,
ShellExecutionService,
flatMapTextParts,
} from '@qwen-code/qwen-code-core';
import type { AnyToolInvocation } from '@qwen-code/qwen-code-core';
import type { CommandContext } from '../../ui/commands/types.js';
import type { IPromptProcessor, PromptPipelineContent } from './types.js';
@@ -124,6 +126,15 @@ export class ShellProcessor implements IPromptProcessor {
// Security check on the final, escaped command string.
const { allAllowed, disallowedCommands, blockReason, isHardDenial } =
checkCommandPermissions(command, config, sessionShellAllowlist);
const allowedTools = config.getAllowedTools() || [];
const invocation = {
params: { command },
} as AnyToolInvocation;
const isAllowedBySettings = doesToolInvocationMatch(
'run_shell_command',
invocation,
allowedTools,
);
if (!allAllowed) {
if (isHardDenial) {
@@ -132,10 +143,17 @@ export class ShellProcessor implements IPromptProcessor {
);
}
// If not a hard denial, respect YOLO mode and auto-approve.
if (config.getApprovalMode() !== ApprovalMode.YOLO) {
disallowedCommands.forEach((uc) => commandsToConfirm.add(uc));
// If the command is allowed by settings, skip confirmation.
if (isAllowedBySettings) {
continue;
}
// If not a hard denial, respect YOLO mode and auto-approve.
if (config.getApprovalMode() === ApprovalMode.YOLO) {
continue;
}
disallowedCommands.forEach((uc) => commandsToConfirm.add(uc));
}
}

View File

@@ -925,7 +925,12 @@ export const AppContainer = (props: AppContainerProps) => {
const handleIdePromptComplete = useCallback(
(result: IdeIntegrationNudgeResult) => {
if (result.userSelection === 'yes') {
handleSlashCommand('/ide install');
// Check whether the extension has been pre-installed
if (result.isExtensionPreInstalled) {
handleSlashCommand('/ide enable');
} else {
handleSlashCommand('/ide install');
}
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
} else if (result.userSelection === 'dismiss') {
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);

View File

@@ -38,6 +38,7 @@ export function IdeIntegrationNudge({
);
const { displayName: ideName } = ide;
const isInSandbox = !!process.env['SANDBOX'];
// Assume extension is already installed if the env variables are set.
const isExtensionPreInstalled =
!!process.env['QWEN_CODE_IDE_SERVER_PORT'] &&
@@ -70,13 +71,15 @@ export function IdeIntegrationNudge({
},
];
const installText = isExtensionPreInstalled
? `If you select Yes, the CLI will have access to your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`;
const installText = isInSandbox
? `Note: In sandbox environments, IDE integration requires manual setup on the host system. If you select Yes, you'll receive instructions on how to set this up.`
: isExtensionPreInstalled
? `If you select Yes, the CLI will connect to your ${
ideName ?? 'editor'
} and have access to your open files and display diffs directly.`
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`;
return (
<Box

View File

@@ -4,31 +4,28 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { approvalModeCommand } from './approvalModeCommand.js';
import {
type CommandContext,
CommandKind,
type OpenDialogActionReturn,
type MessageActionReturn,
} from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import type { LoadedSettings } from '../../config/settings.js';
describe('approvalModeCommand', () => {
let mockContext: CommandContext;
let mockSetApprovalMode: ReturnType<typeof vi.fn>;
beforeEach(() => {
mockSetApprovalMode = vi.fn();
mockContext = createMockCommandContext({
services: {
config: {
getApprovalMode: () => 'default',
setApprovalMode: () => {},
setApprovalMode: mockSetApprovalMode,
},
settings: {
merged: {},
setValue: () => {},
forScope: () => ({}),
} as unknown as LoadedSettings,
},
});
});
@@ -41,7 +38,7 @@ describe('approvalModeCommand', () => {
expect(approvalModeCommand.kind).toBe(CommandKind.BUILT_IN);
});
it('should open approval mode dialog when invoked', async () => {
it('should open approval mode dialog when invoked without arguments', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'',
@@ -51,16 +48,123 @@ describe('approvalModeCommand', () => {
expect(result.dialog).toBe('approval-mode');
});
it('should open approval mode dialog with arguments (ignored)', async () => {
it('should open approval mode dialog when invoked with whitespace only', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'some arguments',
' ',
)) as OpenDialogActionReturn;
expect(result.type).toBe('dialog');
expect(result.dialog).toBe('approval-mode');
});
describe('direct mode setting (session-only)', () => {
it('should set approval mode to "plan" when argument is "plan"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'plan',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('plan');
expect(mockSetApprovalMode).toHaveBeenCalledWith('plan');
});
it('should set approval mode to "yolo" when argument is "yolo"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'yolo',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('yolo');
expect(mockSetApprovalMode).toHaveBeenCalledWith('yolo');
});
it('should set approval mode to "auto-edit" when argument is "auto-edit"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'auto-edit',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('auto-edit');
expect(mockSetApprovalMode).toHaveBeenCalledWith('auto-edit');
});
it('should set approval mode to "default" when argument is "default"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'default',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('default');
expect(mockSetApprovalMode).toHaveBeenCalledWith('default');
});
it('should be case-insensitive for mode argument', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'YOLO',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(mockSetApprovalMode).toHaveBeenCalledWith('yolo');
});
it('should handle argument with leading/trailing whitespace', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
' plan ',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(mockSetApprovalMode).toHaveBeenCalledWith('plan');
});
});
describe('invalid mode argument', () => {
it('should return error for invalid mode', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'invalid-mode',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('error');
expect(result.content).toContain('invalid-mode');
expect(result.content).toContain('plan');
expect(result.content).toContain('yolo');
expect(mockSetApprovalMode).not.toHaveBeenCalled();
});
});
describe('untrusted folder handling', () => {
it('should return error when setApprovalMode throws (e.g., untrusted folder)', async () => {
const errorMessage =
'Cannot enable privileged approval modes in an untrusted folder.';
mockSetApprovalMode.mockImplementation(() => {
throw new Error(errorMessage);
});
const result = (await approvalModeCommand.action?.(
mockContext,
'yolo',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('error');
expect(result.content).toBe(errorMessage);
});
});
it('should not have subcommands', () => {
expect(approvalModeCommand.subCommands).toBeUndefined();
});

View File

@@ -8,9 +8,25 @@ import type {
SlashCommand,
CommandContext,
OpenDialogActionReturn,
MessageActionReturn,
} from './types.js';
import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
import type { ApprovalMode } from '@qwen-code/qwen-code-core';
import { APPROVAL_MODES } from '@qwen-code/qwen-code-core';
/**
* Parses the argument string and returns the corresponding ApprovalMode if valid.
* Returns undefined if the argument is empty or not a valid mode.
*/
function parseApprovalModeArg(arg: string): ApprovalMode | undefined {
const trimmed = arg.trim().toLowerCase();
if (!trimmed) {
return undefined;
}
// Match against valid approval modes (case-insensitive)
return APPROVAL_MODES.find((mode) => mode.toLowerCase() === trimmed);
}
export const approvalModeCommand: SlashCommand = {
name: 'approval-mode',
@@ -19,10 +35,49 @@ export const approvalModeCommand: SlashCommand = {
},
kind: CommandKind.BUILT_IN,
action: async (
_context: CommandContext,
_args: string,
): Promise<OpenDialogActionReturn> => ({
type: 'dialog',
dialog: 'approval-mode',
}),
context: CommandContext,
args: string,
): Promise<OpenDialogActionReturn | MessageActionReturn> => {
const mode = parseApprovalModeArg(args);
// If no argument provided, open the dialog
if (!args.trim()) {
return {
type: 'dialog',
dialog: 'approval-mode',
};
}
// If invalid argument, return error message with valid options
if (!mode) {
return {
type: 'message',
messageType: 'error',
content: t('Invalid approval mode "{{arg}}". Valid modes: {{modes}}', {
arg: args.trim(),
modes: APPROVAL_MODES.join(', '),
}),
};
}
// Set the mode for current session only (not persisted)
const { config } = context.services;
if (config) {
try {
config.setApprovalMode(mode);
} catch (e) {
return {
type: 'message',
messageType: 'error',
content: (e as Error).message,
};
}
}
return {
type: 'message',
messageType: 'info',
content: t('Approval mode set to "{{mode}}"', { mode }),
};
},
};

View File

@@ -191,11 +191,23 @@ export const ideCommand = async (): Promise<SlashCommand> => {
kind: CommandKind.BUILT_IN,
action: async (context) => {
const installer = getIdeInstaller(currentIDE);
const isSandBox = !!process.env['SANDBOX'];
if (isSandBox) {
context.ui.addItem(
{
type: 'info',
text: `IDE integration needs to be installed on the host. If you have already installed it, you can directly connect the ide`,
},
Date.now(),
);
return;
}
if (!installer) {
const ideName = ideClient.getDetectedIdeDisplayName();
context.ui.addItem(
{
type: 'error',
text: `No installer is available for ${ideClient.getDetectedIdeDisplayName()}. Please install the '${QWEN_CODE_COMPANION_EXTENSION_NAME}' extension manually from the marketplace.`,
text: `Automatic installation is not supported for ${ideName}. Please install the '${QWEN_CODE_COMPANION_EXTENSION_NAME}' extension manually from the marketplace.`,
},
Date.now(),
);

View File

@@ -87,7 +87,13 @@ export async function showResumeSessionPicker(
let selectedId: string | undefined;
const { unmount, waitUntilExit } = render(
<KeypressProvider kittyProtocolEnabled={false}>
<KeypressProvider
kittyProtocolEnabled={false}
pasteWorkaround={
process.platform === 'win32' ||
parseInt(process.versions.node.split('.')[0], 10) < 20
}
>
<StandalonePickerScreen
sessionService={sessionService}
onSelect={(id) => {

View File

@@ -6,7 +6,11 @@
import { vi, type Mock, type MockInstance } from 'vitest';
import type { Config } from '@qwen-code/qwen-code-core';
import { OutputFormat, FatalInputError } from '@qwen-code/qwen-code-core';
import {
OutputFormat,
FatalInputError,
ToolErrorType,
} from '@qwen-code/qwen-code-core';
import {
getErrorMessage,
handleError,
@@ -65,6 +69,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
describe('errors', () => {
let mockConfig: Config;
let processExitSpy: MockInstance;
let processStderrWriteSpy: MockInstance;
let consoleErrorSpy: MockInstance;
beforeEach(() => {
@@ -74,6 +79,11 @@ describe('errors', () => {
// Mock console.error
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// Mock process.stderr.write
processStderrWriteSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
// Mock process.exit to throw instead of actually exiting
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit called with code: ${code}`);
@@ -84,11 +94,13 @@ describe('errors', () => {
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'test' }),
getDebugMode: vi.fn().mockReturnValue(true),
isInteractive: vi.fn().mockReturnValue(false),
} as unknown as Config;
});
afterEach(() => {
consoleErrorSpy.mockRestore();
processStderrWriteSpy.mockRestore();
processExitSpy.mockRestore();
});
@@ -432,6 +444,87 @@ describe('errors', () => {
expect(processExitSpy).not.toHaveBeenCalled();
});
});
describe('permission denied warnings', () => {
it('should show warning when EXECUTION_DENIED in non-interactive text mode', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(false);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.TEXT);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.EXECUTION_DENIED,
);
expect(processStderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Warning: Tool "test-tool" requires user approval',
),
);
expect(processStderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('use the -y flag (YOLO mode)'),
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not show warning when EXECUTION_DENIED in interactive mode', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(true);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.TEXT);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.EXECUTION_DENIED,
);
expect(processStderrWriteSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not show warning when EXECUTION_DENIED in JSON mode', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(false);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.JSON);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.EXECUTION_DENIED,
);
expect(processStderrWriteSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not show warning for non-EXECUTION_DENIED errors', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(false);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.TEXT);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.FILE_NOT_FOUND,
);
expect(processStderrWriteSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
});
});
describe('handleCancellationError', () => {

View File

@@ -11,6 +11,7 @@ import {
parseAndFormatApiError,
FatalTurnLimitedError,
FatalCancellationError,
ToolErrorType,
} from '@qwen-code/qwen-code-core';
export function getErrorMessage(error: unknown): string {
@@ -102,10 +103,24 @@ export function handleToolError(
toolName: string,
toolError: Error,
config: Config,
_errorCode?: string | number,
errorCode?: string | number,
resultDisplay?: string,
): void {
// Always just log to stderr; JSON/streaming formatting happens in the tool_result block elsewhere
// Check if this is a permission denied error in non-interactive mode
const isExecutionDenied = errorCode === ToolErrorType.EXECUTION_DENIED;
const isNonInteractive = !config.isInteractive();
const isTextMode = config.getOutputFormat() === OutputFormat.TEXT;
// Show warning for permission denied errors in non-interactive text mode
if (isExecutionDenied && isNonInteractive && isTextMode) {
const warningMessage =
`Warning: Tool "${toolName}" requires user approval but cannot execute in non-interactive mode.\n` +
`To enable automatic tool execution, use the -y flag (YOLO mode):\n` +
`Example: qwen -p 'your prompt' -y\n\n`;
process.stderr.write(warningMessage);
}
// Always log detailed error in debug mode
if (config.getDebugMode()) {
console.error(
`Error executing tool ${toolName}: ${resultDisplay || toolError.message}`,

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.6.0",
"version": "0.6.1-nightly.20260108.570ec432",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -824,7 +824,6 @@ export class CoreToolScheduler {
*/
const shouldAutoDeny =
!this.config.isInteractive() &&
!this.config.getIdeMode() &&
!this.config.getExperimentalZedIntegration() &&
this.config.getInputFormat() !== InputFormat.STREAM_JSON;

View File

@@ -752,6 +752,8 @@ export class OpenAIContentConverter {
usage.prompt_tokens_details?.cached_tokens ??
extendedUsage.cached_tokens ??
0;
const thinkingTokens =
usage.completion_tokens_details?.reasoning_tokens || 0;
// If we only have total tokens but no breakdown, estimate the split
// Typically input is ~70% and output is ~30% for most conversations
@@ -769,6 +771,7 @@ export class OpenAIContentConverter {
candidatesTokenCount: finalCompletionTokens,
totalTokenCount: totalTokens,
cachedContentTokenCount: cachedTokens,
thoughtsTokenCount: thinkingTokens,
};
}

View File

@@ -317,15 +317,22 @@ export class ContentGenerationPipeline {
}
private buildReasoningConfig(): Record<string, unknown> {
const reasoning = this.contentGeneratorConfig.reasoning;
// Reasoning configuration for OpenAI-compatible endpoints is highly fragmented.
// For example, across common providers and models:
//
// - deepseek-reasoner — thinking is enabled by default and cannot be disabled
// - glm-4.7 — thinking is enabled by default; can be disabled via `extra_body.thinking.enabled`
// - kimi-k2-thinking — thinking is enabled by default and cannot be disabled
// - gpt-5.x series — thinking is enabled by default; can be disabled via `reasoning.effort`
// - qwen3 series — model-dependent; can be manually disabled via `extra_body.enable_thinking`
//
// Given this inconsistency, we choose not to set any reasoning config here and
// instead rely on each models default behavior.
if (reasoning === false) {
return {};
}
// We plan to introduce provider- and model-specific settings to enable more
// fine-grained control over reasoning configuration.
return {
reasoning_effort: reasoning?.effort ?? 'medium',
};
return {};
}
/**

View File

@@ -58,8 +58,6 @@ export class DefaultOpenAICompatibleProvider
}
getDefaultGenerationConfig(): GenerateContentConfig {
return {
topP: 0.95,
};
return {};
}
}

View File

@@ -38,6 +38,7 @@ export * from './utils/quotaErrorDetection.js';
export * from './utils/fileUtils.js';
export * from './utils/retry.js';
export * from './utils/shell-utils.js';
export * from './utils/tool-utils.js';
export * from './utils/terminalSerializer.js';
export * from './utils/systemEncoding.js';
export * from './utils/textUtils.js';

View File

@@ -589,7 +589,7 @@ describe('ShellExecutionService child_process fallback', () => {
expect(result.error).toBeNull();
expect(result.aborted).toBe(false);
expect(result.output).toBe('file1.txt\na warning');
expect(handle.pid).toBe(undefined);
expect(handle.pid).toBe(12345);
expect(onOutputEventMock).toHaveBeenCalledWith({
type: 'data',
@@ -829,7 +829,7 @@ describe('ShellExecutionService child_process fallback', () => {
[],
expect.objectContaining({
shell: true,
detached: false,
detached: true,
}),
);
});

View File

@@ -7,7 +7,7 @@
import stripAnsi from 'strip-ansi';
import type { PtyImplementation } from '../utils/getPty.js';
import { getPty } from '../utils/getPty.js';
import { spawn as cpSpawn } from 'node:child_process';
import { spawn as cpSpawn, spawnSync } from 'node:child_process';
import { TextDecoder } from 'node:util';
import os from 'node:os';
import type { IPty } from '@lydell/node-pty';
@@ -98,6 +98,48 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
return lines.join('\n').trimEnd();
};
interface ProcessCleanupStrategy {
killPty(pid: number, pty: ActivePty): void;
killChildProcesses(pids: Set<number>): void;
}
const windowsStrategy: ProcessCleanupStrategy = {
killPty: (_pid, pty) => {
pty.ptyProcess.kill();
},
killChildProcesses: (pids) => {
if (pids.size > 0) {
try {
const args = ['/f', '/t'];
for (const pid of pids) {
args.push('/pid', pid.toString());
}
spawnSync('taskkill', args);
} catch {
// ignore
}
}
},
};
const posixStrategy: ProcessCleanupStrategy = {
killPty: (pid, _pty) => {
process.kill(-pid, 'SIGKILL');
},
killChildProcesses: (pids) => {
for (const pid of pids) {
try {
process.kill(-pid, 'SIGKILL');
} catch {
// ignore
}
}
},
};
const getCleanupStrategy = () =>
os.platform() === 'win32' ? windowsStrategy : posixStrategy;
/**
* A centralized service for executing shell commands with robust process
* management, cross-platform compatibility, and streaming output capabilities.
@@ -106,6 +148,29 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
export class ShellExecutionService {
private static activePtys = new Map<number, ActivePty>();
private static activeChildProcesses = new Set<number>();
static cleanup() {
const strategy = getCleanupStrategy();
// Cleanup PTYs
for (const [pid, pty] of this.activePtys) {
try {
strategy.killPty(pid, pty);
} catch {
// ignore
}
}
// Cleanup child processes
strategy.killChildProcesses(this.activeChildProcesses);
}
static {
process.on('exit', () => {
ShellExecutionService.cleanup();
});
}
/**
* Executes a shell command using `node-pty`, capturing all output and lifecycle events.
*
@@ -164,7 +229,7 @@ export class ShellExecutionService {
stdio: ['ignore', 'pipe', 'pipe'],
windowsVerbatimArguments: true,
shell: isWindows ? true : 'bash',
detached: !isWindows,
detached: true,
env: {
...process.env,
QWEN_CODE: '1',
@@ -281,9 +346,13 @@ export class ShellExecutionService {
abortSignal.addEventListener('abort', abortHandler, { once: true });
if (child.pid) {
this.activeChildProcesses.add(child.pid);
}
child.on('exit', (code, signal) => {
if (child.pid) {
this.activePtys.delete(child.pid);
this.activeChildProcesses.delete(child.pid);
}
handleExit(code, signal);
});
@@ -310,7 +379,7 @@ export class ShellExecutionService {
}
});
return { pid: undefined, result };
return { pid: child.pid, result };
} catch (e) {
const error = e as Error;
return {

View File

@@ -169,6 +169,44 @@ describe('ShellTool', () => {
});
expect(invocation.getDescription()).not.toContain('[background]');
});
describe('is_background parameter coercion', () => {
it('should accept string "true" as boolean true', () => {
const invocation = shellTool.build({
command: 'npm run dev',
is_background: 'true' as unknown as boolean,
});
expect(invocation).toBeDefined();
expect(invocation.getDescription()).toContain('[background]');
});
it('should accept string "false" as boolean false', () => {
const invocation = shellTool.build({
command: 'npm run build',
is_background: 'false' as unknown as boolean,
});
expect(invocation).toBeDefined();
expect(invocation.getDescription()).not.toContain('[background]');
});
it('should accept string "True" as boolean true', () => {
const invocation = shellTool.build({
command: 'npm run dev',
is_background: 'True' as unknown as boolean,
});
expect(invocation).toBeDefined();
expect(invocation.getDescription()).toContain('[background]');
});
it('should accept string "False" as boolean false', () => {
const invocation = shellTool.build({
command: 'npm run build',
is_background: 'False' as unknown as boolean,
});
expect(invocation).toBeDefined();
expect(invocation.getDescription()).not.toContain('[background]');
});
});
});
describe('execute', () => {
@@ -210,7 +248,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir',
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -237,7 +275,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -262,7 +300,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -287,7 +325,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -312,7 +350,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir/subdir',
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -340,7 +378,7 @@ describe('ShellTool', () => {
'dir',
'/test/dir',
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -433,7 +471,7 @@ describe('ShellTool', () => {
expect(summarizer.summarizeToolOutput).toHaveBeenCalledWith(
expect.any(String),
mockConfig.getGeminiClient(),
mockAbortSignal,
expect.any(AbortSignal),
1000,
);
expect(result.llmContent).toBe('summarized output');
@@ -542,7 +580,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -572,7 +610,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -602,7 +640,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -661,7 +699,7 @@ describe('ShellTool', () => {
expect.stringContaining('npm install'),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -690,7 +728,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit'),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -720,7 +758,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -756,7 +794,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit -m "Initial commit"'),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -793,7 +831,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
mockAbortSignal,
expect.any(AbortSignal),
false,
{},
);
@@ -924,4 +962,41 @@ spanning multiple lines"`;
expect(shellTool.description).toMatchSnapshot();
});
});
describe('Windows background execution', () => {
it('should clean up trailing ampersand on Windows for background tasks', async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const mockAbortSignal = new AbortController().signal;
const invocation = shellTool.build({
command: 'npm start &',
is_background: true,
});
const promise = invocation.execute(mockAbortSignal);
// Simulate immediate success (process started)
resolveExecutionPromise({
rawOutput: Buffer.from(''),
output: '',
exitCode: 0,
signal: null,
error: null,
aborted: false,
pid: 12345,
executionMethod: 'child_process',
});
await promise;
expect(mockShellExecutionService).toHaveBeenCalledWith(
'npm start',
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
false,
{},
);
});
});
});

View File

@@ -143,11 +143,24 @@ export class ShellToolInvocation extends BaseToolInvocation<
const shouldRunInBackground = this.params.is_background;
let finalCommand = processedCommand;
// If explicitly marked as background and doesn't already end with &, add it
if (shouldRunInBackground && !finalCommand.trim().endsWith('&')) {
// On non-Windows, use & to run in background.
// On Windows, we don't use start /B because it creates a detached process that
// doesn't die when the parent dies. Instead, we rely on the race logic below
// to return early while keeping the process attached (detached: false).
if (
!isWindows &&
shouldRunInBackground &&
!finalCommand.trim().endsWith('&')
) {
finalCommand = finalCommand.trim() + ' &';
}
// On Windows, we rely on the race logic below to handle background tasks.
// We just ensure the command string is clean.
if (isWindows && shouldRunInBackground) {
finalCommand = finalCommand.trim().replace(/&+$/, '').trim();
}
// pgrep is not available on Windows, so we can't get background PIDs
const commandToExecute = isWindows
? finalCommand
@@ -169,10 +182,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
commandToExecute,
cwd,
(event: ShellOutputEvent) => {
if (!updateOutput) {
return;
}
let shouldUpdate = false;
switch (event.type) {
@@ -201,7 +210,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
}
if (shouldUpdate) {
if (shouldUpdate && updateOutput) {
updateOutput(
typeof cumulativeOutput === 'string'
? cumulativeOutput
@@ -219,6 +228,21 @@ export class ShellToolInvocation extends BaseToolInvocation<
setPidCallback(pid);
}
if (shouldRunInBackground) {
// For background tasks, return immediately with PID info
// Note: We cannot reliably detect startup errors for background processes
// since their stdio is typically detached/ignored
const pidMsg = pid ? ` PID: ${pid}` : '';
const killHint = isWindows
? ' (Use taskkill /F /T /PID <pid> to stop)'
: ' (Use kill <pid> to stop)';
return {
llmContent: `Background command started.${pidMsg}${killHint}`,
returnDisplay: `Background command started.${pidMsg}${killHint}`,
};
}
const result = await resultPromise;
const backgroundPIDs: number[] = [];

View File

@@ -122,4 +122,91 @@ describe('SchemaValidator', () => {
};
expect(SchemaValidator.validate(schema, params)).not.toBeNull();
});
describe('boolean string coercion', () => {
const booleanSchema = {
type: 'object',
properties: {
is_background: {
type: 'boolean',
},
},
required: ['is_background'],
};
it('should coerce string "true" to boolean true', () => {
const params = { is_background: 'true' };
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
expect(params.is_background).toBe(true);
});
it('should coerce string "True" to boolean true', () => {
const params = { is_background: 'True' };
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
expect(params.is_background).toBe(true);
});
it('should coerce string "TRUE" to boolean true', () => {
const params = { is_background: 'TRUE' };
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
expect(params.is_background).toBe(true);
});
it('should coerce string "false" to boolean false', () => {
const params = { is_background: 'false' };
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
expect(params.is_background).toBe(false);
});
it('should coerce string "False" to boolean false', () => {
const params = { is_background: 'False' };
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
expect(params.is_background).toBe(false);
});
it('should coerce string "FALSE" to boolean false', () => {
const params = { is_background: 'FALSE' };
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
expect(params.is_background).toBe(false);
});
it('should handle nested objects with string booleans', () => {
const nestedSchema = {
type: 'object',
properties: {
options: {
type: 'object',
properties: {
enabled: { type: 'boolean' },
},
},
},
};
const params = { options: { enabled: 'true' } };
expect(SchemaValidator.validate(nestedSchema, params)).toBeNull();
expect((params.options as unknown as { enabled: boolean }).enabled).toBe(
true,
);
});
it('should not affect non-boolean strings', () => {
const mixedSchema = {
type: 'object',
properties: {
name: { type: 'string' },
is_active: { type: 'boolean' },
},
};
const params = { name: 'trueman', is_active: 'true' };
expect(SchemaValidator.validate(mixedSchema, params)).toBeNull();
expect(params.name).toBe('trueman');
expect(params.is_active).toBe(true);
});
it('should pass through actual boolean values unchanged', () => {
const params = { is_background: true };
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
expect(params.is_background).toBe(true);
});
});
});

View File

@@ -41,14 +41,12 @@ export class SchemaValidator {
return 'Value of params must be an object';
}
const validate = ajValidator.compile(schema);
const valid = validate(data);
let valid = validate(data);
if (!valid && validate.errors) {
// Find any True or False values and lowercase them
fixBooleanCasing(data as Record<string, unknown>);
const validate = ajValidator.compile(schema);
const valid = validate(data);
// Coerce string boolean values ("true"/"false") to actual booleans
fixBooleanValues(data as Record<string, unknown>);
valid = validate(data);
if (!valid && validate.errors) {
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
}
@@ -57,13 +55,29 @@ export class SchemaValidator {
}
}
function fixBooleanCasing(data: Record<string, unknown>) {
/**
* Coerces string boolean values to actual booleans.
* This handles cases where LLMs return "true"/"false" strings instead of boolean values,
* which is common with self-hosted LLMs.
*
* Converts:
* - "true", "True", "TRUE" -> true
* - "false", "False", "FALSE" -> false
*/
function fixBooleanValues(data: Record<string, unknown>) {
for (const key of Object.keys(data)) {
if (!(key in data)) continue;
const value = data[key];
if (typeof data[key] === 'object') {
fixBooleanCasing(data[key] as Record<string, unknown>);
} else if (data[key] === 'True') data[key] = 'true';
else if (data[key] === 'False') data[key] = 'false';
if (typeof value === 'object' && value !== null) {
fixBooleanValues(value as Record<string, unknown>);
} else if (typeof value === 'string') {
const lower = value.toLowerCase();
if (lower === 'true') {
data[key] = true;
} else if (lower === 'false') {
data[key] = false;
}
}
}
}

View File

@@ -0,0 +1,24 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
tab_width = 4
ij_continuation_indent_size = 8
[*.java]
ij_java_doc_align_exception_comments = false
ij_java_doc_align_param_comments = false
[*.{yaml, yml, sh, ps1}]
indent_size = 2
[*.{md, mkd, markdown}]
trim_trailing_whitespace = false
[{**/res/**.xml, **/AndroidManifest.xml}]
ij_continuation_indent_size = 4

14
packages/sdk-java/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
# Mac
.DS_Store
# Maven
log/
target/
/docs/

201
packages/sdk-java/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

378
packages/sdk-java/QWEN.md Normal file
View File

@@ -0,0 +1,378 @@
# Qwen Code Java SDK
## Project Overview
The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
**Context Information:**
- Current Date: Monday 5 January 2026
- Operating System: darwin
- Working Directory: /Users/weigeng/repos/qwen-code/packages/sdk-java
## Project Details
- **Group ID**: com.alibaba
- **Artifact ID**: qwencode-sdk (as per pom.xml)
- **Version**: 0.0.1-SNAPSHOT
- **Packaging**: JAR
- **Java Version**: 1.8+ (source and target)
- **License**: Apache-2.0
## Architecture
The SDK follows a layered architecture:
- **API Layer**: Provides the main entry points through `QwenCodeCli` class with simple static methods for basic usage
- **Session Layer**: Manages communication sessions with the Qwen Code CLI through the `Session` class
- **Transport Layer**: Handles the communication mechanism between the SDK and CLI process (currently using process transport via `ProcessTransport`)
- **Protocol Layer**: Defines data structures for communication based on the CLI protocol
- **Utils**: Common utilities for concurrent execution, timeout handling, and error management
## Key Components
### Main Classes
- `QwenCodeCli`: Main entry point with static methods for simple queries
- `Session`: Manages communication sessions with the CLI
- `Transport`: Abstracts the communication mechanism (currently using process transport)
- `ProcessTransport`: Implementation that communicates via process execution
- `TransportOptions`: Configuration class for transport layer settings
- `SessionEventSimpleConsumers`: High-level event handler for processing responses
- `AssistantContentSimpleConsumers`: Handles different types of content within assistant messages
### Dependencies
- **Logging**: ch.qos.logback:logback-classic
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: com.alibaba.fastjson2:fastjson2
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)
## Building and Running
### Prerequisites
- Java 8 or higher
- Apache Maven 3.6.0 or higher
### Build Commands
```bash
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
# Run checkstyle verification
mvn checkstyle:check
# Generate Javadoc
mvn javadoc:javadoc
```
### Testing
The project includes basic unit tests using JUnit 5. The main test class `QwenCodeCliTest` demonstrates how to use the SDK to make simple queries to the Qwen Code CLI.
### Code Quality
The project uses Checkstyle for code formatting and style enforcement. The configuration is defined in `checkstyle.xml` and includes rules for:
- Whitespace and indentation
- Naming conventions
- Import ordering
- Code structure
- Line endings (LF only)
- No trailing whitespace
- 8-space indentation for line wrapping
## Development Conventions
### Coding Standards
- Java 8 language features are supported
- Follow standard Java naming conventions
- Use UTF-8 encoding for source files
- Line endings should be LF (Unix-style)
- No trailing whitespace allowed
- Use 8-space indentation for line wrapping
### Testing Practices
- Write unit tests using JUnit 5
- Test classes should be in the `src/test/java` directory
- Follow the naming convention `*Test.java` for test classes
- Use appropriate assertions to validate functionality
### Documentation
- API documentation should follow Javadoc conventions
- Update README files when adding new features
- Include examples in documentation
## API Reference
### QwenCodeCli Class
The main class provides several primary methods:
- `simpleQuery(String prompt)`: Synchronous method that returns a list of responses
- `simpleQuery(String prompt, TransportOptions transportOptions)`: Synchronous method with custom transport options
- `simpleQuery(String prompt, TransportOptions transportOptions, AssistantContentConsumers assistantContentConsumers)`: Advanced method with custom content consumers
- `newSession()`: Creates a new session with default options
- `newSession(TransportOptions transportOptions)`: Creates a new session with custom options
### Permission Modes
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) while other tools require confirmation.
- **`yolo`**: All tools execute automatically without confirmation.
### Transport Options
The `TransportOptions` class allows configuration of how the SDK communicates with the Qwen Code CLI:
- `pathToQwenExecutable`: Path to the Qwen Code CLI executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: List of core tools that should be available to the AI
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `skillsEnable`: Enables or disables skills functionality for the session
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: 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 `Session` class 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()` in `TransportOptions` to 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**: false
- **Rejected Execution Handler**: CallerRunsPolicy
### 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<CLIControlPermissionRequest>, 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 ThingkingAssistantContent)
- `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:**
- `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`, `onUsage`
- `onPartialAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest``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``onSystemMessageTimeout`
- `onResultMessage``onResultMessageTimeout`
- `onAssistantMessage``onAssistantMessageTimeout`
- `onPartialAssistantMessage``onPartialAssistantMessageTimeout`
- `onUserMessage``onUserMessageTimeout`
- `onOtherMessage``onOtherMessageTimeout`
- `onControlResponse``onControlResponseTimeout`
- `onControlRequest``onControlRequestTimeout`
For AssistantContentConsumers timeout methods:
- `onText``onTextTimeout`
- `onThinking``onThinkingTimeout`
- `onToolUse``onToolUseTimeout`
- `onToolResult``onToolResultTimeout`
- `onOtherContent``onOtherContentTimeout`
- `onPermissionRequest``onPermissionRequestTimeout`
- `onOtherControlRequest``onOtherControlRequestTimeout`
**Default 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 values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return values
#### Relationship Between the Interfaces
- `AssistantContentSimpleConsumers` is the default implementation of `AssistantContentConsumers`
- `SessionEventSimpleConsumers` is the concrete implementation that combines both interfaces and depends on an `AssistantContentConsumers` instance to handle content within assistant messages
- The timeout methods in `SessionEventConsumers` now include the message object as a parameter (e.g., `onSystemMessageTimeout(Session session, SDKSystemMessage systemMessage)`)
Event processing is subject to the timeout settings configured in `TransportOptions` and `SessionEventConsumers`. For detailed timeout configuration options, see the "Timeout" section above.
## Usage Examples
The SDK includes several example files in `src/test/java/com/alibaba/qwen/code/cli/example/` that demonstrate different aspects of the API:
### Basic Usage
- `QuickStartExample.java`: Demonstrates simple query usage, transport options configuration, and streaming content handling
### Session Control
- `SessionExample.java`: Shows session control features including permission mode changes, model switching, interruption, and event handling
### Configuration
- `ThreadPoolConfigurationExample.java`: Shows how to configure the thread pool used by the SDK
## Error Handling
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 response
- `SessionClosedException`: Thrown when attempting to use a closed session
## Project Structure
```
src/
├── example/
│ └── java/
│ └── com/
│ └── alibaba/
│ └── qwen/
│ └── code/
│ └── example/
├── main/
│ └── java/
│ └── com/
│ └── alibaba/
│ └── qwen/
│ └── code/
│ └── cli/
│ ├── QwenCodeCli.java
│ ├── protocol/
│ ├── session/
│ ├── transport/
│ └── utils/
└── test/
├── java/
│ └── com/
│ └── alibaba/
│ └── qwen/
│ └── code/
│ └── cli/
│ ├── QwenCodeCliTest.java
│ ├── session/
│ │ └── SessionTest.java
│ └── transport/
│ ├── PermissionModeTest.java
│ └── process/
│ └── ProcessTransportTest.java
└── temp/
```
## Configuration Files
- `pom.xml`: Maven build configuration and dependencies
- `checkstyle.xml`: Code style and formatting rules
- `.editorconfig`: Editor configuration settings
## FAQ / Troubleshooting
### Q: Do I need to install the Qwen CLI separately?
A: No, from v0.1.1, the CLI is bundled with the SDK, so no standalone CLI installation is needed.
### Q: What Java versions are supported?
A: The SDK requires Java 1.8 or higher.
### 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.
### Q: What happens if the CLI process crashes?
A: The SDK will throw appropriate exceptions. Make sure to handle `SessionControlException` and implement retry logic if needed.
## Maintainers
- **Developer**: skyfire (gengwei.gw(at)alibaba-inc.com)
- **Organization**: Alibaba Group

312
packages/sdk-java/README.md Normal file
View File

@@ -0,0 +1,312 @@
# Qwen Code Java SDK
The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
## Requirements
- Java >= 1.8
- Maven >= 3.6.0 (for building from source)
- qwen-code >= 0.5.0
### Dependencies
- **Logging**: ch.qos.logback:logback-classic
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: com.alibaba.fastjson2:fastjson2
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)
## Installation
Add the following dependency to your Maven `pom.xml`:
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<version>{$version}</version>
</dependency>
```
Or if using Gradle, add to your `build.gradle`:
```gradle
implementation 'com.alibaba:qwencode-sdk:{$version}'
```
## Building and Running
### Build Commands
```bash
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
```
## Quick Start
The simplest way to use the SDK is through the `QwenCodeCli.simpleQuery()` method:
```java
public static void runSimpleExample() {
List<String> result = QwenCodeCli.simpleQuery("hello world");
result.forEach(logger::info);
}
```
For more advanced usage with custom transport options:
```java
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:
```java
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, ThingkingAssistantContent thingkingAssistantContent) {
logger.info("Thinking content received: {}", thingkingAssistantContent.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
## Architecture
The SDK follows a layered architecture:
- **API Layer**: Provides the main entry points through `QwenCodeCli` class with simple static methods for basic usage
- **Session Layer**: Manages communication sessions with the Qwen Code CLI through the `Session` class
- **Transport Layer**: Handles the communication mechanism between the SDK and CLI process (currently using process transport via `ProcessTransport`)
- **Protocol Layer**: Defines data structures for communication based on the CLI protocol
- **Utils**: Common utilities for concurrent execution, timeout handling, and error management
## Key Features
### Permission Modes
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) while other tools require confirmation.
- **`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<CLIControlPermissionRequest>, 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 ThingkingAssistantContent)
- `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:**
- `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`, `onUsage`
- `onPartialAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest``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``onSystemMessageTimeout`
- `onResultMessage``onResultMessageTimeout`
- `onAssistantMessage``onAssistantMessageTimeout`
- `onPartialAssistantMessage``onPartialAssistantMessageTimeout`
- `onUserMessage``onUserMessageTimeout`
- `onOtherMessage``onOtherMessageTimeout`
- `onControlResponse``onControlResponseTimeout`
- `onControlRequest``onControlRequestTimeout`
For AssistantContentConsumers timeout methods:
- `onText``onTextTimeout`
- `onThinking``onThinkingTimeout`
- `onToolUse``onToolUseTimeout`
- `onToolResult``onToolResultTimeout`
- `onOtherContent``onOtherContentTimeout`
- `onPermissionRequest``onPermissionRequestTimeout`
- `onOtherControlRequest``onOtherControlRequestTimeout`
**Default 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 values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return 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 executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: List of core tools that should be available to the AI
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `skillsEnable`: Enables or disables skills functionality for the session
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: 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 `Session` class 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()` in `TransportOptions` to 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**: false
- **Rejected Execution Handler**: CallerRunsPolicy
## Error Handling
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 response
- `SessionClosedException`: Thrown when attempting to use a closed session
## FAQ / Troubleshooting
### Q: Do I need to install the Qwen CLI separately?
A: No, from v0.1.1, the CLI is bundled with the SDK, so no standalone CLI installation is needed.
### Q: What Java versions are supported?
A: The SDK requires Java 1.8 or higher.
### 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](./LICENSE) for details.

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="FileTabCharacter" />
<module name="NewlineAtEndOfFile">
<property name="lineSeparator" value="lf" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\r" />
<property name="message" value="Line contains carriage return" />
</module>
<module name="RegexpMultiline">
<property name="format" value=" \n" />
<property name="message" value="Line has trailing whitespace" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\n\n\n" />
<property name="message" value="Multiple consecutive blank lines" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\n\n\Z" />
<property name="message" value="Blank line before end of file" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\{\n\n" />
<property name="message" value="Blank line after opening brace" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\n\n\s*\}" />
<property name="message" value="Blank line before closing brace" />
</module>
<module name="RegexpMultiline">
<property name="format" value="->\s*\{\s+\}" />
<property name="message" value="Whitespace inside empty lambda body" />
</module>
<module name="TreeWalker">
<module name="SuppressWarningsHolder" />
<module name="EmptyBlock">
<property name="option" value="text" />
<property name="tokens" value="
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_IF,
LITERAL_FOR, LITERAL_TRY, LITERAL_WHILE, INSTANCE_INIT, STATIC_INIT" />
</module>
<module name="EmptyStatement" />
<module name="EmptyForInitializerPad" />
<module name="MethodParamPad">
<property name="allowLineBreaks" value="true" />
<property name="option" value="nospace" />
</module>
<module name="ParenPad" />
<module name="TypecastParenPad" />
<module name="NeedBraces" />
<module name="LeftCurly">
<property name="option" value="eol" />
<property name="tokens" value="
LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR,
LITERAL_IF, LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE" />
</module>
<module name="GenericWhitespace" />
<module name="WhitespaceAfter" />
<module name="NoWhitespaceAfter" />
<module name="NoWhitespaceBefore" />
<module name="SingleSpaceSeparator" />
<module name="Indentation">
<property name="throwsIndent" value="8" />
<property name="lineWrappingIndentation" value="8" />
</module>
<module name="UpperEll" />
<module name="DefaultComesLast" />
<module name="ArrayTypeStyle" />
<module name="ModifierOrder" />
<module name="OneStatementPerLine" />
<module name="StringLiteralEquality" />
<module name="MutableException" />
<module name="EqualsHashCode" />
<module name="ExplicitInitialization" />
<module name="OneTopLevelClass" />
<module name="MemberName" />
<module name="PackageName" />
<module name="ClassTypeParameterName">
<property name="format" value="^[A-Z][0-9]?$" />
</module>
<module name="MethodTypeParameterName">
<property name="format" value="^[A-Z][0-9]?$" />
</module>
<module name="AnnotationUseStyle">
<property name="trailingArrayComma" value="ignore" />
</module>
<module name="RedundantImport" />
<module name="UnusedImports" />
<!-- <module name="ImportOrder">-->
<!-- <property name="groups" value="*,javax,java" />-->
<!-- <property name="separated" value="true" />-->
<!-- <property name="option" value="bottom" />-->
<!-- <property name="sortStaticImportsAlphabetically" value="true" />-->
<!-- </module>-->
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true" />
<property name="allowEmptyMethods" value="true" />
<property name="allowEmptyLambdas" value="true" />
<property name="ignoreEnhancedForColon" value="false" />
<property name="tokens" value="
ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN,
BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND,
LAMBDA, LE, LITERAL_ASSERT, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH,
LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE,
LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL,
PLUS, PLUS_ASSIGN, QUESTION, SL, SLIST, SL_ASSIGN, SR, SR_ASSIGN,
STAR, STAR_ASSIGN, TYPE_EXTENSION_AND" />
</module>
<module name="WhitespaceAfter" />
<module name="NoWhitespaceAfter">
<property name="tokens" value="DOT" />
<property name="allowLineBreaks" value="false" />
</module>
<module name="MissingOverride"/>
</module>
</module>

193
packages/sdk-java/pom.xml Normal file
View File

@@ -0,0 +1,193 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<packaging>jar</packaging>
<version>0.0.1-alpha</version>
<name>qwencode-sdk</name>
<description>The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface
to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
</description>
<url>https://maven.apache.org</url>
<licenses>
<license>
<name>Apache 2</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
<comments>A business-friendly OSS license</comments>
</license>
</licenses>
<scm>
<url>https://github.com/QwenLM/qwen-code</url>
<connection>scm:git:https://github.com/QwenLM/qwen-code.git</connection>
</scm>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<checkstyle-maven-plugin.version>3.6.0</checkstyle-maven-plugin.version>
<jacoco-maven-plugin.version>0.8.12</jacoco-maven-plugin.version>
<junit5.version>5.14.1</junit5.version>
<logback-classic.version>1.3.16</logback-classic.version>
<fastjson2.version>2.0.60</fastjson2.version>
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
<central-publishing-maven-plugin.version>0.8.0</central-publishing-maven-plugin.version>
<maven-source-plugin.version>2.2.1</maven-source-plugin.version>
<maven-javadoc-plugin.version>2.9.1</maven-javadoc-plugin.version>
<maven-gpg-plugin.version>1.5</maven-gpg-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<type>pom</type>
<version>${junit5.version}</version>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle-maven-plugin.version}</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>${central-publishing-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${maven-source-plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven-javadoc-plugin.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<organization>
<name>Alibaba Group</name>
<url>https://github.com/alibaba</url>
</organization>
<developers>
<developer>
<id>skyfire</id>
<name>skyfire</name>
<email>gengwei.gw(at)alibaba-inc.com</email>
<roles>
<role>Developer</role>
<role>Designer</role>
</roles>
<timezone>+8</timezone>
<url>https://github.com/gwinthis</url>
</developer>
</developers>
<distributionManagement>
<snapshotRepository>
<id>central</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
</snapshotRepository>
<repository>
<id>central</id>
<url>https://central.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</project>

View File

@@ -0,0 +1,142 @@
package com.alibaba.qwen.code.cli;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson2.JSON;
import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior.Operation;
import com.alibaba.qwen.code.cli.session.Session;
import com.alibaba.qwen.code.cli.session.event.consumers.AssistantContentConsumers;
import com.alibaba.qwen.code.cli.session.event.consumers.AssistantContentSimpleConsumers;
import com.alibaba.qwen.code.cli.session.event.consumers.SessionEventSimpleConsumers;
import com.alibaba.qwen.code.cli.transport.Transport;
import com.alibaba.qwen.code.cli.transport.TransportOptions;
import com.alibaba.qwen.code.cli.transport.process.ProcessTransport;
import com.alibaba.qwen.code.cli.utils.MyConcurrentUtils;
import com.alibaba.qwen.code.cli.utils.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main entry point for interacting with the Qwen Code CLI. Provides static methods for simple queries and session management.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class QwenCodeCli {
private static final Logger log = LoggerFactory.getLogger(QwenCodeCli.class);
/**
* Sends a simple query to the Qwen Code CLI and returns a list of responses.
*
* @param prompt The input prompt to send to the CLI
* @return A list of strings representing the CLI's responses
*/
public static List<String> simpleQuery(String prompt) {
return simpleQuery(prompt, new TransportOptions());
}
/**
* Sends a simple query with custom transport options.
*
* @param prompt The input prompt to send to the CLI
* @param transportOptions Configuration options for the transport layer
* @return A list of strings representing the CLI's responses
*/
public static List<String> simpleQuery(String prompt, TransportOptions transportOptions) {
final List<String> response = new ArrayList<>();
MyConcurrentUtils.runAndWait(() -> simpleQuery(prompt, transportOptions, new AssistantContentSimpleConsumers() {
@Override
public void onText(Session session, TextAssistantContent textAssistantContent) {
response.add(textAssistantContent.getText());
}
@Override
public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) {
response.add(thingkingAssistantContent.getThinking());
}
@Override
public void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent) {
response.add(JSON.toJSONString(toolUseAssistantContent.getContentOfAssistant()));
}
@Override
public void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent) {
response.add(JSON.toJSONString(toolResultAssistantContent));
}
public void onOtherContent(Session session, AssistantContent<?> other) {
response.add(JSON.toJSONString(other.getContentOfAssistant()));
}
@Override
public void onUsage(Session session, AssistantUsage assistantUsage) {
log.info("received usage {} of message {}", assistantUsage.getUsage(), assistantUsage.getMessageId());
}
}.setDefaultPermissionOperation(Operation.allow)), Timeout.TIMEOUT_30_MINUTES);
return response;
}
/**
* Sends a query with custom content consumers.
*
* @param prompt The input prompt to send to the CLI
* @param transportOptions Configuration options for the transport layer
* @param assistantContentConsumers Consumers for handling different types of assistant content
*/
public static void simpleQuery(String prompt, TransportOptions transportOptions, AssistantContentConsumers assistantContentConsumers) {
Session session = newSession(transportOptions);
try {
session.sendPrompt(prompt, new SessionEventSimpleConsumers()
.setAssistantContentConsumer(assistantContentConsumers));
} catch (Exception e) {
throw new RuntimeException("sendPrompt error!", e);
} finally {
try {
session.close();
} catch (Exception e) {
log.error("close session error!", e);
}
}
}
/**
* Creates a new session with default transport options.
*
* @return A new Session instance
*/
public static Session newSession() {
return newSession(new TransportOptions());
}
/**
* Creates a new session with custom transport options.
*
* @param transportOptions Configuration options for the transport layer
* @return A new Session instance
*/
public static Session newSession(TransportOptions transportOptions) {
Transport transport;
try {
transport = new ProcessTransport(transportOptions);
} catch (Exception e) {
throw new RuntimeException("initialized ProcessTransport error!", e);
}
Session session;
try {
session = new Session(transport);
} catch (Exception e) {
throw new RuntimeException("initialized Session error!", e);
}
return session;
}
}

View File

@@ -0,0 +1,95 @@
package com.alibaba.qwen.code.cli.protocol.data;
import java.util.Map;
/**
* Represents content from the assistant in a Qwen Code session.
*
* @param <C> The type of content
* @author skyfire
* @version $Id: 0.0.1
*/
public interface AssistantContent<C> {
/**
* Gets the type of the assistant content.
*
* @return The type of the assistant content
*/
String getType();
/**
* Gets the actual content from the assistant.
*
* @return The content from the assistant
*/
C getContentOfAssistant();
/**
* Gets the message ID associated with this content.
*
* @return The message ID
*/
String getMessageId();
/**
* Represents text content from the assistant.
*/
interface TextAssistantContent extends AssistantContent<String> {
/**
* Gets the text content.
*
* @return The text content
*/
String getText();
}
/**
* Represents thinking content from the assistant.
*/
interface ThingkingAssistantContent extends AssistantContent<String> {
/**
* Gets the thinking content.
*
* @return The thinking content
*/
String getThinking();
}
/**
* Represents tool use content from the assistant.
*/
interface ToolUseAssistantContent extends AssistantContent<Map<String, Object>> {
/**
* Gets the tool input.
*
* @return The tool input
*/
Map<String, Object> getInput();
}
/**
* Represents tool result content from the assistant.
*/
interface ToolResultAssistantContent extends AssistantContent<String> {
/**
* Gets whether the tool result indicates an error.
*
* @return Whether the tool result indicates an error
*/
Boolean getIsError();
/**
* Gets the tool result content.
*
* @return The tool result content
*/
String getContent();
/**
* Gets the tool use ID.
*
* @return The tool use ID
*/
String getToolUseId();
}
}

View File

@@ -0,0 +1,76 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.JSON;
/**
* Represents usage information for an assistant message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class AssistantUsage {
/**
* The ID of the message.
*/
String messageId;
/**
* The usage information.
*/
Usage usage;
/**
* Gets the message ID.
*
* @return The message ID
*/
public String getMessageId() {
return messageId;
}
/**
* Sets the message ID.
*
* @param messageId The message ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* Gets the usage information.
*
* @return The usage information
*/
public Usage getUsage() {
return usage;
}
/**
* Sets the usage information.
*
* @param usage The usage information
*/
public void setUsage(Usage usage) {
this.usage = usage;
}
/**
* Constructs a new AssistantUsage instance.
*
* @param messageId The message ID
* @param usage The usage information
*/
public AssistantUsage(String messageId, Usage usage) {
this.messageId = messageId;
this.usage = usage;
}
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@@ -0,0 +1,83 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents a permission denial from the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class CLIPermissionDenial {
/**
* The name of the denied tool.
*/
@JSONField(name = "tool_name")
private String toolName;
/**
* The ID of the denied tool use.
*/
@JSONField(name = "tool_use_id")
private String toolUseId;
/**
* The input for the denied tool.
*/
@JSONField(name = "tool_input")
private Object toolInput;
/**
* Gets the name of the denied tool.
*
* @return The name of the denied tool
*/
public String getToolName() {
return toolName;
}
/**
* Sets the name of the denied tool.
*
* @param toolName The name of the denied tool
*/
public void setToolName(String toolName) {
this.toolName = toolName;
}
/**
* Gets the ID of the denied tool use.
*
* @return The ID of the denied tool use
*/
public String getToolUseId() {
return toolUseId;
}
/**
* Sets the ID of the denied tool use.
*
* @param toolUseId The ID of the denied tool use
*/
public void setToolUseId(String toolUseId) {
this.toolUseId = toolUseId;
}
/**
* Gets the input for the denied tool.
*
* @return The input for the denied tool
*/
public Object getToolInput() {
return toolInput;
}
/**
* Sets the input for the denied tool.
*
* @param toolInput The input for the denied tool
*/
public void setToolInput(Object toolInput) {
this.toolInput = toolInput;
}
}

View File

@@ -0,0 +1,131 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents the capabilities of the Qwen Code CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class Capabilities {
/**
* Whether the CLI can handle can_use_tool requests.
*/
@JSONField(name = "can_handle_can_use_tool")
boolean canHandleCanUseTool;
/**
* Whether the CLI can handle hook callbacks.
*/
@JSONField(name = "can_handle_hook_callback")
boolean canHandleHookCallback;
/**
* Whether the CLI can set permission mode.
*/
@JSONField(name = "can_set_permission_mode")
boolean canSetPermissionMode;
/**
* Whether the CLI can set the model.
*/
@JSONField(name = "can_set_model")
boolean canSetModel;
/**
* Whether the CLI can handle MCP messages.
*/
@JSONField(name = "can_handle_mcp_message")
boolean canHandleMcpMessage;
/**
* Checks if the CLI can handle can_use_tool requests.
*
* @return true if the CLI can handle can_use_tool requests, false otherwise
*/
public boolean isCanHandleCanUseTool() {
return canHandleCanUseTool;
}
/**
* Sets whether the CLI can handle can_use_tool requests.
*
* @param canHandleCanUseTool Whether the CLI can handle can_use_tool requests
*/
public void setCanHandleCanUseTool(boolean canHandleCanUseTool) {
this.canHandleCanUseTool = canHandleCanUseTool;
}
/**
* Checks if the CLI can handle hook callbacks.
*
* @return true if the CLI can handle hook callbacks, false otherwise
*/
public boolean isCanHandleHookCallback() {
return canHandleHookCallback;
}
/**
* Sets whether the CLI can handle hook callbacks.
*
* @param canHandleHookCallback Whether the CLI can handle hook callbacks
*/
public void setCanHandleHookCallback(boolean canHandleHookCallback) {
this.canHandleHookCallback = canHandleHookCallback;
}
/**
* Checks if the CLI can set permission mode.
*
* @return true if the CLI can set permission mode, false otherwise
*/
public boolean isCanSetPermissionMode() {
return canSetPermissionMode;
}
/**
* Sets whether the CLI can set permission mode.
*
* @param canSetPermissionMode Whether the CLI can set permission mode
*/
public void setCanSetPermissionMode(boolean canSetPermissionMode) {
this.canSetPermissionMode = canSetPermissionMode;
}
/**
* Checks if the CLI can set the model.
*
* @return true if the CLI can set the model, false otherwise
*/
public boolean isCanSetModel() {
return canSetModel;
}
/**
* Sets whether the CLI can set the model.
*
* @param canSetModel Whether the CLI can set the model
*/
public void setCanSetModel(boolean canSetModel) {
this.canSetModel = canSetModel;
}
/**
* Checks if the CLI can handle MCP messages.
*
* @return true if the CLI can handle MCP messages, false otherwise
*/
public boolean isCanHandleMcpMessage() {
return canHandleMcpMessage;
}
/**
* Sets whether the CLI can handle MCP messages.
*
* @param canHandleMcpMessage Whether the CLI can handle MCP messages
*/
public void setCanHandleMcpMessage(boolean canHandleMcpMessage) {
this.canHandleMcpMessage = canHandleMcpMessage;
}
}

View File

@@ -0,0 +1,147 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Extends the Usage class with additional usage information.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class ExtendedUsage extends Usage {
/**
* Server tool use information.
*/
@JSONField(name = "server_tool_use")
private ServerToolUse serverToolUse;
/**
* Service tier information.
*/
@JSONField(name = "service_tier")
private String serviceTier;
/**
* Cache creation information.
*/
@JSONField(name = "cache_creation")
private CacheCreation cacheCreation;
/**
* Gets the server tool use information.
*
* @return The server tool use information
*/
public ServerToolUse getServerToolUse() {
return serverToolUse;
}
/**
* Sets the server tool use information.
*
* @param serverToolUse The server tool use information
*/
public void setServerToolUse(ServerToolUse serverToolUse) {
this.serverToolUse = serverToolUse;
}
/**
* Gets the service tier information.
*
* @return The service tier information
*/
public String getServiceTier() {
return serviceTier;
}
/**
* Sets the service tier information.
*
* @param serviceTier The service tier information
*/
public void setServiceTier(String serviceTier) {
this.serviceTier = serviceTier;
}
/**
* Gets the cache creation information.
*
* @return The cache creation information
*/
public CacheCreation getCacheCreation() {
return cacheCreation;
}
/**
* Sets the cache creation information.
*
* @param cacheCreation The cache creation information
*/
public void setCacheCreation(CacheCreation cacheCreation) {
this.cacheCreation = cacheCreation;
}
/**
* Represents server tool use information.
*/
public static class ServerToolUse {
/**
* Number of web search requests.
*/
@JSONField(name = "web_search_requests")
private int webSearchRequests;
}
/**
* Represents cache creation information.
*/
public static class CacheCreation {
/**
* Number of ephemeral 1-hour input tokens.
*/
@JSONField(name = "ephemeral_1h_input_tokens")
private int ephemeral1hInputTokens;
/**
* Number of ephemeral 5-minute input tokens.
*/
@JSONField(name = "ephemeral_5m_input_tokens")
private int ephemeral5mInputTokens;
/**
* Gets the number of ephemeral 1-hour input tokens.
*
* @return The number of ephemeral 1-hour input tokens
*/
public int getEphemeral1hInputTokens() {
return ephemeral1hInputTokens;
}
/**
* Sets the number of ephemeral 1-hour input tokens.
*
* @param ephemeral1hInputTokens The number of ephemeral 1-hour input tokens
*/
public void setEphemeral1hInputTokens(int ephemeral1hInputTokens) {
this.ephemeral1hInputTokens = ephemeral1hInputTokens;
}
/**
* Gets the number of ephemeral 5-minute input tokens.
*
* @return The number of ephemeral 5-minute input tokens
*/
public int getEphemeral5mInputTokens() {
return ephemeral5mInputTokens;
}
/**
* Sets the number of ephemeral 5-minute input tokens.
*
* @param ephemeral5mInputTokens The number of ephemeral 5-minute input tokens
*/
public void setEphemeral5mInputTokens(int ephemeral5mInputTokens) {
this.ephemeral5mInputTokens = ephemeral5mInputTokens;
}
}
}

View File

@@ -0,0 +1,98 @@
package com.alibaba.qwen.code.cli.protocol.data;
/**
* Configuration for initializing the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class InitializeConfig {
/**
* Hooks configuration.
*/
String hooks;
/**
* SDK MCP servers configuration.
*/
String sdkMcpServers;
/**
* MCP servers configuration.
*/
String mcpServers;
/**
* Agents configuration.
*/
String agents;
/**
* Gets the hooks configuration.
*
* @return The hooks configuration
*/
public String getHooks() {
return hooks;
}
/**
* Sets the hooks configuration.
*
* @param hooks The hooks configuration
*/
public void setHooks(String hooks) {
this.hooks = hooks;
}
/**
* Gets the SDK MCP servers configuration.
*
* @return The SDK MCP servers configuration
*/
public String getSdkMcpServers() {
return sdkMcpServers;
}
/**
* Sets the SDK MCP servers configuration.
*
* @param sdkMcpServers The SDK MCP servers configuration
*/
public void setSdkMcpServers(String sdkMcpServers) {
this.sdkMcpServers = sdkMcpServers;
}
/**
* Gets the MCP servers configuration.
*
* @return The MCP servers configuration
*/
public String getMcpServers() {
return mcpServers;
}
/**
* Sets the MCP servers configuration.
*
* @param mcpServers The MCP servers configuration
*/
public void setMcpServers(String mcpServers) {
this.mcpServers = mcpServers;
}
/**
* Gets the agents configuration.
*
* @return The agents configuration
*/
public String getAgents() {
return agents;
}
/**
* Sets the agents configuration.
*
* @param agents The agents configuration
*/
public void setAgents(String agents) {
this.agents = agents;
}
}

View File

@@ -0,0 +1,142 @@
package com.alibaba.qwen.code.cli.protocol.data;
/**
* Represents usage information for a specific model.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class ModelUsage {
/**
* Number of input tokens.
*/
private int inputTokens;
/**
* Number of output tokens.
*/
private int outputTokens;
/**
* Number of cache read input tokens.
*/
private int cacheReadInputTokens;
/**
* Number of cache creation input tokens.
*/
private int cacheCreationInputTokens;
/**
* Number of web search requests.
*/
private int webSearchRequests;
/**
* Context window size.
*/
private int contextWindow;
/**
* Gets the number of input tokens.
*
* @return The number of input tokens
*/
public int getInputTokens() {
return inputTokens;
}
/**
* Sets the number of input tokens.
*
* @param inputTokens The number of input tokens
*/
public void setInputTokens(int inputTokens) {
this.inputTokens = inputTokens;
}
/**
* Gets the number of output tokens.
*
* @return The number of output tokens
*/
public int getOutputTokens() {
return outputTokens;
}
/**
* Sets the number of output tokens.
*
* @param outputTokens The number of output tokens
*/
public void setOutputTokens(int outputTokens) {
this.outputTokens = outputTokens;
}
/**
* Gets the number of cache read input tokens.
*
* @return The number of cache read input tokens
*/
public int getCacheReadInputTokens() {
return cacheReadInputTokens;
}
/**
* Sets the number of cache read input tokens.
*
* @param cacheReadInputTokens The number of cache read input tokens
*/
public void setCacheReadInputTokens(int cacheReadInputTokens) {
this.cacheReadInputTokens = cacheReadInputTokens;
}
/**
* Gets the number of cache creation input tokens.
*
* @return The number of cache creation input tokens
*/
public int getCacheCreationInputTokens() {
return cacheCreationInputTokens;
}
/**
* Sets the number of cache creation input tokens.
*
* @param cacheCreationInputTokens The number of cache creation input tokens
*/
public void setCacheCreationInputTokens(int cacheCreationInputTokens) {
this.cacheCreationInputTokens = cacheCreationInputTokens;
}
/**
* Gets the number of web search requests.
*
* @return The number of web search requests
*/
public int getWebSearchRequests() {
return webSearchRequests;
}
/**
* Sets the number of web search requests.
*
* @param webSearchRequests The number of web search requests
*/
public void setWebSearchRequests(int webSearchRequests) {
this.webSearchRequests = webSearchRequests;
}
/**
* Gets the context window size.
*
* @return The context window size
*/
public int getContextWindow() {
return contextWindow;
}
/**
* Sets the context window size.
*
* @param contextWindow The context window size
*/
public void setContextWindow(int contextWindow) {
this.contextWindow = contextWindow;
}
}

View File

@@ -0,0 +1,56 @@
package com.alibaba.qwen.code.cli.protocol.data;
/**
* Represents different permission modes for the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public enum PermissionMode {
/**
* Default permission mode.
*/
DEFAULT("default"),
/**
* Plan permission mode.
*/
PLAN("plan"),
/**
* Auto-edit permission mode.
*/
AUTO_EDIT("auto-edit"),
/**
* YOLO permission mode.
*/
YOLO("yolo");
private final String value;
PermissionMode(String value) {
this.value = value;
}
/**
* Gets the string value of the permission mode.
*
* @return The string value of the permission mode
*/
public String getValue() {
return value;
}
/**
* Gets the permission mode from its string value.
*
* @param value The string value
* @return The corresponding permission mode
*/
public static PermissionMode fromValue(String value) {
for (PermissionMode mode : PermissionMode.values()) {
if (mode.value.equals(value)) {
return mode;
}
}
throw new IllegalArgumentException("Unknown permission mode: " + value);
}
}

View File

@@ -0,0 +1,137 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents usage information for a message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class Usage {
/**
* Number of input tokens.
*/
@JSONField(name = "input_tokens")
private Integer inputTokens;
/**
* Number of output tokens.
*/
@JSONField(name = "output_tokens")
private Integer outputTokens;
/**
* Number of cache creation input tokens.
*/
@JSONField(name = "cache_creation_input_tokens")
private Integer cacheCreationInputTokens;
/**
* Number of cache read input tokens.
*/
@JSONField(name = "cache_read_input_tokens")
private Integer cacheReadInputTokens;
/**
* Total number of tokens.
*/
@JSONField(name = "total_tokens")
private Integer totalTokens;
/**
* Gets the number of input tokens.
*
* @return The number of input tokens
*/
public Integer getInputTokens() {
return inputTokens;
}
/**
* Sets the number of input tokens.
*
* @param inputTokens The number of input tokens
*/
public void setInputTokens(Integer inputTokens) {
this.inputTokens = inputTokens;
}
/**
* Gets the number of output tokens.
*
* @return The number of output tokens
*/
public Integer getOutputTokens() {
return outputTokens;
}
/**
* Sets the number of output tokens.
*
* @param outputTokens The number of output tokens
*/
public void setOutputTokens(Integer outputTokens) {
this.outputTokens = outputTokens;
}
/**
* Gets the number of cache creation input tokens.
*
* @return The number of cache creation input tokens
*/
public Integer getCacheCreationInputTokens() {
return cacheCreationInputTokens;
}
/**
* Sets the number of cache creation input tokens.
*
* @param cacheCreationInputTokens The number of cache creation input tokens
*/
public void setCacheCreationInputTokens(Integer cacheCreationInputTokens) {
this.cacheCreationInputTokens = cacheCreationInputTokens;
}
/**
* Gets the number of cache read input tokens.
*
* @return The number of cache read input tokens
*/
public Integer getCacheReadInputTokens() {
return cacheReadInputTokens;
}
/**
* Sets the number of cache read input tokens.
*
* @param cacheReadInputTokens The number of cache read input tokens
*/
public void setCacheReadInputTokens(Integer cacheReadInputTokens) {
this.cacheReadInputTokens = cacheReadInputTokens;
}
/**
* Gets the total number of tokens.
*
* @return The total number of tokens
*/
public Integer getTotalTokens() {
return totalTokens;
}
/**
* Sets the total number of tokens.
*
* @param totalTokens The total number of tokens
*/
public void setTotalTokens(Integer totalTokens) {
this.totalTokens = totalTokens;
}
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@@ -0,0 +1,46 @@
package com.alibaba.qwen.code.cli.protocol.data.behavior;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents an allow behavior that permits an operation.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "operation", typeName = "allow")
public class Allow extends Behavior {
/**
* Creates a new Allow instance and sets the behavior to allow.
*/
public Allow() {
super();
this.behavior = Operation.allow;
}
/**
* Updated input for the operation.
*/
Map<String, Object> updatedInput;
/**
* Gets the updated input.
*
* @return The updated input
*/
public Map<String, Object> getUpdatedInput() {
return updatedInput;
}
/**
* Sets the updated input.
*
* @param updatedInput The updated input
* @return This instance for method chaining
*/
public Allow setUpdatedInput(Map<String, Object> updatedInput) {
this.updatedInput = updatedInput;
return this;
}
}

View File

@@ -0,0 +1,62 @@
package com.alibaba.qwen.code.cli.protocol.data.behavior;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Base class for behavior objects that define how the CLI should handle requests.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "operation", typeName = "Behavior", seeAlso = {Allow.class, Deny.class})
public class Behavior {
/**
* The behavior operation (allow or deny).
*/
Operation behavior;
/**
* Gets the behavior operation.
*
* @return The behavior operation
*/
public Operation getBehavior() {
return behavior;
}
/**
* Sets the behavior operation.
*
* @param behavior The behavior operation
*/
public void setBehavior(Operation behavior) {
this.behavior = behavior;
}
/**
* Represents the type of operation.
*/
public enum Operation {
/**
* Allow the operation.
*/
allow,
/**
* Deny the operation.
*/
deny
}
/**
* Gets the default behavior (deny with message).
*
* @return The default behavior
*/
public static Behavior defaultBehavior() {
return denyBehavior();
}
public static Behavior denyBehavior() {
return new Deny().setMessage("Default Behavior Permission denied");
}
}

View File

@@ -0,0 +1,45 @@
package com.alibaba.qwen.code.cli.protocol.data.behavior;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a deny behavior that rejects an operation.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "operation", typeName = "deny")
public class Deny extends Behavior {
/**
* Creates a new Deny instance and sets the behavior to deny.
*/
public Deny() {
super();
this.behavior = Operation.deny;
}
/**
* The message explaining why the operation was denied.
*/
String message;
/**
* Gets the denial message.
*
* @return The denial message
*/
public String getMessage() {
return message;
}
/**
* Sets the denial message.
*
* @param message The denial message
* @return This instance for method chaining
*/
public Deny setMessage(String message) {
this.message = message;
return this;
}
}

View File

@@ -0,0 +1,23 @@
package com.alibaba.qwen.code.cli.protocol.message;
/**
* Represents a message in the Qwen Code protocol.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public interface Message {
/**
* Gets the type of the message.
*
* @return The type of the message
*/
String getType();
/**
* Gets the ID of the message.
*
* @return The ID of the message
*/
String getMessageId();
}

View File

@@ -0,0 +1,64 @@
package com.alibaba.qwen.code.cli.protocol.message;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Base class for messages in the Qwen Code protocol.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(alphabetic = false, typeKey = "type", typeName = "MessageBase")
public class MessageBase implements Message{
/**
* The type of the message.
*/
protected String type;
/**
* The ID of the message.
*/
@JSONField(name = "message_id")
protected String messageId;
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
/** {@inheritDoc} */
@Override
public String getType() {
return type;
}
/**
* Sets the type of the message.
*
* @param type The type of the message
*/
public void setType(String type) {
this.type = type;
}
/** {@inheritDoc} */
@Override
public String getMessageId() {
return messageId;
}
/**
* Sets the ID of the message.
*
* @param messageId The ID of the message
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
}

View File

@@ -0,0 +1,332 @@
package com.alibaba.qwen.code.cli.protocol.message;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.CLIPermissionDenial;
import com.alibaba.qwen.code.cli.protocol.data.ExtendedUsage;
import com.alibaba.qwen.code.cli.protocol.data.Usage;
/**
* Represents a result message from the SDK.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "result")
public class SDKResultMessage extends MessageBase {
/**
* The subtype of the result.
*/
private String subtype; // 'error_max_turns' | 'error_during_execution'
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* Whether the result represents an error.
*/
@JSONField(name = "is_error")
private boolean isError = true;
/**
* Duration in milliseconds.
*/
@JSONField(name = "duration_ms")
private Long durationMs;
/**
* API duration in milliseconds.
*/
@JSONField(name = "duration_api_ms")
private Long durationApiMs;
/**
* Number of turns.
*/
@JSONField(name = "num_turns")
private Integer numTurns;
/**
* Usage information.
*/
private ExtendedUsage usage;
/**
* Model usage information.
*/
private Map<String, Usage> modelUsage;
/**
* List of permission denials.
*/
@JSONField(name = "permission_denials")
private List<CLIPermissionDenial> permissionDenials;
/**
* Error information.
*/
private Error error;
/**
* Creates a new SDKResultMessage instance and sets the type to "result".
*/
public SDKResultMessage() {
super();
this.type = "result";
}
/**
* Gets the subtype of the result.
*
* @return The subtype of the result
*/
public String getSubtype() {
return subtype;
}
/**
* Sets the subtype of the result.
*
* @param subtype The subtype of the result
*/
public void setSubtype(String subtype) {
this.subtype = subtype;
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Checks if the result represents an error.
*
* @return Whether the result represents an error
*/
public boolean isError() {
return isError;
}
/**
* Sets whether the result represents an error.
*
* @param error Whether the result represents an error
*/
public void setError(boolean error) {
isError = error;
}
/**
* Gets the duration in milliseconds.
*
* @return The duration in milliseconds
*/
public Long getDurationMs() {
return durationMs;
}
/**
* Sets the duration in milliseconds.
*
* @param durationMs The duration in milliseconds
*/
public void setDurationMs(Long durationMs) {
this.durationMs = durationMs;
}
/**
* Gets the API duration in milliseconds.
*
* @return The API duration in milliseconds
*/
public Long getDurationApiMs() {
return durationApiMs;
}
/**
* Sets the API duration in milliseconds.
*
* @param durationApiMs The API duration in milliseconds
*/
public void setDurationApiMs(Long durationApiMs) {
this.durationApiMs = durationApiMs;
}
/**
* Gets the number of turns.
*
* @return The number of turns
*/
public Integer getNumTurns() {
return numTurns;
}
/**
* Sets the number of turns.
*
* @param numTurns The number of turns
*/
public void setNumTurns(Integer numTurns) {
this.numTurns = numTurns;
}
/**
* Gets the usage information.
*
* @return The usage information
*/
public ExtendedUsage getUsage() {
return usage;
}
/**
* Sets the usage information.
*
* @param usage The usage information
*/
public void setUsage(ExtendedUsage usage) {
this.usage = usage;
}
/**
* Gets the model usage information.
*
* @return The model usage information
*/
public Map<String, Usage> getModelUsage() {
return modelUsage;
}
/**
* Sets the model usage information.
*
* @param modelUsage The model usage information
*/
public void setModelUsage(Map<String, Usage> modelUsage) {
this.modelUsage = modelUsage;
}
/**
* Gets the list of permission denials.
*
* @return The list of permission denials
*/
public List<CLIPermissionDenial> getPermissionDenials() {
return permissionDenials;
}
/**
* Sets the list of permission denials.
*
* @param permissionDenials The list of permission denials
*/
public void setPermissionDenials(List<CLIPermissionDenial> permissionDenials) {
this.permissionDenials = permissionDenials;
}
/**
* Gets the error information.
*
* @return The error information
*/
public Error getError() {
return error;
}
/**
* Sets the error information.
*
* @param error The error information
*/
public void setError(Error error) {
this.error = error;
}
/**
* Represents error information.
*/
public static class Error {
/**
* Error type.
*/
private String type;
/**
* Error message.
*/
private String message;
/**
* Gets the error type.
*
* @return The error type
*/
public String getType() {
return type;
}
/**
* Sets the error type.
*
* @param type The error type
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the error message.
*
* @return The error message
*/
public String getMessage() {
return message;
}
/**
* Sets the error message.
*
* @param message The error message
*/
public void setMessage(String message) {
this.message = message;
}
}
}

View File

@@ -0,0 +1,486 @@
package com.alibaba.qwen.code.cli.protocol.message;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a system message from the SDK.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "system")
public class SDKSystemMessage extends MessageBase {
/**
* The subtype of the system message.
*/
private String subtype;
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* Additional data.
*/
private Object data;
/**
* Current working directory.
*/
private String cwd;
/**
* List of available tools.
*/
private List<String> tools;
/**
* List of MCP servers.
*/
@JSONField(name = "mcp_servers")
private List<McpServer> mcpServers;
/**
* Model information.
*/
private String model;
/**
* Permission mode.
*/
@JSONField(name = "permission_mode")
private String permissionMode;
/**
* Available slash commands.
*/
@JSONField(name = "slash_commands")
private List<String> slashCommands;
/**
* Qwen Code version.
*/
@JSONField(name = "qwen_code_version")
private String qwenCodeVersion;
/**
* Output style.
*/
@JSONField(name = "output_style")
private String outputStyle;
/**
* Available agents.
*/
private List<String> agents;
/**
* Available skills.
*/
private List<String> skills;
/**
* Capabilities information.
*/
private Map<String, Object> capabilities;
/**
* Compact metadata.
*/
@JSONField(name = "compact_metadata")
private CompactMetadata compactMetadata;
/**
* Creates a new SDKSystemMessage instance and sets the type to "system".
*/
public SDKSystemMessage() {
super();
this.type = "system";
}
/**
* Gets the subtype of the system message.
*
* @return The subtype of the system message
*/
public String getSubtype() {
return subtype;
}
/**
* Sets the subtype of the system message.
*
* @param subtype The subtype of the system message
*/
public void setSubtype(String subtype) {
this.subtype = subtype;
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Gets the additional data.
*
* @return The additional data
*/
public Object getData() {
return data;
}
/**
* Sets the additional data.
*
* @param data The additional data
*/
public void setData(Object data) {
this.data = data;
}
/**
* Gets the current working directory.
*
* @return The current working directory
*/
public String getCwd() {
return cwd;
}
/**
* Sets the current working directory.
*
* @param cwd The current working directory
*/
public void setCwd(String cwd) {
this.cwd = cwd;
}
/**
* Gets the list of available tools.
*
* @return The list of available tools
*/
public List<String> getTools() {
return tools;
}
/**
* Sets the list of available tools.
*
* @param tools The list of available tools
*/
public void setTools(List<String> tools) {
this.tools = tools;
}
/**
* Gets the list of MCP servers.
*
* @return The list of MCP servers
*/
public List<McpServer> getMcpServers() {
return mcpServers;
}
/**
* Sets the list of MCP servers.
*
* @param mcpServers The list of MCP servers
*/
public void setMcpServers(List<McpServer> mcpServers) {
this.mcpServers = mcpServers;
}
/**
* Gets the model information.
*
* @return The model information
*/
public String getModel() {
return model;
}
/**
* Sets the model information.
*
* @param model The model information
*/
public void setModel(String model) {
this.model = model;
}
/**
* Gets the permission mode.
*
* @return The permission mode
*/
public String getPermissionMode() {
return permissionMode;
}
/**
* Sets the permission mode.
*
* @param permissionMode The permission mode
*/
public void setPermissionMode(String permissionMode) {
this.permissionMode = permissionMode;
}
/**
* Gets the available slash commands.
*
* @return The available slash commands
*/
public List<String> getSlashCommands() {
return slashCommands;
}
/**
* Sets the available slash commands.
*
* @param slashCommands The available slash commands
*/
public void setSlashCommands(List<String> slashCommands) {
this.slashCommands = slashCommands;
}
/**
* Gets the Qwen Code version.
*
* @return The Qwen Code version
*/
public String getQwenCodeVersion() {
return qwenCodeVersion;
}
/**
* Sets the Qwen Code version.
*
* @param qwenCodeVersion The Qwen Code version
*/
public void setQwenCodeVersion(String qwenCodeVersion) {
this.qwenCodeVersion = qwenCodeVersion;
}
/**
* Gets the output style.
*
* @return The output style
*/
public String getOutputStyle() {
return outputStyle;
}
/**
* Sets the output style.
*
* @param outputStyle The output style
*/
public void setOutputStyle(String outputStyle) {
this.outputStyle = outputStyle;
}
/**
* Gets the available agents.
*
* @return The available agents
*/
public List<String> getAgents() {
return agents;
}
/**
* Sets the available agents.
*
* @param agents The available agents
*/
public void setAgents(List<String> agents) {
this.agents = agents;
}
/**
* Gets the available skills.
*
* @return The available skills
*/
public List<String> getSkills() {
return skills;
}
/**
* Sets the available skills.
*
* @param skills The available skills
*/
public void setSkills(List<String> skills) {
this.skills = skills;
}
/**
* Gets the capabilities information.
*
* @return The capabilities information
*/
public Map<String, Object> getCapabilities() {
return capabilities;
}
/**
* Sets the capabilities information.
*
* @param capabilities The capabilities information
*/
public void setCapabilities(Map<String, Object> capabilities) {
this.capabilities = capabilities;
}
/**
* Gets the compact metadata.
*
* @return The compact metadata
*/
public CompactMetadata getCompactMetadata() {
return compactMetadata;
}
/**
* Sets the compact metadata.
*
* @param compactMetadata The compact metadata
*/
public void setCompactMetadata(CompactMetadata compactMetadata) {
this.compactMetadata = compactMetadata;
}
/**
* Represents MCP server information.
*/
public static class McpServer {
/**
* Server name.
*/
private String name;
/**
* Server status.
*/
private String status;
/**
* Gets the server name.
*
* @return The server name
*/
public String getName() {
return name;
}
/**
* Sets the server name.
*
* @param name The server name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the server status.
*
* @return The server status
*/
public String getStatus() {
return status;
}
/**
* Sets the server status.
*
* @param status The server status
*/
public void setStatus(String status) {
this.status = status;
}
}
/**
* Represents compact metadata.
*/
public static class CompactMetadata {
/**
* Trigger information.
*/
private String trigger;
/**
* Pre-tokens information.
*/
@JSONField(name = "pre_tokens")
private Integer preTokens;
/**
* Gets the trigger information.
*
* @return The trigger information
*/
public String getTrigger() {
return trigger;
}
/**
* Sets the trigger information.
*
* @param trigger The trigger information
*/
public void setTrigger(String trigger) {
this.trigger = trigger;
}
/**
* Gets the pre-tokens information.
*
* @return The pre-tokens information
*/
public Integer getPreTokens() {
return preTokens;
}
/**
* Sets the pre-tokens information.
*
* @param preTokens The pre-tokens information
*/
public void setPreTokens(Integer preTokens) {
this.preTokens = preTokens;
}
}
}

View File

@@ -0,0 +1,196 @@
package com.alibaba.qwen.code.cli.protocol.message;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a user message in the SDK protocol.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "user")
public class SDKUserMessage extends MessageBase {
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* The API user message.
*/
private final APIUserMessage message = new APIUserMessage();
/**
* The parent tool use ID.
*/
@JSONField(name = "parent_tool_use_id")
private String parentToolUseId;
/**
* Additional options.
*/
private Map<String, String> options;
/**
* Creates a new SDKUserMessage instance and sets the type to "user".
*/
public SDKUserMessage() {
super();
this.setType("user");
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
* @return This instance for method chaining
*/
public SDKUserMessage setSessionId(String sessionId) {
this.sessionId = sessionId;
return this;
}
/**
* Sets the content of the message.
*
* @param content The content of the message
* @return This instance for method chaining
*/
public SDKUserMessage setContent(String content) {
message.setContent(content);
return this;
}
/**
* Gets the content of the message.
*
* @return The content of the message
*/
public String getContent() {
return message.getContent();
}
/**
* Gets the parent tool use ID.
*
* @return The parent tool use ID
*/
public String getParentToolUseId() {
return parentToolUseId;
}
/**
* Sets the parent tool use ID.
*
* @param parentToolUseId The parent tool use ID
* @return This instance for method chaining
*/
public SDKUserMessage setParentToolUseId(String parentToolUseId) {
this.parentToolUseId = parentToolUseId;
return this;
}
/**
* Gets the additional options.
*
* @return The additional options
*/
public Map<String, String> getOptions() {
return options;
}
/**
* Sets the additional options.
*
* @param options The additional options
* @return This instance for method chaining
*/
public SDKUserMessage setOptions(Map<String, String> options) {
this.options = options;
return this;
}
/**
* Represents the API user message.
*/
public static class APIUserMessage {
/**
* User role.
*/
private String role = "user";
/**
* Message content.
*/
private String content;
/**
* Gets the user role.
*
* @return The user role
*/
public String getRole() {
return role;
}
/**
* Sets the user role.
*
* @param role The user role
*/
public void setRole(String role) {
this.role = role;
}
/**
* Gets the message content.
*
* @return The message content
*/
public String getContent() {
return content;
}
/**
* Sets the message content.
*
* @param content The message content
*/
public void setContent(String content) {
this.content = content;
}
}
}

View File

@@ -0,0 +1,172 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant;
import java.util.List;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.qwen.code.cli.protocol.data.Usage;
import com.alibaba.qwen.code.cli.protocol.message.assistant.block.ContentBlock;
/**
* Represents an API assistant message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class APIAssistantMessage {
/**
* Message ID.
*/
private String id;
/**
* Message type.
*/
private String type = "message";
/**
* Message role.
*/
private String role = "assistant";
/**
* Message model.
*/
private String model;
/**
* Message content.
*/
private List<ContentBlock<?>> content;
/**
* Stop reason.
*/
@JSONField(name = "stop_reason")
private String stopReason;
/**
* Usage information.
*/
private Usage usage;
/**
* Gets the message ID.
*
* @return The message ID
*/
public String getId() {
return id;
}
/**
* Sets the message ID.
*
* @param id The message ID
*/
public void setId(String id) {
this.id = id;
}
/**
* Gets the message type.
*
* @return The message type
*/
public String getType() {
return type;
}
/**
* Sets the message type.
*
* @param type The message type
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the message role.
*
* @return The message role
*/
public String getRole() {
return role;
}
/**
* Sets the message role.
*
* @param role The message role
*/
public void setRole(String role) {
this.role = role;
}
/**
* Gets the message model.
*
* @return The message model
*/
public String getModel() {
return model;
}
/**
* Sets the message model.
*
* @param model The message model
*/
public void setModel(String model) {
this.model = model;
}
/**
* Gets the stop reason.
*
* @return The stop reason
*/
public String getStopReason() {
return stopReason;
}
/**
* Sets the stop reason.
*
* @param stopReason The stop reason
*/
public void setStopReason(String stopReason) {
this.stopReason = stopReason;
}
/**
* Gets the usage information.
*
* @return The usage information
*/
public Usage getUsage() {
return usage;
}
/**
* Sets the usage information.
*
* @param usage The usage information
*/
public void setUsage(Usage usage) {
this.usage = usage;
}
/**
* Gets the message content.
*
* @return The message content
*/
public List<ContentBlock<?>> getContent() {
return content;
}
/**
* Sets the message content.
*
* @param content The message content
*/
public void setContent(List<ContentBlock<?>> content) {
this.content = content;
}
}

View File

@@ -0,0 +1,121 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
/**
* Represents an SDK assistant message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "assistant")
public class SDKAssistantMessage extends MessageBase {
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* The API assistant message.
*/
private APIAssistantMessage message;
/**
* The parent tool use ID.
*/
@JSONField(name = "parent_tool_use_id")
private String parentToolUseId;
/**
* Creates a new SDKAssistantMessage instance and sets the type to "assistant".
*/
public SDKAssistantMessage() {
super();
this.type = "assistant";
}
/** {@inheritDoc} */
@Override
public String getMessageId() {
return this.getUuid();
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Gets the API assistant message.
*
* @return The API assistant message
*/
public APIAssistantMessage getMessage() {
return message;
}
/**
* Sets the API assistant message.
*
* @param message The API assistant message
*/
public void setMessage(APIAssistantMessage message) {
this.message = message;
}
/**
* Gets the parent tool use ID.
*
* @return The parent tool use ID
*/
public String getParentToolUseId() {
return parentToolUseId;
}
/**
* Sets the parent tool use ID.
*
* @param parentToolUseId The parent tool use ID
*/
public void setParentToolUseId(String parentToolUseId) {
this.parentToolUseId = parentToolUseId;
}
}

View File

@@ -0,0 +1,116 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
import com.alibaba.qwen.code.cli.protocol.message.assistant.event.StreamEvent;
/**
* Represents a partial assistant message during streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "stream_event")
public class SDKPartialAssistantMessage extends MessageBase {
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* The stream event.
*/
private StreamEvent event;
/**
* The parent tool use ID.
*/
@JSONField(name = "parent_tool_use_id")
private String parentToolUseId;
/**
* Creates a new SDKPartialAssistantMessage instance and sets the type to "stream_event".
*/
public SDKPartialAssistantMessage() {
super();
this.type = "stream_event";
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Gets the stream event.
*
* @return The stream event
*/
public StreamEvent getEvent() {
return event;
}
/**
* Sets the stream event.
*
* @param event The stream event
*/
public void setEvent(StreamEvent event) {
this.event = event;
}
/**
* Gets the parent tool use ID.
*
* @return The parent tool use ID
*/
public String getParentToolUseId() {
return parentToolUseId;
}
/**
* Sets the parent tool use ID.
*
* @param parentToolUseId The parent tool use ID
*/
public void setParentToolUseId(String parentToolUseId) {
this.parentToolUseId = parentToolUseId;
}
}

View File

@@ -0,0 +1,59 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents an annotation for a content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class Annotation {
/**
* The annotation type.
*/
@JSONField(name = "type")
private String type;
/**
* The annotation value.
*/
@JSONField(name = "value")
private String value;
/**
* Gets the annotation type.
*
* @return The annotation type
*/
public String getType() {
return type;
}
/**
* Sets the annotation type.
*
* @param type The annotation type
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the annotation value.
*
* @return The annotation value
*/
public String getValue() {
return value;
}
/**
* Sets the annotation value.
*
* @param value The annotation value
*/
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,87 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import java.util.List;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
/**
* Abstract base class for content blocks in assistant messages.
*
* @param <C> The type of content
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "ContentBlock", seeAlso = { TextBlock.class, ToolResultBlock.class, ThinkingBlock.class, ToolUseBlock.class })
public abstract class ContentBlock<C> implements AssistantContent<C> {
/**
* The type of the content block.
*/
protected String type;
/**
* List of annotations.
*/
protected List<Annotation> annotations;
/**
* The message ID.
*/
protected String messageId;
/** {@inheritDoc} */
@Override
public String getType() {
return type;
}
/**
* Sets the type of the content block.
*
* @param type The type of the content block
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the list of annotations.
*
* @return The list of annotations
*/
public List<Annotation> getAnnotations() {
return annotations;
}
/**
* Sets the list of annotations.
*
* @param annotations The list of annotations
*/
public void setAnnotations(List<Annotation> annotations) {
this.annotations = annotations;
}
/** {@inheritDoc} */
@Override
public String getMessageId() {
return messageId;
}
/**
* Sets the message ID.
*
* @param messageId The message ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@@ -0,0 +1,42 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
/**
* Represents a text content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "text")
public class TextBlock extends ContentBlock<String> implements TextAssistantContent {
/**
* The text content.
*/
private String text;
/**
* Gets the text content.
*
* @return The text content
*/
public String getText() {
return text;
}
/**
* Sets the text content.
*
* @param text The text content
*/
public void setText(String text) {
this.text = text;
}
/** {@inheritDoc} */
@Override
public String getContentOfAssistant() {
return text;
}
}

View File

@@ -0,0 +1,64 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
/**
* Represents a thinking content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "thinking")
public class ThinkingBlock extends ContentBlock<String> implements ThingkingAssistantContent {
/**
* The thinking content.
*/
private String thinking;
/**
* The signature.
*/
private String signature;
/**
* Gets the thinking content.
*
* @return The thinking content
*/
public String getThinking() {
return thinking;
}
/**
* Sets the thinking content.
*
* @param thinking The thinking content
*/
public void setThinking(String thinking) {
this.thinking = thinking;
}
/**
* Gets the signature.
*
* @return The signature
*/
public String getSignature() {
return signature;
}
/**
* Sets the signature.
*
* @param signature The signature
*/
public void setSignature(String signature) {
this.signature = signature;
}
/** {@inheritDoc} */
@Override
public String getContentOfAssistant() {
return thinking;
}
}

View File

@@ -0,0 +1,92 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent;
/**
* Represents a tool result content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "tool_result")
public class ToolResultBlock extends ContentBlock<String> implements ToolResultAssistantContent {
/**
* The tool use ID.
*/
@JSONField(name = "tool_use_id")
private String toolUseId;
/**
* The result content.
*/
@JSONField(name = "content")
private String content;
/**
* Whether the result is an error.
*/
@JSONField(name = "is_error")
private Boolean isError;
/**
* Gets the tool use ID.
*
* @return The tool use ID
*/
public String getToolUseId() {
return toolUseId;
}
/**
* Sets the tool use ID.
*
* @param toolUseId The tool use ID
*/
public void setToolUseId(String toolUseId) {
this.toolUseId = toolUseId;
}
/**
* Gets the result content.
*
* @return The result content
*/
public String getContent() {
return content;
}
/**
* Sets the result content.
*
* @param content The result content
*/
public void setContent(String content) {
this.content = content;
}
/**
* Gets whether the result is an error.
*
* @return Whether the result is an error
*/
public Boolean getIsError() {
return isError;
}
/**
* Sets whether the result is an error.
*
* @param isError Whether the result is an error
*/
public void setIsError(Boolean isError) {
this.isError = isError;
}
/** {@inheritDoc} */
@Override
public String getContentOfAssistant() {
return content;
}
}

View File

@@ -0,0 +1,122 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
/**
* Represents a tool use content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "tool_use")
public class ToolUseBlock extends ContentBlock<Map<String, Object>> implements ToolUseAssistantContent {
/**
* The tool use ID.
*/
private String id;
/**
* The tool name.
*/
private String name;
/**
* The tool input.
*/
private Map<String, Object> input;
/**
* List of annotations.
*/
private List<Annotation> annotations;
/**
* Creates a new ToolUseBlock instance.
*/
public ToolUseBlock() {}
/**
* Gets the tool use ID.
*
* @return The tool use ID
*/
public String getId() {
return id;
}
/**
* Sets the tool use ID.
*
* @param id The tool use ID
*/
public void setId(String id) {
this.id = id;
}
/**
* Gets the tool name.
*
* @return The tool name
*/
public String getName() {
return name;
}
/**
* Sets the tool name.
*
* @param name The tool name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the tool input.
*
* @return The tool input
*/
public Map<String, Object> getInput() {
return input;
}
/**
* Sets the tool input.
*
* @param input The tool input
*/
public void setInput(Map<String, Object> input) {
this.input = input;
}
/**
* Gets the list of annotations.
*
* @return The list of annotations
*/
public List<Annotation> getAnnotations() {
return annotations;
}
/**
* {@inheritDoc}
*
* Sets the list of annotations.
*/
@Override
public void setAnnotations(List<Annotation> annotations) {
this.annotations = annotations;
}
/**
* {@inheritDoc}
*
* Gets the content of the assistant.
*/
@Override
public Map<String, Object> getContentOfAssistant() {
return Collections.emptyMap();
}
}

View File

@@ -0,0 +1,224 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import java.util.Map;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
/**
* Represents a content block delta event during streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "content_block_delta")
public class ContentBlockDeltaEvent extends StreamEvent {
/**
* The index of the content block.
*/
private int index;
/**
* The content block delta.
*/
private ContentBlockDelta<?> delta;
/**
* Gets the index of the content block.
*
* @return The index of the content block
*/
public int getIndex() {
return index;
}
/**
* Sets the index of the content block.
*
* @param index The index of the content block
*/
public void setIndex(int index) {
this.index = index;
}
/**
* Gets the content block delta.
*
* @return The content block delta
*/
public ContentBlockDelta<?> getDelta() {
return delta;
}
/**
* Sets the content block delta.
*
* @param delta The content block delta
*/
public void setDelta(ContentBlockDelta<?> delta) {
this.delta = delta;
}
/**
* Abstract base class for content block deltas.
*
* @param <C> The type of content
*/
@JSONType(typeKey = "type", typeName = "ContentBlockDelta",
seeAlso = {ContentBlockDeltaText.class, ContentBlockDeltaThinking.class, ContentBlockDeltaInputJson.class})
public abstract static class ContentBlockDelta<C> implements AssistantContent<C> {
/**
* The type of the content block delta.
*/
protected String type;
/**
* The message ID.
*/
protected String messageId;
@Override
public String getType() {
return type;
}
/**
* Sets the type of the content block delta.
*
* @param type The type of the content block delta
*/
public void setType(String type) {
this.type = type;
}
@Override
public String getMessageId() {
return messageId;
}
/**
* Sets the message ID.
*
* @param messageId The message ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
public String toString() {
return JSON.toJSONString(this);
}
}
/**
* Represents a text delta.
*/
@JSONType(typeKey = "type", typeName = "text_delta")
public static class ContentBlockDeltaText extends ContentBlockDelta<String> implements TextAssistantContent {
/**
* The text content.
*/
private String text;
/**
* Gets the text content.
*
* @return The text content
*/
public String getText() {
return text;
}
/**
* Sets the text content.
*
* @param text The text content
*/
public void setText(String text) {
this.text = text;
}
@Override
public String getContentOfAssistant() {
return text;
}
}
/**
* Represents a thinking delta.
*/
@JSONType(typeKey = "type", typeName = "thinking_delta")
public static class ContentBlockDeltaThinking extends ContentBlockDelta<String> implements ThingkingAssistantContent {
/**
* The thinking content.
*/
private String thinking;
/**
* Gets the thinking content.
*
* @return The thinking content
*/
public String getThinking() {
return thinking;
}
/**
* Sets the thinking content.
*
* @param thinking The thinking content
*/
public void setThinking(String thinking) {
this.thinking = thinking;
}
@Override
public String getContentOfAssistant() {
return thinking;
}
}
/**
* Represents an input JSON delta.
*/
@JSONType(typeKey = "type", typeName = "input_json_delta")
public static class ContentBlockDeltaInputJson extends ContentBlockDelta<Map<String, Object>> implements ToolUseAssistantContent {
/**
* The partial JSON content.
*/
@JSONField(name = "partial_json")
private String partialJson;
/**
* Gets the partial JSON content.
*
* @return The partial JSON content
*/
public String getPartialJson() {
return partialJson;
}
/**
* Sets the partial JSON content.
*
* @param partialJson The partial JSON content
*/
public void setPartialJson(String partialJson) {
this.partialJson = partialJson;
}
@Override
public Map<String, Object> getContentOfAssistant() {
return getInput();
}
@Override
public Map<String, Object> getInput() {
return JSON.parseObject(partialJson, new TypeReference<Map<String, Object>>() {});
}
}
}

View File

@@ -0,0 +1,25 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.assistant.block.ContentBlock;
/**
* Represents a content block start event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "content_block_start")
public class ContentBlockStartEvent extends StreamEvent{
/**
* The index of the content block.
*/
private int index;
/**
* The content block that is starting.
*/
@JSONField(name = "content_block")
private ContentBlock contentBlock;
}

View File

@@ -0,0 +1,35 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a content block stop event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "content_block_stop")
public class ContentBlockStopEvent extends StreamEvent{
/**
* The index of the content block.
*/
Long index;
/**
* Gets the index of the content block.
*
* @return The index of the content block
*/
public Long getIndex() {
return index;
}
/**
* Sets the index of the content block.
*
* @param index The index of the content block
*/
public void setIndex(Long index) {
this.index = index;
}
}

View File

@@ -0,0 +1,107 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a message start event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeName = "message_start")
public class MessageStartStreamEvent extends StreamEvent{
/**
* The message that is starting.
*/
private Message message;
/**
* Represents the message information.
*/
public static class Message {
/**
* Message ID.
*/
private String id;
/**
* Message role.
*/
private String role;
/**
* Message model.
*/
private String model;
/**
* Gets the message ID.
*
* @return The message ID
*/
public String getId() {
return id;
}
/**
* Sets the message ID.
*
* @param id The message ID
*/
public void setId(String id) {
this.id = id;
}
/**
* Gets the message role.
*
* @return The message role
*/
public String getRole() {
return role;
}
/**
* Sets the message role.
*
* @param role The message role
*/
public void setRole(String role) {
this.role = role;
}
/**
* Gets the message model.
*
* @return The message model
*/
public String getModel() {
return model;
}
/**
* Sets the message model.
*
* @param model The message model
*/
public void setModel(String model) {
this.model = model;
}
}
/**
* Gets the message that is starting.
*
* @return The message that is starting
*/
public Message getMessage() {
return message;
}
/**
* Sets the message that is starting.
*
* @param message The message that is starting
*/
public void setMessage(Message message) {
this.message = message;
}
}

View File

@@ -0,0 +1,13 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a message stop event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeName = "message_stop")
public class MessageStopStreamEvent extends StreamEvent{
}

View File

@@ -0,0 +1,37 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Base class for stream events during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "StreamEvent",
seeAlso = {MessageStartStreamEvent.class, MessageStopStreamEvent.class, ContentBlockStartEvent.class, ContentBlockStopEvent.class,
ContentBlockDeltaEvent.class})
public class StreamEvent {
/**
* The type of the stream event.
*/
protected String type;
/**
* Gets the type of the stream event.
*
* @return The type of the stream event
*/
public String getType() {
return type;
}
/**
* Sets the type of the stream event.
*
* @param type The type of the stream event
*/
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,90 @@
package com.alibaba.qwen.code.cli.protocol.message.control;
import java.util.UUID;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload;
/**
* Represents a control request to the CLI.
*
* @param <R> The type of the request object
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "control_request")
public class CLIControlRequest<R extends ControlRequestPayload> extends MessageBase {
/**
* The ID of the request.
*/
@JSONField(name = "request_id")
private String requestId = UUID.randomUUID().toString();
/**
* The actual request object.
*/
private R request;
/**
* Creates a new CLIControlRequest instance and sets the type to "control_request".
*/
public CLIControlRequest() {
super();
type = "control_request";
}
/**
* Creates a new control request with the specified request object.
*
* @param request The request object
* @param <T> The type of the request object
* @return A new control request instance
*/
public static <T extends ControlRequestPayload> CLIControlRequest<T> create(T request) {
CLIControlRequest<T> controlRequest = new CLIControlRequest<>();
controlRequest.setRequest(request);
return controlRequest;
}
/**
* Gets the ID of the request.
*
* @return The ID of the request
*/
public String getRequestId() {
return requestId;
}
/**
* Sets the ID of the request.
*
* @param requestId The ID of the request
* @return This instance for method chaining
*/
public CLIControlRequest<R> setRequestId(String requestId) {
this.requestId = requestId;
return this;
}
/**
* Gets the actual request object.
*
* @return The actual request object
*/
public R getRequest() {
return request;
}
/**
* Sets the actual request object.
*
* @param request The actual request object
* @return This instance for method chaining
*/
public CLIControlRequest<R> setRequest(R request) {
this.request = request;
return this;
}
}

View File

@@ -0,0 +1,138 @@
package com.alibaba.qwen.code.cli.protocol.message.control;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
/**
* Represents a control response from the CLI.
*
* @param <R> The type of the response object
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "control_response")
public class CLIControlResponse<R> extends MessageBase {
/**
* The response object.
*/
private Response<R> response;
/**
* Creates a new CLIControlResponse instance and sets the type to "control_response".
*/
public CLIControlResponse() {
super();
this.type = "control_response";
}
/**
* Gets the response object.
*
* @return The response object
*/
public Response<R> getResponse() {
return response;
}
/**
* Sets the response object.
*
* @param response The response object
*/
public void setResponse(Response<R> response) {
this.response = response;
}
/**
* Creates a new response object.
*
* @return A new response object
*/
public Response<R> createResponse() {
Response<R> response = new Response<>();
this.setResponse(response);
return response;
}
/**
* Represents the response information.
*
* @param <R> The type of the response object
*/
public static class Response<R> {
/**
* The ID of the request.
*/
@JSONField(name = "request_id")
private String requestId;
/**
* The subtype of the response.
*/
private String subtype = "success";
/**
* The actual response.
*/
R response;
/**
* Gets the ID of the request.
*
* @return The ID of the request
*/
public String getRequestId() {
return requestId;
}
/**
* Sets the ID of the request.
*
* @param requestId The ID of the request
* @return This instance for method chaining
*/
public Response<R> setRequestId(String requestId) {
this.requestId = requestId;
return this;
}
/**
* Gets the subtype of the response.
*
* @return The subtype of the response
*/
public String getSubtype() {
return subtype;
}
/**
* Sets the subtype of the response.
*
* @param subtype The subtype of the response
* @return This instance for method chaining
*/
public Response<R> setSubtype(String subtype) {
this.subtype = subtype;
return this;
}
/**
* Gets the actual response.
*
* @return The actual response
*/
public R getResponse() {
return response;
}
/**
* Sets the actual response.
*
* @param response The actual response
* @return This instance for method chaining
*/
public Response<R> setResponse(R response) {
this.response = response;
return this;
}
}
}

View File

@@ -0,0 +1,45 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.InitializeConfig;
/**
* Represents a control initialize request to the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "initialize")
public class CLIControlInitializeRequest extends ControlRequestPayload {
public CLIControlInitializeRequest() {
super();
this.subtype = "initialize";
}
/**
* The initialization configuration.
*/
@JSONField(unwrapped = true)
InitializeConfig initializeConfig = new InitializeConfig();
/**
* Gets the initialization configuration.
*
* @return The initialization configuration
*/
public InitializeConfig getInitializeConfig() {
return initializeConfig;
}
/**
* Sets the initialization configuration.
*
* @param initializeConfig The initialization configuration
* @return This instance for method chaining
*/
public CLIControlInitializeRequest setInitializeConfig(InitializeConfig initializeConfig) {
this.initializeConfig = initializeConfig;
return this;
}
}

View File

@@ -0,0 +1,41 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.Capabilities;
/**
* Represents a control initialize response from the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "initialize")
public class CLIControlInitializeResponse extends ControlResponsePayload {
public CLIControlInitializeResponse() {
super();
this.subtype = "initialize";
}
/**
* The capabilities' information.
*/
Capabilities capabilities;
/**
* Gets the capabilities information.
*
* @return The capabilities information
*/
public Capabilities getCapabilities() {
return capabilities;
}
/**
* Sets the capabilities information.
*
* @param capabilities The capabilities information
*/
public void setCapabilities(Capabilities capabilities) {
this.capabilities = capabilities;
}
}

View File

@@ -0,0 +1,17 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a control interrupt request to the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "interrupt")
public class CLIControlInterruptRequest extends ControlRequestPayload {
public CLIControlInterruptRequest() {
super();
setSubtype("interrupt");
}
}

View File

@@ -0,0 +1,235 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a control permission request to the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "can_use_tool")
public class CLIControlPermissionRequest extends ControlRequestPayload {
public CLIControlPermissionRequest() {
super();
this.subtype = "can_use_tool";
}
/**
* The name of the tool requesting permission.
*/
@JSONField(name = "tool_name")
private String toolName;
/**
* The ID of the tool use.
*/
@JSONField(name = "tool_use_id")
private String toolUseId;
/**
* The input for the tool.
*/
private Map<String, Object> input;
/**
* List of permission suggestions.
*/
@JSONField(name = "permission_suggestions")
private List<PermissionSuggestion> permissionSuggestions;
/**
* The blocked path.
*/
@JSONField(name = "blocked_path")
private String blockedPath;
/**
* Gets the name of the tool requesting permission.
*
* @return The name of the tool requesting permission
*/
public String getToolName() {
return toolName;
}
/**
* Sets the name of the tool requesting permission.
*
* @param toolName The name of the tool requesting permission
*/
public void setToolName(String toolName) {
this.toolName = toolName;
}
/**
* Gets the ID of the tool use.
*
* @return The ID of the tool use
*/
public String getToolUseId() {
return toolUseId;
}
/**
* Sets the ID of the tool use.
*
* @param toolUseId The ID of the tool use
*/
public void setToolUseId(String toolUseId) {
this.toolUseId = toolUseId;
}
/**
* Gets the input for the tool.
*
* @return The input for the tool
*/
public Map<String, Object> getInput() {
return input;
}
/**
* Sets the input for the tool.
*
* @param input The input for the tool
*/
public void setInput(Map<String, Object> input) {
this.input = input;
}
/**
* Gets the list of permission suggestions.
*
* @return The list of permission suggestions
*/
public List<PermissionSuggestion> getPermissionSuggestions() {
return permissionSuggestions;
}
/**
* Sets the list of permission suggestions.
*
* @param permissionSuggestions The list of permission suggestions
*/
public void setPermissionSuggestions(
List<PermissionSuggestion> permissionSuggestions) {
this.permissionSuggestions = permissionSuggestions;
}
/**
* Gets the blocked path.
*
* @return The blocked path
*/
public String getBlockedPath() {
return blockedPath;
}
/**
* Sets the blocked path.
*
* @param blockedPath The blocked path
*/
public void setBlockedPath(String blockedPath) {
this.blockedPath = blockedPath;
}
/**
* Represents a permission suggestion.
*/
public static class PermissionSuggestion {
/**
* The type of suggestion (allow, deny, modify).
*/
private String type; // 'allow' | 'deny' | 'modify'
/**
* The label for the suggestion.
*/
private String label;
/**
* The description of the suggestion.
*/
private String description;
/**
* The modified input.
*/
private Object modifiedInput;
/**
* Gets the type of suggestion.
*
* @return The type of suggestion
*/
public String getType() {
return type;
}
/**
* Sets the type of suggestion.
*
* @param type The type of suggestion
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the label for the suggestion.
*
* @return The label for the suggestion
*/
public String getLabel() {
return label;
}
/**
* Sets the label for the suggestion.
*
* @param label The label for the suggestion
*/
public void setLabel(String label) {
this.label = label;
}
/**
* Gets the description of the suggestion.
*
* @return The description of the suggestion
*/
public String getDescription() {
return description;
}
/**
* Sets the description of the suggestion.
*
* @param description The description of the suggestion
*/
public void setDescription(String description) {
this.description = description;
}
/**
* Gets the modified input.
*
* @return The modified input
*/
public Object getModifiedInput() {
return modifiedInput;
}
/**
* Sets the modified input.
*
* @param modifiedInput The modified input
*/
public void setModifiedInput(Object modifiedInput) {
this.modifiedInput = modifiedInput;
}
}
}

View File

@@ -0,0 +1,45 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior;
/**
* Represents a control permission response from the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "can_use_tool")
public class CLIControlPermissionResponse extends ControlResponsePayload {
public CLIControlPermissionResponse() {
super();
this.subtype = "can_use_tool";
}
/**
* The behavior for the permission request.
*/
@JSONField(unwrapped = true)
Behavior behavior;
/**
* Gets the behavior for the permission request.
*
* @return The behavior for the permission request
*/
public Behavior getBehavior() {
return behavior;
}
/**
* Sets the behavior for the permission request.
*
* @param behavior The behavior for the permission request
* @return This instance for method chaining
*/
public CLIControlPermissionResponse setBehavior(Behavior behavior) {
this.behavior = behavior;
return this;
}
}

View File

@@ -0,0 +1,40 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a control request to set the model in the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "set_model")
public class CLIControlSetModelRequest extends ControlRequestPayload {
public CLIControlSetModelRequest() {
super();
this.subtype = "set_model";
}
/**
* The model to set.
*/
String model;
/**
* Gets the model to set.
*
* @return The model to set
*/
public String getModel() {
return model;
}
/**
* Sets the model to set.
*
* @param model The model to set
*/
public void setModel(String model) {
this.model = model;
}
}

View File

@@ -0,0 +1,54 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
/**
* Represents a control response for setting the model in the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class CLIControlSetModelResponse {
/**
* The subtype of the response ("set_model").
*/
String subtype = "set_model";
/**
* The model that was set.
*/
String model;
/**
* Gets the subtype of the response.
*
* @return The subtype of the response
*/
public String getSubtype() {
return subtype;
}
/**
* Sets the subtype of the response.
*
* @param subtype The subtype of the response
*/
public void setSubtype(String subtype) {
this.subtype = subtype;
}
/**
* Gets the model that was set.
*
* @return The model that was set
*/
public String getModel() {
return model;
}
/**
* Sets the model that was set.
*
* @param model The model that was set
*/
public void setModel(String model) {
this.model = model;
}
}

View File

@@ -0,0 +1,40 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a control request to set the permission mode in the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "set_permission_mode")
public class CLIControlSetPermissionModeRequest extends ControlRequestPayload {
public CLIControlSetPermissionModeRequest() {
super();
setSubtype("set_permission_mode");
}
/**
* The permission mode to set.
*/
String mode;
/**
* Gets the permission mode to set.
*
* @return The permission mode to set
*/
public String getMode() {
return mode;
}
/**
* Sets the permission mode to set.
*
* @param mode The permission mode to set
*/
public void setMode(String mode) {
this.mode = mode;
}
}

View File

@@ -0,0 +1,26 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a payload request in the CLI control message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "ControlRequestPayload",
seeAlso = {CLIControlInitializeRequest.class, CLIControlInterruptRequest.class, CLIControlPermissionRequest.class, CLIControlSetModelRequest.class, CLIControlSetPermissionModeRequest.class})
public class ControlRequestPayload {
/**
* The subtype of the request.
*/
protected String subtype;
public String getSubtype() {
return subtype;
}
public void setSubtype(String subtype) {
this.subtype = subtype;
}
}

View File

@@ -0,0 +1,26 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a payload request in the CLI control message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "ControlResponsePayload",
seeAlso = {CLIControlInitializeResponse.class, CLIControlPermissionResponse.class})
public class ControlResponsePayload {
/**
* The subtype of the request.
*/
protected String subtype;
public String getSubtype() {
return subtype;
}
public void setSubtype(String subtype) {
this.subtype = subtype;
}
}

View File

@@ -0,0 +1,594 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
export interface Annotation {
type: string;
value: string;
}
export interface Usage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
total_tokens?: number;
}
export interface ExtendedUsage extends Usage {
server_tool_use?: {
web_search_requests: number;
};
service_tier?: string;
cache_creation?: {
ephemeral_1h_input_tokens: number;
ephemeral_5m_input_tokens: number;
};
}
export interface ModelUsage {
inputTokens: number;
outputTokens: number;
cacheReadInputTokens: number;
cacheCreationInputTokens: number;
webSearchRequests: number;
contextWindow: number;
}
export interface CLIPermissionDenial {
tool_name: string;
tool_use_id: string;
tool_input: unknown;
}
export interface TextBlock {
type: 'text';
text: string;
annotations?: Annotation[];
}
export interface ThinkingBlock {
type: 'thinking';
thinking: string;
signature?: string;
annotations?: Annotation[];
}
export interface ToolUseBlock {
type: 'tool_use';
id: string;
name: string;
input: unknown;
annotations?: Annotation[];
}
export interface ToolResultBlock {
type: 'tool_result';
tool_use_id: string;
content?: string | ContentBlock[];
is_error?: boolean;
annotations?: Annotation[];
}
export type ContentBlock =
| TextBlock
| ThinkingBlock
| ToolUseBlock
| ToolResultBlock;
export interface APIUserMessage {
role: 'user';
content: string | ContentBlock[];
}
export interface APIAssistantMessage {
id: string;
type: 'message';
role: 'assistant';
model: string;
content: ContentBlock[];
stop_reason?: string | null;
usage: Usage;
}
export interface SDKUserMessage {
type: 'user';
uuid?: string;
session_id: string;
message: APIUserMessage;
parent_tool_use_id: string | null;
options?: Record<string, unknown>;
}
export interface SDKAssistantMessage {
type: 'assistant';
uuid: string;
session_id: string;
message: APIAssistantMessage;
parent_tool_use_id: string | null;
}
export interface SDKSystemMessage {
type: 'system';
subtype: string;
uuid: string;
session_id: string;
data?: unknown;
cwd?: string;
tools?: string[];
mcp_servers?: Array<{
name: string;
status: string;
}>;
model?: string;
permission_mode?: string;
slash_commands?: string[];
qwen_code_version?: string;
output_style?: string;
agents?: string[];
skills?: string[];
capabilities?: Record<string, unknown>;
compact_metadata?: {
trigger: 'manual' | 'auto';
pre_tokens: number;
};
}
export interface SDKResultMessageSuccess {
type: 'result';
subtype: 'success';
uuid: string;
session_id: string;
is_error: false;
duration_ms: number;
duration_api_ms: number;
num_turns: number;
result: string;
usage: ExtendedUsage;
modelUsage?: Record<string, ModelUsage>;
permission_denials: CLIPermissionDenial[];
[key: string]: unknown;
}
export interface SDKResultMessageError {
type: 'result';
subtype: 'error_max_turns' | 'error_during_execution';
uuid: string;
session_id: string;
is_error: true;
duration_ms: number;
duration_api_ms: number;
num_turns: number;
usage: ExtendedUsage;
modelUsage?: Record<string, ModelUsage>;
permission_denials: CLIPermissionDenial[];
error?: {
type?: string;
message: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
export type SDKResultMessage = SDKResultMessageSuccess | SDKResultMessageError;
export interface MessageStartStreamEvent {
type: 'message_start';
message: {
id: string;
role: 'assistant';
model: string;
};
}
export interface ContentBlockStartEvent {
type: 'content_block_start';
index: number;
content_block: ContentBlock;
}
export type ContentBlockDelta =
| {
type: 'text_delta';
text: string;
}
| {
type: 'thinking_delta';
thinking: string;
}
| {
type: 'input_json_delta';
partial_json: string;
};
export interface ContentBlockDeltaEvent {
type: 'content_block_delta';
index: number;
delta: ContentBlockDelta;
}
export interface ContentBlockStopEvent {
type: 'content_block_stop';
index: number;
}
export interface MessageStopStreamEvent {
type: 'message_stop';
}
export type StreamEvent =
| MessageStartStreamEvent
| ContentBlockStartEvent
| ContentBlockDeltaEvent
| ContentBlockStopEvent
| MessageStopStreamEvent;
export interface SDKPartialAssistantMessage {
type: 'stream_event';
uuid: string;
session_id: string;
event: StreamEvent;
parent_tool_use_id: string | null;
}
export type PermissionMode = 'default' | 'plan' | 'auto-edit' | 'yolo';
/**
* TODO: Align with `ToolCallConfirmationDetails`
*/
export interface PermissionSuggestion {
type: 'allow' | 'deny' | 'modify';
label: string;
description?: string;
modifiedInput?: unknown;
}
export interface HookRegistration {
event: string;
callback_id: string;
}
export interface HookCallbackResult {
shouldSkip?: boolean;
shouldInterrupt?: boolean;
suppressOutput?: boolean;
message?: string;
}
export interface CLIControlInterruptRequest {
subtype: 'interrupt';
}
export interface CLIControlPermissionRequest {
subtype: 'can_use_tool';
tool_name: string;
tool_use_id: string;
input: unknown;
permission_suggestions: PermissionSuggestion[] | null;
blocked_path: string | null;
}
export enum AuthProviderType {
DYNAMIC_DISCOVERY = 'dynamic_discovery',
GOOGLE_CREDENTIALS = 'google_credentials',
SERVICE_ACCOUNT_IMPERSONATION = 'service_account_impersonation',
}
export interface MCPServerConfig {
command?: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
url?: string;
httpUrl?: string;
headers?: Record<string, string>;
tcp?: string;
timeout?: number;
trust?: boolean;
description?: string;
includeTools?: string[];
excludeTools?: string[];
extensionName?: string;
oauth?: Record<string, unknown>;
authProviderType?: AuthProviderType;
targetAudience?: string;
targetServiceAccount?: string;
}
/**
* SDK MCP Server configuration
*
* SDK MCP servers run in the SDK process and are connected via in-memory transport.
* Tool calls are routed through the control plane between SDK and CLI.
*/
export interface SDKMcpServerConfig {
/**
* Type identifier for SDK MCP servers
*/
type: 'sdk';
/**
* Server name for identification and routing
*/
name: string;
/**
* The MCP Server instance created by createSdkMcpServer()
*/
instance: McpServer;
}
/**
* Wire format for SDK MCP servers sent to the CLI
*/
export type WireSDKMcpServerConfig = Omit<SDKMcpServerConfig, 'instance'>;
export interface CLIControlInitializeRequest {
subtype: 'initialize';
hooks?: HookRegistration[] | null;
/**
* SDK MCP servers config
* These are MCP servers running in the SDK process, connected via control plane.
* External MCP servers are configured separately in settings, not via initialization.
*/
sdkMcpServers?: Record<string, WireSDKMcpServerConfig>;
/**
* External MCP servers that should be managed by the CLI.
*/
mcpServers?: Record<string, MCPServerConfig>;
agents?: SubagentConfig[];
}
export interface CLIControlSetPermissionModeRequest {
subtype: 'set_permission_mode';
mode: PermissionMode;
}
export interface CLIHookCallbackRequest {
subtype: 'hook_callback';
callback_id: string;
input: unknown;
tool_use_id: string | null;
}
export interface CLIControlMcpMessageRequest {
subtype: 'mcp_message';
server_name: string;
message: {
jsonrpc?: string;
method: string;
params?: Record<string, unknown>;
id?: string | number | null;
};
}
export interface CLIControlSetModelRequest {
subtype: 'set_model';
model: string;
}
export interface CLIControlMcpStatusRequest {
subtype: 'mcp_server_status';
}
export interface CLIControlSupportedCommandsRequest {
subtype: 'supported_commands';
}
export type ControlRequestPayload =
| CLIControlInterruptRequest
| CLIControlPermissionRequest
| CLIControlInitializeRequest
| CLIControlSetPermissionModeRequest
| CLIHookCallbackRequest
| CLIControlMcpMessageRequest
| CLIControlSetModelRequest
| CLIControlMcpStatusRequest
| CLIControlSupportedCommandsRequest;
export interface CLIControlRequest {
type: 'control_request';
request_id: string;
request: ControlRequestPayload;
}
export interface PermissionApproval {
allowed: boolean;
reason?: string;
modifiedInput?: unknown;
}
export interface ControlResponse {
subtype: 'success';
request_id: string;
response: unknown;
}
export interface ControlErrorResponse {
subtype: 'error';
request_id: string;
error: string | { message: string; [key: string]: unknown };
}
export interface CLIControlResponse {
type: 'control_response';
response: ControlResponse | ControlErrorResponse;
}
export interface ControlCancelRequest {
type: 'control_cancel_request';
request_id?: string;
}
export type ControlMessage =
| CLIControlRequest
| CLIControlResponse
| ControlCancelRequest;
/**
* Union of all SDK message types
*/
export type SDKMessage =
| SDKUserMessage
| SDKAssistantMessage
| SDKSystemMessage
| SDKResultMessage
| SDKPartialAssistantMessage;
export function isSDKUserMessage(msg: any): msg is SDKUserMessage {
return (
msg && typeof msg === 'object' && msg.type === 'user' && 'message' in msg
);
}
export function isSDKAssistantMessage(msg: any): msg is SDKAssistantMessage {
return (
msg &&
typeof msg === 'object' &&
msg.type === 'assistant' &&
'uuid' in msg &&
'message' in msg &&
'session_id' in msg &&
'parent_tool_use_id' in msg
);
}
export function isSDKSystemMessage(msg: any): msg is SDKSystemMessage {
return (
msg &&
typeof msg === 'object' &&
msg.type === 'system' &&
'subtype' in msg &&
'uuid' in msg &&
'session_id' in msg
);
}
export function isSDKResultMessage(msg: any): msg is SDKResultMessage {
return (
msg &&
typeof msg === 'object' &&
msg.type === 'result' &&
'subtype' in msg &&
'duration_ms' in msg &&
'is_error' in msg &&
'uuid' in msg &&
'session_id' in msg
);
}
export function isSDKPartialAssistantMessage(
msg: any,
): msg is SDKPartialAssistantMessage {
return (
msg &&
typeof msg === 'object' &&
msg.type === 'stream_event' &&
'uuid' in msg &&
'session_id' in msg &&
'event' in msg &&
'parent_tool_use_id' in msg
);
}
export function isControlRequest(msg: any): msg is CLIControlRequest {
return (
msg &&
typeof msg === 'object' &&
msg.type === 'control_request' &&
'request_id' in msg &&
'request' in msg
);
}
export function isControlResponse(msg: any): msg is CLIControlResponse {
return (
msg &&
typeof msg === 'object' &&
msg.type === 'control_response' &&
'response' in msg
);
}
export function isControlCancel(msg: any): msg is ControlCancelRequest {
return (
msg &&
typeof msg === 'object' &&
msg.type === 'control_cancel_request' &&
'request_id' in msg
);
}
export function isTextBlock(block: any): block is TextBlock {
return block && typeof block === 'object' && block.type === 'text';
}
export function isThinkingBlock(block: any): block is ThinkingBlock {
return block && typeof block === 'object' && block.type === 'thinking';
}
export function isToolUseBlock(block: any): block is ToolUseBlock {
return block && typeof block === 'object' && block.type === 'tool_use';
}
export function isToolResultBlock(block: any): block is ToolResultBlock {
return block && typeof block === 'object' && block.type === 'tool_result';
}
export type SubagentLevel = 'session';
export interface ModelConfig {
model?: string;
temp?: number;
top_p?: number;
}
export interface RunConfig {
max_time_minutes?: number;
max_turns?: number;
}
export interface SubagentConfig {
name: string;
description: string;
tools?: string[];
systemPrompt: string;
level: SubagentLevel;
filePath?: string;
modelConfig?: Partial<ModelConfig>;
runConfig?: Partial<RunConfig>;
color?: string;
readonly isBuiltin?: boolean;
}
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Control Request Types
*
* Centralized enum for all control request subtypes supported by the CLI.
* This enum should be kept in sync with the controllers in:
* - packages/cli/src/services/control/controllers/systemController.ts
* - packages/cli/src/services/control/controllers/permissionController.ts
* - packages/cli/src/services/control/controllers/mcpController.ts
* - packages/cli/src/services/control/controllers/hookController.ts
*/
export enum ControlRequestType {
// SystemController requests
INITIALIZE = 'initialize',
INTERRUPT = 'interrupt',
SET_MODEL = 'set_model',
SUPPORTED_COMMANDS = 'supported_commands',
// PermissionController requests
CAN_USE_TOOL = 'can_use_tool',
SET_PERMISSION_MODE = 'set_permission_mode',
// MCPController requests
MCP_MESSAGE = 'mcp_message',
MCP_SERVER_STATUS = 'mcp_server_status',
// HookController requests
HOOK_CALLBACK = 'hook_callback',
}

View File

@@ -0,0 +1,302 @@
package com.alibaba.qwen.code.cli.session;
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.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.assistant.SDKPartialAssistantMessage;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInitializeRequest;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInitializeResponse;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInterruptRequest;
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.payload.CLIControlSetModelRequest;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlSetPermissionModeRequest;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload;
import com.alibaba.qwen.code.cli.session.event.consumers.SessionEventConsumers;
import com.alibaba.qwen.code.cli.session.exception.SessionControlException;
import com.alibaba.qwen.code.cli.session.exception.SessionSendPromptException;
import com.alibaba.qwen.code.cli.transport.Transport;
import com.alibaba.qwen.code.cli.transport.TransportOptions;
import com.alibaba.qwen.code.cli.utils.MyConcurrentUtils;
import com.alibaba.qwen.code.cli.utils.Timeout;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages a session with the Qwen Code CLI, handling communication, sending prompts, and processing responses.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class Session {
private static final Logger log = LoggerFactory.getLogger(Session.class);
private final Transport transport;
private CLIControlInitializeResponse lastCliControlInitializeResponse;
private SDKSystemMessage lastSdkSystemMessage;
private final Timeout defaultEventTimeout = Timeout.TIMEOUT_60_SECONDS;
/**
* Checks if the session is configured for streaming.
*
* @return true if streaming is enabled, false otherwise
*/
public boolean isStreaming() {
return Optional.ofNullable(transport)
.map(Transport::getTransportOptions)
.map(TransportOptions::getIncludePartialMessages)
.orElse(false);
}
/**
* Constructs a new session with the specified transport.
*
* @param transport The transport layer to use for communication
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the transport is not available
*/
public Session(Transport transport) throws SessionControlException {
if (transport == null || !transport.isAvailable()) {
throw new SessionControlException("Transport is not available");
}
this.transport = transport;
start();
}
/**
* Starts the session by initializing communication with the CLI.
*
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if initialization fails
*/
public void start() throws SessionControlException {
try {
if (!transport.isAvailable()) {
transport.start();
}
String response = transport.inputWaitForOneLine(CLIControlRequest.create(new CLIControlInitializeRequest()).toString());
CLIControlResponse<CLIControlInitializeResponse> cliControlResponse = JSON.parseObject(response,
new TypeReference<CLIControlResponse<CLIControlInitializeResponse>>() {});
this.lastCliControlInitializeResponse = cliControlResponse.getResponse().getResponse();
} catch (Exception e) {
throw new SessionControlException("Failed to initialize the session", e);
}
}
/**
* Closes the session and releases resources.
*
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if closing fails
*/
public void close() throws SessionControlException {
try {
transport.close();
} catch (Exception e) {
throw new SessionControlException("Failed to close the session", e);
}
}
/**
* Interrupts the current operation in the CLI.
*
* @return An optional boolean indicating success of the interrupt operation
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails
*/
public Optional<Boolean> interrupt() throws SessionControlException {
checkAvailable();
return processControlRequest(new CLIControlRequest<CLIControlInterruptRequest>().setRequest(new CLIControlInterruptRequest()).toString());
}
/**
* Sets the model to be used in the session.
*
* @param modelName The name of the model to use
* @return An optional boolean indicating success of the operation
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails
*/
public Optional<Boolean> setModel(String modelName) throws SessionControlException {
checkAvailable();
CLIControlSetModelRequest cliControlSetModelRequest = new CLIControlSetModelRequest();
cliControlSetModelRequest.setModel(modelName);
return processControlRequest(new CLIControlRequest<CLIControlSetModelRequest>().setRequest(cliControlSetModelRequest).toString());
}
/**
* Sets the permission mode for the session.
*
* @param permissionMode The permission mode to use
* @return An optional boolean indicating success of the operation
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails
*/
public Optional<Boolean> setPermissionMode(PermissionMode permissionMode) throws SessionControlException {
checkAvailable();
CLIControlSetPermissionModeRequest cliControlSetPermissionModeRequest = new CLIControlSetPermissionModeRequest();
cliControlSetPermissionModeRequest.setMode(permissionMode.getValue());
return processControlRequest(
new CLIControlRequest<CLIControlSetPermissionModeRequest>().setRequest(cliControlSetPermissionModeRequest).toString());
}
private Optional<Boolean> processControlRequest(String request) throws SessionControlException {
try {
if (transport.isReading()) {
transport.inputNoWaitResponse(request);
return Optional.empty();
} else {
String response = transport.inputWaitForOneLine(request);
CLIControlResponse<?> cliControlResponse = JSON.parseObject(response, new TypeReference<CLIControlResponse<?>>() {});
return Optional.of("success".equals(cliControlResponse.getResponse().getSubtype()));
}
} catch (Exception e) {
throw new SessionControlException("Failed to set model", e);
}
}
/**
* Continues the current session.
*
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails
*/
public void continueSession() throws SessionControlException {
resumeSession(getSessionId());
}
/**
* Resumes a session with the specified ID.
*
* @param sessionId The ID of the session to resume
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails
*/
public void resumeSession(String sessionId) throws SessionControlException {
if (StringUtils.isNotBlank(sessionId)) {
transport.getTransportOptions().setResumeSessionId(sessionId);
}
this.start();
}
/**
* Sends a prompt to the CLI and processes the response.
*
* @param prompt The prompt to send to the CLI
* @param sessionEventConsumers Consumers for handling different types of events
* @throws com.alibaba.qwen.code.cli.session.exception.SessionSendPromptException if sending the prompt fails
* @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if a control operation fails
*/
public void sendPrompt(String prompt, SessionEventConsumers sessionEventConsumers) throws SessionSendPromptException, SessionControlException {
checkAvailable();
try {
transport.inputWaitForMultiLine(new SDKUserMessage().setContent(prompt).toString(), (line) -> {
JSONObject jsonObject = JSON.parseObject(line);
String messageType = jsonObject.getString("type");
if ("system".equals(messageType)) {
lastSdkSystemMessage = jsonObject.to(SDKSystemMessage.class);
MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onSystemMessage(this, lastSdkSystemMessage),
Optional.ofNullable(sessionEventConsumers.onSystemMessageTimeout(this, lastSdkSystemMessage))
.orElse(defaultEventTimeout));
return false;
} else if ("assistant".equals(messageType)) {
SDKAssistantMessage assistantMessage = jsonObject.to(SDKAssistantMessage.class);
MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onAssistantMessage(this, assistantMessage),
Optional.ofNullable(sessionEventConsumers.onAssistantMessageTimeout(this, assistantMessage)).orElse(defaultEventTimeout));
return false;
} else if ("stream_event".equals(messageType)) {
SDKPartialAssistantMessage sdkPartialAssistantMessage = jsonObject.to(SDKPartialAssistantMessage.class);
MyConcurrentUtils.runAndWait(
() -> sessionEventConsumers.onPartialAssistantMessage(this, sdkPartialAssistantMessage),
Optional.ofNullable(sessionEventConsumers.onPartialAssistantMessageTimeout(this, sdkPartialAssistantMessage))
.orElse(defaultEventTimeout));
return false;
} else if ("user".equals(messageType)) {
SDKUserMessage sdkUserMessage = jsonObject.to(SDKUserMessage.class, Feature.FieldBased);
MyConcurrentUtils.runAndWait(
() -> sessionEventConsumers.onUserMessage(this, sdkUserMessage),
Optional.ofNullable(sessionEventConsumers.onUserMessageTimeout(this, sdkUserMessage)).orElse(defaultEventTimeout));
return false;
} else if ("result".equals(messageType)) {
SDKResultMessage sdkResultMessage = jsonObject.to(SDKResultMessage.class);
MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onResultMessage(this, sdkResultMessage),
Optional.ofNullable(sessionEventConsumers.onResultMessageTimeout(this, sdkResultMessage)).orElse(defaultEventTimeout));
return true;
} else if ("control_response".equals(messageType)) {
CLIControlResponse<? extends ControlResponsePayload> controlResponse = jsonObject.to(
new TypeReference<CLIControlResponse<? extends ControlResponsePayload>>() {});
MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onControlResponse(this, controlResponse),
Optional.ofNullable(sessionEventConsumers.onControlResponseTimeout(this, controlResponse)).orElse(defaultEventTimeout));
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)) {
CLIControlResponse<? extends ControlResponsePayload> controlResponse;
try {
CLIControlRequest<? extends ControlRequestPayload> controlRequest = jsonObject.to(
new TypeReference<CLIControlRequest<? extends ControlRequestPayload>>() {});
controlResponse = MyConcurrentUtils.runAndWait(
() -> sessionEventConsumers.onControlRequest(this, controlRequest),
Optional.ofNullable(sessionEventConsumers.onControlRequestTimeout(this, controlRequest)).orElse(defaultEventTimeout));
} catch (Exception e) {
log.error("Failed to process control request", e);
controlResponse = new CLIControlResponse<>();
}
try {
transport.inputNoWaitResponse(Optional.ofNullable(controlResponse).map(CLIControlResponse::toString)
.orElse(new CLIControlResponse<ControlResponsePayload>().toString()));
} catch (Exception e) {
throw new RuntimeException("Failed to send control response", e);
}
return false;
} else {
log.warn("unknown message type: {}", messageType);
MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onOtherMessage(this, line),
Optional.ofNullable(sessionEventConsumers.onOtherMessageTimeout(this, line)).orElse(defaultEventTimeout));
return false;
}
});
} catch (Exception e) {
throw new SessionSendPromptException("Failed to send prompt", e);
}
}
/**
* Gets the current session ID.
*
* @return The session ID, or null if not available
*/
public String getSessionId() {
return Optional.ofNullable(lastSdkSystemMessage).map(SDKSystemMessage::getSessionId).orElse(null);
}
/**
* Checks if the session is available for operations.
*
* @return true if the session is available, false otherwise
*/
public boolean isAvailable() {
return transport.isAvailable();
}
/**
* Gets the capabilities of the CLI.
*
* @return A Capabilities object representing the CLI's capabilities
*/
public Capabilities getCapabilities() {
return Optional.ofNullable(lastCliControlInitializeResponse).map(CLIControlInitializeResponse::getCapabilities).orElse(new Capabilities());
}
private void checkAvailable() throws SessionControlException {
if (!isAvailable()) {
throw new SessionControlException("Session is not available");
}
}
}

View File

@@ -0,0 +1,159 @@
package com.alibaba.qwen.code.cli.session.event.consumers;
import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior;
import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior.Operation;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlPermissionRequest;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload;
import com.alibaba.qwen.code.cli.session.Session;
import com.alibaba.qwen.code.cli.utils.Timeout;
/**
* Interface for handling different types of assistant content during a session.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public interface AssistantContentConsumers {
/**
* Handles text content from the assistant.
*
* @param session The session
* @param textAssistantContent The text content from the assistant
*/
void onText(Session session, TextAssistantContent textAssistantContent);
/**
* Handles thinking content from the assistant.
*
* @param session The session
* @param thingkingAssistantContent The thinking content from the assistant
*/
void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent);
/**
* Handles tool use content from the assistant.
*
* @param session The session
* @param toolUseAssistantContent The tool use content from the assistant
*/
void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent);
/**
* Handles tool result content from the assistant.
*
* @param session The session
* @param toolResultAssistantContent The tool result content from the assistant
*/
void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent);
/**
* Handles other types of assistant content.
*
* @param session The session
* @param other The other content from the assistant
*/
void onOtherContent(Session session, AssistantContent<?> other);
/**
* Handles permission requests.
*
* @param session The session
* @param permissionRequest The permission request
* @return The behavior for the permission request
*/
Behavior onPermissionRequest(Session session, CLIControlPermissionRequest permissionRequest);
/**
* Handles permission requests.
*
* @param session The session
* @param requestPayload The control request payload
* @return The response payload for the control request
*/
ControlResponsePayload onOtherControlRequest(Session session, ControlRequestPayload requestPayload);
/**
* Handles usage information from the assistant.
*
* @param session The session
* @param AssistantUsage The usage information from the assistant
*/
void onUsage(Session session, AssistantUsage AssistantUsage);
/**
* Sets the default permission operation.
*
* @param defaultPermissionOperation The default permission operation
* @return This instance for method chaining
*/
AssistantContentSimpleConsumers setDefaultPermissionOperation(Operation defaultPermissionOperation);
/**
* Gets timeout for permission request handling.
*
* @param session The session
* @return The timeout for permission request handling
*/
Timeout onPermissionRequestTimeout(Session session, CLIControlPermissionRequest permissionRequest);
/**
* Gets timeout for other control request handling.
*
* @param session The session
* @param requestPayload The control request payload
* @return The timeout for other control request handling
*/
Timeout onOtherControlRequestTimeout(Session session, ControlRequestPayload requestPayload);
/**
* Gets timeout for text handling.
*
* @param session The session
* @param textAssistantContent The text content from the assistant
* @return The timeout for text handling
*/
Timeout onTextTimeout(Session session, TextAssistantContent textAssistantContent);
/**
* Gets timeout for thinking handling.
*
* @param session The session
* @param thingkingAssistantContent The thinking content from the assistant
* @return The timeout for thinking handling
*/
Timeout onThinkingTimeout(Session session, ThingkingAssistantContent thingkingAssistantContent);
/**
* Gets timeout for tool use handling.
*
* @param session The session
* @param toolUseAssistantContent The tool use content from the assistant
* @return The timeout for tool use handling
*/
Timeout onToolUseTimeout(Session session, ToolUseAssistantContent toolUseAssistantContent);
/**
* Gets timeout for tool result handling.
*
* @param session The session
* @param toolResultAssistantContent The tool result content from the assistant
* @return The timeout for tool result handling
*/
Timeout onToolResultTimeout(Session session, ToolResultAssistantContent toolResultAssistantContent);
/**
* Gets timeout for other content handling.
*
* @param session The session
* @param other The other content from the assistant
* @return The timeout for other content handling
*/
Timeout onOtherContentTimeout(Session session, AssistantContent<?> other);
}

View File

@@ -0,0 +1,193 @@
package com.alibaba.qwen.code.cli.session.event.consumers;
import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
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.data.behavior.Behavior.Operation;
import com.alibaba.qwen.code.cli.protocol.data.behavior.Deny;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlPermissionRequest;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload;
import com.alibaba.qwen.code.cli.session.Session;
import com.alibaba.qwen.code.cli.utils.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple implementation of AssistantContentConsumers that provides empty implementations for all methods.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class AssistantContentSimpleConsumers implements AssistantContentConsumers {
/**
* {@inheritDoc}
*/
@Override
public void onText(Session session, TextAssistantContent textAssistantContent) {
log.debug("Received textAssistantContent {}", textAssistantContent.getText());
}
/**
* {@inheritDoc}
*/
@Override
public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) {
log.debug("Received thingkingAssistantContent {}", thingkingAssistantContent.getThinking());
}
/**
* {@inheritDoc}
*/
@Override
public void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent) {
log.debug("Received toolUseAssistantContent {}", toolUseAssistantContent.getInput());
}
/**
* {@inheritDoc}
*/
@Override
public void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent) {
if (log.isDebugEnabled()) {
log.debug("Received toolResultAssistantContent {}", toolResultAssistantContent);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onOtherContent(Session session, AssistantContent<?> other) {
if (log.isDebugEnabled()) {
log.debug("Received other content {}", other);
}
}
/**
* {@inheritDoc}
*/
@Override
public Behavior onPermissionRequest(Session session, CLIControlPermissionRequest permissionRequest) {
if (Operation.deny.equals(this.defaultPermissionOperation)) {
log.info("use defaultPermissionOperation Permission denied.");
return new Deny().setMessage("Permission denied.");
} else {
log.info("use defaultPermissionOperation Permission allowed.");
return new Allow().setUpdatedInput(permissionRequest.getInput());
}
}
@Override
public ControlResponsePayload onOtherControlRequest(Session session, ControlRequestPayload requestPayload) {
throw new RuntimeException("need override onOtherControlRequest");
}
/**
* {@inheritDoc}
*/
@Override
public void onUsage(Session session, AssistantUsage AssistantUsage) {
log.info("received usage {} of message {}", AssistantUsage.getUsage(), AssistantUsage.getMessageId());
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onPermissionRequestTimeout(Session session, CLIControlPermissionRequest permissionRequest) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onOtherControlRequestTimeout(Session session, ControlRequestPayload requestPayload) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onTextTimeout(Session session, TextAssistantContent textAssistantContent) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onThinkingTimeout(Session session, ThingkingAssistantContent thingkingAssistantContent) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onToolUseTimeout(Session session, ToolUseAssistantContent toolUseAssistantContent) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onToolResultTimeout(Session session, ToolResultAssistantContent toolResultAssistantContent) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onOtherContentTimeout(Session session, AssistantContent<?> other) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public AssistantContentSimpleConsumers setDefaultPermissionOperation(Operation defaultPermissionOperation) {
this.defaultPermissionOperation = defaultPermissionOperation;
return this;
}
/**
* Constructor.
*
* @param defaultPermissionOperation The default permission operation.
* @param defaultEventTimeout The default event timeout.
*/
public AssistantContentSimpleConsumers(Operation defaultPermissionOperation, Timeout defaultEventTimeout) {
this.defaultPermissionOperation = defaultPermissionOperation;
this.defaultEventTimeout = defaultEventTimeout;
}
/**
* Constructor.
*/
public AssistantContentSimpleConsumers() {
}
/**
* The default permission operation.
*/
private Operation defaultPermissionOperation = Operation.deny;
/**
* The default event timeout.
*/
protected Timeout defaultEventTimeout = Timeout.TIMEOUT_60_SECONDS;
private static final Logger log = LoggerFactory.getLogger(AssistantContentSimpleConsumers.class);
}

View File

@@ -0,0 +1,158 @@
package com.alibaba.qwen.code.cli.session.event.consumers;
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.assistant.SDKPartialAssistantMessage;
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.payload.ControlRequestPayload;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload;
import com.alibaba.qwen.code.cli.session.Session;
import com.alibaba.qwen.code.cli.utils.Timeout;
/**
* Interface for handling different types of events during a session.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public interface SessionEventConsumers {
/**
* Handles system messages.
*
* @param session The session
* @param systemMessage The system message
*/
void onSystemMessage(Session session, SDKSystemMessage systemMessage);
/**
* Handles result messages.
*
* @param session The session
* @param resultMessage The result message
*/
void onResultMessage(Session session, SDKResultMessage resultMessage);
/**
* Handles assistant messages.
*
* @param session The session
* @param assistantMessage The assistant message
*/
void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage);
/**
* Handles partial assistant messages.
*
* @param session The session
* @param partialAssistantMessage The partial assistant message
*/
void onPartialAssistantMessage(Session session, SDKPartialAssistantMessage partialAssistantMessage);
/**
* Handles user messages.
*
* @param session The session
* @param userMessage The user message
*/
void onUserMessage(Session session, SDKUserMessage userMessage);
/**
* Handles other types of messages.
*
* @param session The session
* @param message The message
*/
void onOtherMessage(Session session, String message);
/**
* Handles control responses.
*
* @param session The session
* @param cliControlResponse The control response
*/
void onControlResponse(Session session, CLIControlResponse<?> cliControlResponse);
/**
* Handles control requests.
*
* @param session The session
* @param cliControlRequest The control request
* @return The control response
*/
CLIControlResponse<? extends ControlResponsePayload> onControlRequest(Session session, CLIControlRequest<? extends ControlRequestPayload> cliControlRequest);
/**
* Gets timeout for system message handling.
*
* @param session The session
* @param systemMessage The system message
* @return The timeout for system message handling
*/
Timeout onSystemMessageTimeout(Session session, SDKSystemMessage systemMessage);
/**
* Gets timeout for result message handling.
*
* @param session The session
* @param resultMessage The result message
* @return The timeout for result message handling
*/
Timeout onResultMessageTimeout(Session session, SDKResultMessage resultMessage);
/**
* Gets timeout for assistant message handling.
*
* @param session The session
* @param assistantMessage The assistant message
* @return The timeout for assistant message handling
*/
Timeout onAssistantMessageTimeout(Session session, SDKAssistantMessage assistantMessage);
/**
* Gets timeout for partial assistant message handling.
*
* @param session The session
* @param partialAssistantMessage The partial assistant message
* @return The timeout for partial assistant message handling
*/
Timeout onPartialAssistantMessageTimeout(Session session, SDKPartialAssistantMessage partialAssistantMessage);
/**
* Gets timeout for user message handling.
*
* @param session The session
* @param userMessage The user message
* @return The timeout for user message handling
*/
Timeout onUserMessageTimeout(Session session, SDKUserMessage userMessage);
/**
* Gets timeout for other message handling.
*
* @param session The session
* @param message The message
* @return The timeout for other message handling
*/
Timeout onOtherMessageTimeout(Session session, String message);
/**
* Gets timeout for control response handling.
*
* @param session The session
* @param cliControlResponse The control response
* @return The timeout for control response handling
*/
Timeout onControlResponseTimeout(Session session, CLIControlResponse<?> cliControlResponse);
/**
* Gets timeout for control request handling.
*
* @param session The session
* @param cliControlRequest The control request
* @return The timeout for control request handling
*/
Timeout onControlRequestTimeout(Session session, CLIControlRequest<?> cliControlRequest);
}

View File

@@ -0,0 +1,339 @@
package com.alibaba.qwen.code.cli.session.event.consumers;
import java.util.List;
import java.util.Optional;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage;
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.assistant.SDKPartialAssistantMessage;
import com.alibaba.qwen.code.cli.protocol.message.assistant.block.ContentBlock;
import com.alibaba.qwen.code.cli.protocol.message.assistant.event.ContentBlockDeltaEvent;
import com.alibaba.qwen.code.cli.protocol.message.assistant.event.StreamEvent;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlPermissionRequest;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.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.payload.ControlRequestPayload;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload;
import com.alibaba.qwen.code.cli.session.Session;
import com.alibaba.qwen.code.cli.utils.MyConcurrentUtils;
import com.alibaba.qwen.code.cli.utils.Timeout;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple implementation of SessionEventConsumers that provides basic implementations for all methods.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class SessionEventSimpleConsumers implements SessionEventConsumers {
/**
* {@inheritDoc}
*/
@Override
public void onSystemMessage(Session session, SDKSystemMessage systemMessage) {
}
/**
* {@inheritDoc}
*/
@Override
public void onResultMessage(Session session, SDKResultMessage resultMessage) {
}
/**
* {@inheritDoc}
*/
@Override
public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) {
List<ContentBlock<?>> contentBlocks = assistantMessage.getMessage().getContent();
if (assistantContentConsumers == null || contentBlocks == null || contentBlocks.isEmpty()) {
return;
}
assistantContentConsumers.onUsage(session,
new AssistantUsage(assistantMessage.getMessage().getId(), assistantMessage.getMessage().getUsage()));
if (!session.isStreaming()) {
contentBlocks.forEach(contentBlock -> consumeAssistantContent(session, contentBlock));
}
}
/**
* {@inheritDoc}
*/
@Override
public void onPartialAssistantMessage(Session session, SDKPartialAssistantMessage partialAssistantMessage) {
StreamEvent event = partialAssistantMessage.getEvent();
if (!(event instanceof ContentBlockDeltaEvent)) {
log.debug("received partialAssistantMessage and is not instance of ContentBlockDeltaEvent, will ignore process. the message is {}",
partialAssistantMessage);
return;
}
ContentBlockDeltaEvent contentBlockDeltaEvent = (ContentBlockDeltaEvent) event;
contentBlockDeltaEvent.getDelta().setMessageId(partialAssistantMessage.getMessageId());
consumeAssistantContent(session, contentBlockDeltaEvent.getDelta());
}
/**
* <p>consumeAssistantContent.</p>
*
* @param session a {@link com.alibaba.qwen.code.cli.session.Session} object.
* @param assistantContent a {@link com.alibaba.qwen.code.cli.protocol.data.AssistantContent} object.
*/
protected void consumeAssistantContent(Session session, AssistantContent<?> assistantContent) {
if (assistantContent instanceof TextAssistantContent) {
MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onText(session, (TextAssistantContent) assistantContent),
Optional.ofNullable(assistantContentConsumers.onTextTimeout(session, (TextAssistantContent) assistantContent))
.orElse(defaultEventTimeout));
} else if (assistantContent instanceof ThingkingAssistantContent) {
MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onThinking(session, (ThingkingAssistantContent) assistantContent),
Optional.ofNullable(assistantContentConsumers.onThinkingTimeout(session, (ThingkingAssistantContent) assistantContent))
.orElse(defaultEventTimeout));
} else if (assistantContent instanceof ToolUseAssistantContent) {
MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onToolUse(session, (ToolUseAssistantContent) assistantContent),
Optional.ofNullable(assistantContentConsumers.onToolUseTimeout(session, (ToolUseAssistantContent) assistantContent))
.orElse(defaultEventTimeout));
} else if (assistantContent instanceof ToolResultAssistantContent) {
MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onToolResult(session, (ToolResultAssistantContent) assistantContent),
Optional.ofNullable(assistantContentConsumers.onToolResultTimeout(session, (ToolResultAssistantContent) assistantContent))
.orElse(defaultEventTimeout));
} else {
MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onOtherContent(session, assistantContent),
Optional.ofNullable(assistantContentConsumers.onOtherContentTimeout(session, assistantContent)).orElse(defaultEventTimeout));
}
}
/**
* {@inheritDoc}
*/
@Override
public void onUserMessage(Session session, SDKUserMessage userMessage) {
}
/**
* {@inheritDoc}
*/
@Override
public void onOtherMessage(Session session, String message) {
}
/**
* {@inheritDoc}
*/
@Override
public void onControlResponse(Session session, CLIControlResponse<?> cliControlResponse) {
}
/**
* {@inheritDoc}
*/
@Override
public CLIControlResponse<? extends ControlResponsePayload> onControlRequest(Session session, CLIControlRequest<?> cliControlRequest) {
if (assistantContentConsumers == null) {
throw new RuntimeException("please set assistantContentConsumers or override onControlRequest of ");
}
ControlRequestPayload payload = cliControlRequest.getRequest();
if (payload instanceof CLIControlPermissionRequest) {
CLIControlPermissionRequest permissionRequest = (CLIControlPermissionRequest) payload;
return supplyPermissionControlResponse(session, permissionRequest, cliControlRequest.getRequestId());
} else {
ControlRequestPayload request = cliControlRequest.getRequest();
return supplyOtherControlResponse(session, request, cliControlRequest.getRequestId());
}
}
private CLIControlResponse<CLIControlPermissionResponse> supplyPermissionControlResponse(Session session,
CLIControlPermissionRequest permissionRequest, String requestId) {
Behavior behavior;
try {
behavior = Optional.ofNullable(
MyConcurrentUtils.runAndWait(() -> this.assistantContentConsumers.onPermissionRequest(session, permissionRequest),
Optional.ofNullable(assistantContentConsumers.onPermissionRequestTimeout(session, permissionRequest))
.orElse(defaultEventTimeout)))
.map(b -> {
if (b instanceof Allow) {
Allow allow = (Allow) b;
if (allow.getUpdatedInput() == null) {
allow.setUpdatedInput(permissionRequest.getInput());
}
}
return b;
})
.orElse(Behavior.defaultBehavior());
} catch (Exception e) {
log.error("Failed to process permission response", e);
behavior = Behavior.defaultBehavior();
}
CLIControlResponse<CLIControlPermissionResponse> permissionResponse = new CLIControlResponse<>();
permissionResponse.createResponse().setResponse(new CLIControlPermissionResponse().setBehavior(behavior)).setRequestId(requestId);
return permissionResponse;
}
private CLIControlResponse<ControlResponsePayload> supplyOtherControlResponse(Session session, ControlRequestPayload requestPayload,
String requestId) {
ControlResponsePayload controlResponsePayload;
try {
controlResponsePayload = Optional.ofNullable(
MyConcurrentUtils.runAndWait(() -> this.assistantContentConsumers.onOtherControlRequest(session, requestPayload),
ObjectUtils.getIfNull(assistantContentConsumers.onOtherControlRequestTimeout(session, requestPayload),
defaultEventTimeout)))
.orElse(new ControlResponsePayload());
} catch (Exception e) {
log.error("Failed to process permission response", e);
controlResponsePayload = new ControlResponsePayload();
}
CLIControlResponse<ControlResponsePayload> cliControlResponse = new CLIControlResponse<>();
cliControlResponse.createResponse().setResponse(controlResponsePayload).setRequestId(requestId);
return cliControlResponse;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onSystemMessageTimeout(Session session, SDKSystemMessage systemMessage) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onResultMessageTimeout(Session session, SDKResultMessage resultMessage) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onAssistantMessageTimeout(Session session, SDKAssistantMessage assistantMessage) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onPartialAssistantMessageTimeout(Session session, SDKPartialAssistantMessage partialAssistantMessage) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onUserMessageTimeout(Session session, SDKUserMessage userMessage) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onOtherMessageTimeout(Session session, String message) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onControlResponseTimeout(Session session, CLIControlResponse<?> cliControlResponse) {
return defaultEventTimeout;
}
/**
* {@inheritDoc}
*/
@Override
public Timeout onControlRequestTimeout(Session session, CLIControlRequest<?> cliControlRequest) {
return defaultEventTimeout;
}
/**
* Gets the default event timeout.
*
* @return The default event timeout
*/
protected Timeout getDefaultEventTimeout() {
return defaultEventTimeout;
}
/**
* Sets the default event timeout.
*
* @param defaultEventTimeout The default event timeout
* @return This instance for method chaining
*/
public SessionEventSimpleConsumers setDefaultEventTimeout(Timeout defaultEventTimeout) {
this.defaultEventTimeout = defaultEventTimeout;
return this;
}
/**
* Creates a new SessionEventSimpleConsumers instance with default values.
*/
public SessionEventSimpleConsumers() {
}
/**
* Creates a new SessionEventSimpleConsumers instance with the specified parameters.
*
* @param defaultEventTimeout The default event timeout
* @param assistantContentConsumers The assistant content consumers
*/
public SessionEventSimpleConsumers(Timeout defaultEventTimeout, AssistantContentConsumers assistantContentConsumers) {
Validate.notNull(defaultEventTimeout, "defaultEventTimeout can't be null");
Validate.notNull(assistantContentConsumers, "assistantContentConsumers can't be null");
this.defaultEventTimeout = defaultEventTimeout;
this.assistantContentConsumers = assistantContentConsumers;
}
/**
* The default event timeout.
*/
protected Timeout defaultEventTimeout = Timeout.TIMEOUT_180_SECONDS;
/**
* The assistant content consumers.
*/
protected AssistantContentConsumers assistantContentConsumers = new AssistantContentSimpleConsumers();
private static final Logger log = LoggerFactory.getLogger(SessionEventSimpleConsumers.class);
/**
* Sets the assistant content consumers.
*
* @param assistantContentConsumers The assistant content consumers
* @return This instance for method chaining
*/
public SessionEventSimpleConsumers setAssistantContentConsumer(AssistantContentConsumers assistantContentConsumers) {
Validate.notNull(assistantContentConsumers, "assistantContentConsumers can't be null");
this.assistantContentConsumers = assistantContentConsumers;
return this;
}
/**
* Gets the assistant content consumers.
*
* @return The assistant content consumers
*/
public AssistantContentConsumers getAssistantContentConsumers() {
return assistantContentConsumers;
}
}

Some files were not shown because too many files have changed in this diff Show More