mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-15 05:19:15 +00:00
Compare commits
94 Commits
fix/missin
...
feat/skill
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cfc9f4686 | ||
|
|
d86903ced5 | ||
|
|
a47bdc0b06 | ||
|
|
0e769e100b | ||
|
|
b5bcc07223 | ||
|
|
9653dc90d5 | ||
|
|
f8aecb2631 | ||
|
|
570ec432af | ||
|
|
bfc3bbfa9c | ||
|
|
91af9bf6c8 | ||
|
|
f6771c0858 | ||
|
|
2c8be05029 | ||
|
|
4744af1ea8 | ||
|
|
2c285394c7 | ||
|
|
f2d941e469 | ||
|
|
9b2dfe1e06 | ||
|
|
3e695cd82b | ||
|
|
177a91f1d5 | ||
|
|
870d207f18 | ||
|
|
3f512528cb | ||
|
|
0878ee4cbd | ||
|
|
bfe7298858 | ||
|
|
2f2937aafe | ||
|
|
8fcdd86b91 | ||
|
|
d7d7bf0c39 | ||
|
|
b95d9a8d2d | ||
|
|
6f39ae120c | ||
|
|
627857621a | ||
|
|
65c7cf5d8f | ||
|
|
7a823060ac | ||
|
|
2c88ea6dc1 | ||
|
|
ad3086f7dd | ||
|
|
8f3bbef575 | ||
|
|
e2d6ab9b7e | ||
|
|
35bf5ef4d0 | ||
|
|
1d16513e27 | ||
|
|
731fd99800 | ||
|
|
c6ae0a8be7 | ||
|
|
49892a8e17 | ||
|
|
d1a3e828b7 | ||
|
|
b19bb6cb20 | ||
|
|
e8625658ba | ||
|
|
19f8f631b4 | ||
|
|
a4eb3adea8 | ||
|
|
7dc7c6380d | ||
|
|
d2d2b845c5 | ||
|
|
96080f84a6 | ||
|
|
2b6218e564 | ||
|
|
24edf32da8 | ||
|
|
51b08f700c | ||
|
|
58eac7f595 | ||
|
|
32e8b01cf0 | ||
|
|
db9d5cb45d | ||
|
|
473cb7b951 | ||
|
|
e5cced8813 | ||
|
|
73848d3867 | ||
|
|
6a62167f79 | ||
|
|
6ff437671e | ||
|
|
30f9e9c782 | ||
|
|
e4caa7a856 | ||
|
|
aaa66b3172 | ||
|
|
0ae59b900c | ||
|
|
5a5dae1987 | ||
|
|
ac7ba95d65 | ||
|
|
e3c20b03bd | ||
|
|
4db50d4158 | ||
|
|
4154493640 | ||
|
|
61aad5a162 | ||
|
|
98c043bf50 | ||
|
|
f610133660 | ||
|
|
fe7ff5b148 | ||
|
|
5417de4219 | ||
|
|
422998d7f0 | ||
|
|
68628bf952 | ||
|
|
e5efad89e0 | ||
|
|
e09bb5f5c0 | ||
|
|
24d11179d8 | ||
|
|
2ef8b6f350 | ||
|
|
5779f7ab1d | ||
|
|
43e0815def | ||
|
|
0c14f4ce08 | ||
|
|
34d8dbf9b2 | ||
|
|
b3b2bc6ad5 | ||
|
|
6ca54beba2 | ||
|
|
8673426d5c | ||
|
|
b272ac0119 | ||
|
|
574d89da14 | ||
|
|
16939c0bc8 | ||
|
|
6fc09a82fb | ||
|
|
d622f8d1bf | ||
|
|
28d178b5c1 | ||
|
|
4c69d536ac | ||
|
|
403fd06117 | ||
|
|
d9928eab66 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ package-lock.json
|
||||
.idea
|
||||
*.iml
|
||||
.cursor
|
||||
.qoder
|
||||
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
|
||||
@@ -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
312
docs/developers/sdk-java.md
Normal 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.
|
||||
@@ -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. | | |
|
||||
|
||||
@@ -59,6 +59,7 @@ Commands for managing AI tools and models.
|
||||
| ---------------- | --------------------------------------------- | --------------------------------------------- |
|
||||
| `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` |
|
||||
| `/tools` | Display currently available tool list | `/tools`, `/tools desc` |
|
||||
| `/skills` | List and run available skills (experimental) | `/skills`, `/skills <name>` |
|
||||
| `/approval-mode` | Change approval mode for tool usage | `/approval-mode <mode (auto-edit)> --project` |
|
||||
| →`plan` | Analysis only, no execution | Secure review |
|
||||
| →`default` | Require approval for edits | Daily use |
|
||||
|
||||
@@ -27,6 +27,14 @@ Agent Skills package expertise into discoverable capabilities. Each Skill consis
|
||||
|
||||
Skills are **model-invoked** — the model autonomously decides when to use them based on your request and the Skill’s description. This is different from slash commands, which are **user-invoked** (you explicitly type `/command`).
|
||||
|
||||
If you want to invoke a Skill explicitly, use the `/skills` slash command:
|
||||
|
||||
```bash
|
||||
/skills <skill-name>
|
||||
```
|
||||
|
||||
The `/skills` command is only available when you run with `--experimental-skills`. Use autocomplete to browse available Skills and descriptions.
|
||||
|
||||
### Benefits
|
||||
|
||||
- Extend Qwen Code for your workflows
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"Qwen Code": {
|
||||
"type": "custom",
|
||||
"command": "qwen",
|
||||
"args": ["--experimental-acp"],
|
||||
"args": ["--acp"],
|
||||
"env": {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -6216,10 +6216,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -13882,10 +13879,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
@@ -17316,7 +17310,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"dependencies": {
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -17953,7 +17947,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.36.1",
|
||||
@@ -17974,6 +17968,7 @@
|
||||
"ajv-formats": "^3.0.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chardet": "^2.1.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^17.1.0",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
@@ -21413,7 +21408,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
@@ -21425,7 +21420,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "qwen-code-vscode-ide-companion",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"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"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "1.30.0",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -342,6 +344,7 @@ export async function main() {
|
||||
extensionEnablementManager,
|
||||
argv,
|
||||
);
|
||||
registerCleanup(() => config.shutdown());
|
||||
|
||||
if (config.getListExtensions()) {
|
||||
console.log('Installed extensions:');
|
||||
|
||||
1073
packages/cli/src/i18n/locales/de.js
Normal file
1073
packages/cli/src/i18n/locales/de.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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...",
|
||||
|
||||
@@ -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...',
|
||||
'Раскручиваем колесо для хомяка...',
|
||||
'Это не баг, а фича...',
|
||||
|
||||
@@ -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': '选择主题',
|
||||
|
||||
@@ -630,6 +630,67 @@ describe('BaseJsonOutputAdapter', () => {
|
||||
|
||||
expect(state.blocks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should preserve whitespace in thinking content', () => {
|
||||
const state = adapter.exposeCreateMessageState();
|
||||
adapter.startAssistantMessage();
|
||||
|
||||
adapter.exposeAppendThinking(
|
||||
state,
|
||||
'',
|
||||
'The user just said "Hello"',
|
||||
null,
|
||||
);
|
||||
|
||||
expect(state.blocks).toHaveLength(1);
|
||||
expect(state.blocks[0]).toMatchObject({
|
||||
type: 'thinking',
|
||||
thinking: 'The user just said "Hello"',
|
||||
});
|
||||
// Verify spaces are preserved
|
||||
const block = state.blocks[0] as { thinking: string };
|
||||
expect(block.thinking).toContain('user just');
|
||||
expect(block.thinking).not.toContain('userjust');
|
||||
});
|
||||
|
||||
it('should preserve whitespace when appending multiple thinking fragments', () => {
|
||||
const state = adapter.exposeCreateMessageState();
|
||||
adapter.startAssistantMessage();
|
||||
|
||||
// Simulate streaming thinking content in fragments
|
||||
adapter.exposeAppendThinking(state, '', 'The user just', null);
|
||||
adapter.exposeAppendThinking(state, '', ' said "Hello"', null);
|
||||
adapter.exposeAppendThinking(
|
||||
state,
|
||||
'',
|
||||
'. This is a simple greeting',
|
||||
null,
|
||||
);
|
||||
|
||||
expect(state.blocks).toHaveLength(1);
|
||||
const block = state.blocks[0] as { thinking: string };
|
||||
// Verify the complete text with all spaces preserved
|
||||
expect(block.thinking).toBe(
|
||||
'The user just said "Hello". This is a simple greeting',
|
||||
);
|
||||
// Verify specific space preservation
|
||||
expect(block.thinking).toContain('user just ');
|
||||
expect(block.thinking).toContain(' said');
|
||||
expect(block.thinking).toContain('". This');
|
||||
expect(block.thinking).not.toContain('userjust');
|
||||
expect(block.thinking).not.toContain('justsaid');
|
||||
});
|
||||
|
||||
it('should preserve leading and trailing whitespace in description', () => {
|
||||
const state = adapter.exposeCreateMessageState();
|
||||
adapter.startAssistantMessage();
|
||||
|
||||
adapter.exposeAppendThinking(state, '', ' content with spaces ', null);
|
||||
|
||||
expect(state.blocks).toHaveLength(1);
|
||||
const block = state.blocks[0] as { thinking: string };
|
||||
expect(block.thinking).toBe(' content with spaces ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendToolUse', () => {
|
||||
|
||||
@@ -816,9 +816,18 @@ export abstract class BaseJsonOutputAdapter {
|
||||
parentToolUseId?: string | null,
|
||||
): void {
|
||||
const actualParentToolUseId = parentToolUseId ?? null;
|
||||
const fragment = [subject?.trim(), description?.trim()]
|
||||
.filter((value) => value && value.length > 0)
|
||||
.join(': ');
|
||||
|
||||
// Build fragment without trimming to preserve whitespace in streaming content
|
||||
// Only filter out null/undefined/empty values
|
||||
const parts: string[] = [];
|
||||
if (subject && subject.length > 0) {
|
||||
parts.push(subject);
|
||||
}
|
||||
if (description && description.length > 0) {
|
||||
parts.push(description);
|
||||
}
|
||||
|
||||
const fragment = parts.join(': ');
|
||||
if (!fragment) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,6 +323,68 @@ describe('StreamJsonOutputAdapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve whitespace in thinking content (issue #1356)', () => {
|
||||
adapter.processEvent({
|
||||
type: GeminiEventType.Thought,
|
||||
value: {
|
||||
subject: '',
|
||||
description: 'The user just said "Hello"',
|
||||
},
|
||||
});
|
||||
|
||||
const message = adapter.finalizeAssistantMessage();
|
||||
expect(message.message.content).toHaveLength(1);
|
||||
const block = message.message.content[0] as {
|
||||
type: string;
|
||||
thinking: string;
|
||||
};
|
||||
expect(block.type).toBe('thinking');
|
||||
expect(block.thinking).toBe('The user just said "Hello"');
|
||||
// Verify spaces are preserved
|
||||
expect(block.thinking).toContain('user just');
|
||||
expect(block.thinking).not.toContain('userjust');
|
||||
});
|
||||
|
||||
it('should preserve whitespace when streaming multiple thinking fragments (issue #1356)', () => {
|
||||
// Simulate streaming thinking content in multiple events
|
||||
adapter.processEvent({
|
||||
type: GeminiEventType.Thought,
|
||||
value: {
|
||||
subject: '',
|
||||
description: 'The user just',
|
||||
},
|
||||
});
|
||||
adapter.processEvent({
|
||||
type: GeminiEventType.Thought,
|
||||
value: {
|
||||
subject: '',
|
||||
description: ' said "Hello"',
|
||||
},
|
||||
});
|
||||
adapter.processEvent({
|
||||
type: GeminiEventType.Thought,
|
||||
value: {
|
||||
subject: '',
|
||||
description: '. This is a simple greeting',
|
||||
},
|
||||
});
|
||||
|
||||
const message = adapter.finalizeAssistantMessage();
|
||||
expect(message.message.content).toHaveLength(1);
|
||||
const block = message.message.content[0] as {
|
||||
type: string;
|
||||
thinking: string;
|
||||
};
|
||||
expect(block.thinking).toBe(
|
||||
'The user just said "Hello". This is a simple greeting',
|
||||
);
|
||||
// Verify specific spaces are preserved
|
||||
expect(block.thinking).toContain('user just ');
|
||||
expect(block.thinking).toContain(' said');
|
||||
expect(block.thinking).not.toContain('userjust');
|
||||
expect(block.thinking).not.toContain('justsaid');
|
||||
});
|
||||
|
||||
it('should append tool use from ToolCallRequest events', () => {
|
||||
adapter.processEvent({
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
|
||||
@@ -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(
|
||||
@@ -1823,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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -335,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(
|
||||
@@ -345,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
|
||||
|
||||
@@ -31,6 +31,7 @@ import { quitCommand } from '../ui/commands/quitCommand.js';
|
||||
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
||||
import { resumeCommand } from '../ui/commands/resumeCommand.js';
|
||||
import { settingsCommand } from '../ui/commands/settingsCommand.js';
|
||||
import { skillsCommand } from '../ui/commands/skillsCommand.js';
|
||||
import { statsCommand } from '../ui/commands/statsCommand.js';
|
||||
import { summaryCommand } from '../ui/commands/summaryCommand.js';
|
||||
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
|
||||
@@ -78,6 +79,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
quitCommand,
|
||||
restoreCommand(this.config),
|
||||
resumeCommand,
|
||||
...(this.config?.getExperimentalSkills?.() ? [skillsCommand] : []),
|
||||
statsCommand,
|
||||
summaryCommand,
|
||||
themeCommand,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
132
packages/cli/src/ui/commands/skillsCommand.ts
Normal file
132
packages/cli/src/ui/commands/skillsCommand.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
CommandKind,
|
||||
type CommandCompletionItem,
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
} from './types.js';
|
||||
import { MessageType, type HistoryItemSkillsList } from '../types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { AsyncFzf } from 'fzf';
|
||||
import type { SkillConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
export const skillsCommand: SlashCommand = {
|
||||
name: 'skills',
|
||||
get description() {
|
||||
return t('List available skills.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context: CommandContext, args?: string) => {
|
||||
const rawArgs = args?.trim() ?? '';
|
||||
const [skillName = ''] = rawArgs.split(/\s+/);
|
||||
|
||||
const skillManager = context.services.config?.getSkillManager();
|
||||
if (!skillManager) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: t('Could not retrieve skill manager.'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const skills = await skillManager.listSkills();
|
||||
if (skills.length === 0) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: t('No skills are currently available.'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!skillName) {
|
||||
const sortedSkills = [...skills].sort((left, right) =>
|
||||
left.name.localeCompare(right.name),
|
||||
);
|
||||
const skillsListItem: HistoryItemSkillsList = {
|
||||
type: MessageType.SKILLS_LIST,
|
||||
skills: sortedSkills.map((skill) => ({ name: skill.name })),
|
||||
};
|
||||
context.ui.addItem(skillsListItem, Date.now());
|
||||
return;
|
||||
}
|
||||
const normalizedName = skillName.toLowerCase();
|
||||
const hasSkill = skills.some(
|
||||
(skill) => skill.name.toLowerCase() === normalizedName,
|
||||
);
|
||||
|
||||
if (!hasSkill) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: t('Unknown skill: {{name}}', { name: skillName }),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawInput = context.invocation?.raw ?? `/skills ${rawArgs}`;
|
||||
return {
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: rawInput }],
|
||||
};
|
||||
},
|
||||
completion: async (
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
): Promise<CommandCompletionItem[]> => {
|
||||
const skillManager = context.services.config?.getSkillManager();
|
||||
if (!skillManager) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const skills = await skillManager.listSkills();
|
||||
const normalizedPartial = partialArg.trim();
|
||||
const matches = await getSkillMatches(skills, normalizedPartial);
|
||||
|
||||
return matches.map((skill) => ({
|
||||
value: skill.name,
|
||||
description: skill.description,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
async function getSkillMatches(
|
||||
skills: SkillConfig[],
|
||||
query: string,
|
||||
): Promise<SkillConfig[]> {
|
||||
if (!query) {
|
||||
return skills;
|
||||
}
|
||||
|
||||
const names = skills.map((skill) => skill.name);
|
||||
const skillMap = new Map(skills.map((skill) => [skill.name, skill]));
|
||||
|
||||
try {
|
||||
const fzf = new AsyncFzf(names, {
|
||||
fuzzy: 'v2',
|
||||
casing: 'case-insensitive',
|
||||
});
|
||||
const results = (await fzf.find(query)) as Array<{ item: string }>;
|
||||
return results
|
||||
.map((result) => skillMap.get(result.item))
|
||||
.filter((skill): skill is SkillConfig => !!skill);
|
||||
} catch (error) {
|
||||
console.error('[skillsCommand] Fuzzy match failed:', error);
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return skills.filter((skill) =>
|
||||
skill.name.toLowerCase().startsWith(lowerQuery),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,12 @@ export enum CommandKind {
|
||||
MCP_PROMPT = 'mcp-prompt',
|
||||
}
|
||||
|
||||
export interface CommandCompletionItem {
|
||||
value: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// The standardized contract for any command in the system.
|
||||
export interface SlashCommand {
|
||||
name: string;
|
||||
@@ -234,7 +240,7 @@ export interface SlashCommand {
|
||||
completion?: (
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
) => Promise<string[]>;
|
||||
) => Promise<Array<string | CommandCompletionItem> | null>;
|
||||
|
||||
subCommands?: SlashCommand[];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Help } from './Help.js';
|
||||
import type { SlashCommand } from '../commands/types.js';
|
||||
import { ExtensionsList } from './views/ExtensionsList.js';
|
||||
import { getMCPServerStatus } from '@qwen-code/qwen-code-core';
|
||||
import { SkillsList } from './views/SkillsList.js';
|
||||
import { ToolsList } from './views/ToolsList.js';
|
||||
import { McpStatus } from './views/McpStatus.js';
|
||||
|
||||
@@ -153,6 +154,9 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
showDescriptions={itemForDisplay.showDescriptions}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'skills_list' && (
|
||||
<SkillsList skills={itemForDisplay.skills} />
|
||||
)}
|
||||
{itemForDisplay.type === 'mcp_status' && (
|
||||
<McpStatus {...itemForDisplay} serverStatus={getMCPServerStatus} />
|
||||
)}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -106,7 +106,7 @@ export function SuggestionsDisplay({
|
||||
</Box>
|
||||
|
||||
{suggestion.description && (
|
||||
<Box flexGrow={1} paddingLeft={3}>
|
||||
<Box flexGrow={1} paddingLeft={2}>
|
||||
<Text color={textColor} wrap="truncate">
|
||||
{suggestion.description}
|
||||
</Text>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const InfoMessage: React.FC<InfoMessageProps> = ({ text }) => {
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.status.warning}>{prefix}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const WarningMessage: React.FC<WarningMessageProps> = ({ text }) => {
|
||||
const prefixWidth = 3;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={Colors.AccentYellow}>{prefix}</Text>
|
||||
</Box>
|
||||
|
||||
36
packages/cli/src/ui/components/views/SkillsList.tsx
Normal file
36
packages/cli/src/ui/components/views/SkillsList.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type SkillDefinition } from '../../types.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
interface SkillsListProps {
|
||||
skills: readonly SkillDefinition[];
|
||||
}
|
||||
|
||||
export const SkillsList: React.FC<SkillsListProps> = ({ skills }) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{t('Available skills:')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{skills.length > 0 ? (
|
||||
skills.map((skill) => (
|
||||
<Box key={skill.name} flexDirection="row">
|
||||
<Text color={theme.text.primary}>{' '}- </Text>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{skill.name}
|
||||
</Text>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Text color={theme.text.primary}> {t('No skills available')}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -573,6 +573,45 @@ describe('useSlashCompletion', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should map completion items with descriptions for argument suggestions', async () => {
|
||||
const mockCompletionFn = vi.fn().mockResolvedValue([
|
||||
{ value: 'pdf', description: 'Create PDF documents' },
|
||||
{ value: 'xlsx', description: 'Work with spreadsheets' },
|
||||
]);
|
||||
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'skills',
|
||||
description: 'List available skills',
|
||||
completion: mockCompletionFn,
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/skills ',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'pdf',
|
||||
value: 'pdf',
|
||||
description: 'Create PDF documents',
|
||||
},
|
||||
{
|
||||
label: 'xlsx',
|
||||
value: 'xlsx',
|
||||
description: 'Work with spreadsheets',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call command.completion with an empty string when args start with a space', async () => {
|
||||
const mockCompletionFn = vi
|
||||
.fn()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AsyncFzf } from 'fzf';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
import {
|
||||
CommandKind,
|
||||
type CommandCompletionItem,
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
} from '../commands/types.js';
|
||||
@@ -215,10 +216,9 @@ function useCommandSuggestions(
|
||||
)) || [];
|
||||
|
||||
if (!signal.aborted) {
|
||||
const finalSuggestions = results.map((s) => ({
|
||||
label: s,
|
||||
value: s,
|
||||
}));
|
||||
const finalSuggestions = results
|
||||
.map((item) => toSuggestion(item))
|
||||
.filter((suggestion): suggestion is Suggestion => !!suggestion);
|
||||
setSuggestions(finalSuggestions);
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -310,6 +310,20 @@ function useCommandSuggestions(
|
||||
return { suggestions, isLoading };
|
||||
}
|
||||
|
||||
function toSuggestion(item: string | CommandCompletionItem): Suggestion | null {
|
||||
if (typeof item === 'string') {
|
||||
return { label: item, value: item };
|
||||
}
|
||||
if (!item.value) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: item.label ?? item.value,
|
||||
value: item.value,
|
||||
description: item.description,
|
||||
};
|
||||
}
|
||||
|
||||
function useCompletionPositions(
|
||||
query: string | null,
|
||||
parserResult: CommandParserResult,
|
||||
|
||||
@@ -201,12 +201,21 @@ export interface ToolDefinition {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SkillDefinition {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type HistoryItemToolsList = HistoryItemBase & {
|
||||
type: 'tools_list';
|
||||
tools: ToolDefinition[];
|
||||
showDescriptions: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItemSkillsList = HistoryItemBase & {
|
||||
type: 'skills_list';
|
||||
skills: SkillDefinition[];
|
||||
};
|
||||
|
||||
// JSON-friendly types for using as a simple data model showing info about an
|
||||
// MCP Server.
|
||||
export interface JsonMcpTool {
|
||||
@@ -268,6 +277,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemCompression
|
||||
| HistoryItemExtensionsList
|
||||
| HistoryItemToolsList
|
||||
| HistoryItemSkillsList
|
||||
| HistoryItemMcpStatus;
|
||||
|
||||
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
||||
@@ -289,6 +299,7 @@ export enum MessageType {
|
||||
SUMMARY = 'summary',
|
||||
EXTENSIONS_LIST = 'extensions_list',
|
||||
TOOLS_LIST = 'tools_list',
|
||||
SKILLS_LIST = 'skills_list',
|
||||
MCP_STATUS = 'mcp_status',
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "Qwen Code Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,7 +27,6 @@
|
||||
"@google/genai": "1.30.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.203.0",
|
||||
@@ -40,7 +39,9 @@
|
||||
"@xterm/headless": "5.5.0",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chardet": "^2.1.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^17.1.0",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
|
||||
@@ -650,6 +650,7 @@ export class Config {
|
||||
this.promptRegistry = new PromptRegistry();
|
||||
this.subagentManager = new SubagentManager(this);
|
||||
this.skillManager = new SkillManager(this);
|
||||
await this.skillManager.startWatching();
|
||||
|
||||
// Load session subagents if they were provided before initialization
|
||||
if (this.sessionSubagents.length > 0) {
|
||||
@@ -734,6 +735,13 @@ export class Config {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources owned by the config instance.
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
this.skillManager?.stopWatching();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new session and resets session-scoped services.
|
||||
*/
|
||||
|
||||
@@ -824,7 +824,6 @@ export class CoreToolScheduler {
|
||||
*/
|
||||
const shouldAutoDeny =
|
||||
!this.config.isInteractive() &&
|
||||
!this.config.getIdeMode() &&
|
||||
!this.config.getExperimentalZedIntegration() &&
|
||||
this.config.getInputFormat() !== InputFormat.STREAM_JSON;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 model’s 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 {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,8 +58,6 @@ export class DefaultOpenAICompatibleProvider
|
||||
}
|
||||
|
||||
getDefaultGenerationConfig(): GenerateContentConfig {
|
||||
return {
|
||||
topP: 0.95,
|
||||
};
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as fsSync from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { watch as watchFs, type FSWatcher } from 'chokidar';
|
||||
import { parse as parseYaml } from '../utils/yaml-parser.js';
|
||||
import type {
|
||||
SkillConfig,
|
||||
@@ -29,6 +31,9 @@ export class SkillManager {
|
||||
private skillsCache: Map<SkillLevel, SkillConfig[]> | null = null;
|
||||
private readonly changeListeners: Set<() => void> = new Set();
|
||||
private parseErrors: Map<string, SkillError> = new Map();
|
||||
private readonly watchers: Map<string, FSWatcher> = new Map();
|
||||
private watchStarted = false;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(private readonly config: Config) {}
|
||||
|
||||
@@ -221,6 +226,36 @@ export class SkillManager {
|
||||
this.notifyChangeListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts watching skill directories for changes.
|
||||
*/
|
||||
async startWatching(): Promise<void> {
|
||||
if (this.watchStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.watchStarted = true;
|
||||
await this.refreshCache();
|
||||
this.updateWatchersFromCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops watching skill directories for changes.
|
||||
*/
|
||||
stopWatching(): void {
|
||||
for (const watcher of this.watchers.values()) {
|
||||
void watcher.close().catch((error) => {
|
||||
console.warn('Failed to close skills watcher:', error);
|
||||
});
|
||||
}
|
||||
this.watchers.clear();
|
||||
this.watchStarted = false;
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a SKILL.md file and returns the configuration.
|
||||
*
|
||||
@@ -449,4 +484,77 @@ export class SkillManager {
|
||||
this.skillsCache.set(level, levelSkills);
|
||||
}
|
||||
}
|
||||
|
||||
private updateWatchersFromCache(): void {
|
||||
const desiredPaths = new Set<string>();
|
||||
|
||||
for (const level of ['project', 'user'] as const) {
|
||||
const baseDir = this.getSkillsBaseDir(level);
|
||||
const parentDir = path.dirname(baseDir);
|
||||
if (fsSync.existsSync(parentDir)) {
|
||||
desiredPaths.add(parentDir);
|
||||
}
|
||||
if (fsSync.existsSync(baseDir)) {
|
||||
desiredPaths.add(baseDir);
|
||||
}
|
||||
|
||||
const levelSkills = this.skillsCache?.get(level) || [];
|
||||
for (const skill of levelSkills) {
|
||||
const skillDir = path.dirname(skill.filePath);
|
||||
if (fsSync.existsSync(skillDir)) {
|
||||
desiredPaths.add(skillDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingPath of this.watchers.keys()) {
|
||||
if (!desiredPaths.has(existingPath)) {
|
||||
void this.watchers
|
||||
.get(existingPath)
|
||||
?.close()
|
||||
.catch((error) => {
|
||||
console.warn(
|
||||
`Failed to close skills watcher for ${existingPath}:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
this.watchers.delete(existingPath);
|
||||
}
|
||||
}
|
||||
|
||||
for (const watchPath of desiredPaths) {
|
||||
if (this.watchers.has(watchPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const watcher = watchFs(watchPath, {
|
||||
ignoreInitial: true,
|
||||
})
|
||||
.on('all', () => {
|
||||
this.scheduleRefresh();
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.warn(`Skills watcher error for ${watchPath}:`, error);
|
||||
});
|
||||
this.watchers.set(watchPath, watcher);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to watch skills directory at ${watchPath}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleRefresh(): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
}
|
||||
|
||||
this.refreshTimer = setTimeout(() => {
|
||||
this.refreshTimer = null;
|
||||
void this.refreshCache().then(() => this.updateWatchersFromCache());
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,66 +2,88 @@
|
||||
|
||||
exports[`ShellTool > getDescription > should return the non-windows description when not on windows 1`] = `
|
||||
"This tool executes a given shell command as \`bash -c <command>\`. Command can start background processes using \`&\`. Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.
|
||||
**Usage notes**:
|
||||
- The command argument is required.
|
||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||
|
||||
**Background vs Foreground Execution:**
|
||||
You should decide whether commands should run in background or foreground based on their nature:
|
||||
|
||||
**Use background execution (is_background: true) for:**
|
||||
- Long-running development servers: \`npm run start\`, \`npm run dev\`, \`yarn dev\`, \`bun run start\`
|
||||
- Build watchers: \`npm run watch\`, \`webpack --watch\`
|
||||
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
|
||||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
**Use foreground execution (is_background: false) for:**
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
- Installation commands: \`npm install\`, \`pip install\`
|
||||
- Git operations: \`git commit\`, \`git push\`
|
||||
- Test runs: \`npm test\`, \`pytest\`
|
||||
|
||||
The following information is returned:
|
||||
- Avoid using run_shell_command with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||
- File search: Use glob (NOT find or ls)
|
||||
- Content search: Use grep_search (NOT grep or rg)
|
||||
- Read files: Use read_file (NOT cat/head/tail)
|
||||
- Edit files: Use edit (NOT sed/awk)
|
||||
- Write files: Use write_file (NOT echo >/cat <<EOF)
|
||||
- Communication: Output text directly (NOT echo/printf)
|
||||
- When issuing multiple commands:
|
||||
- If the commands are independent and can run in parallel, make multiple run_shell_command tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two run_shell_command tool calls in parallel.
|
||||
- If the commands depend on each other and must run sequentially, use a single run_shell_command call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before run_shell_command for git operations, or git add before git commit), run these operations sequentially instead.
|
||||
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
||||
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
|
||||
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
|
||||
<good-example>
|
||||
pytest /foo/bar/tests
|
||||
</good-example>
|
||||
<bad-example>
|
||||
cd /foo/bar && pytest tests
|
||||
</bad-example>
|
||||
|
||||
Command: Executed command.
|
||||
Directory: Directory where command was executed, or \`(root)\`.
|
||||
Stdout: Output on stdout stream. Can be \`(empty)\` or partial on error and for any unwaited background processes.
|
||||
Stderr: Output on stderr stream. Can be \`(empty)\` or partial on error and for any unwaited background processes.
|
||||
Error: Error or \`(none)\` if no error was reported for the subprocess.
|
||||
Exit Code: Exit code or \`(none)\` if terminated by signal.
|
||||
Signal: Signal number or \`(none)\` if no signal was received.
|
||||
Background PIDs: List of background processes started or \`(none)\`.
|
||||
Process Group PGID: Process group started or \`(none)\`"
|
||||
**Background vs Foreground Execution:**
|
||||
You should decide whether commands should run in background or foreground based on their nature:
|
||||
|
||||
**Use background execution (is_background: true) for:**
|
||||
- Long-running development servers: \`npm run start\`, \`npm run dev\`, \`yarn dev\`, \`bun run start\`
|
||||
- Build watchers: \`npm run watch\`, \`webpack --watch\`
|
||||
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
|
||||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
**Use foreground execution (is_background: false) for:**
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
- Installation commands: \`npm install\`, \`pip install\`
|
||||
- Git operations: \`git commit\`, \`git push\`
|
||||
- Test runs: \`npm test\`, \`pytest\`"
|
||||
`;
|
||||
|
||||
exports[`ShellTool > getDescription > should return the windows description when on windows 1`] = `
|
||||
"This tool executes a given shell command as \`cmd.exe /c <command>\`. Command can start background processes using \`start /b\`.
|
||||
**Usage notes**:
|
||||
- The command argument is required.
|
||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||
|
||||
**Background vs Foreground Execution:**
|
||||
You should decide whether commands should run in background or foreground based on their nature:
|
||||
|
||||
**Use background execution (is_background: true) for:**
|
||||
- Long-running development servers: \`npm run start\`, \`npm run dev\`, \`yarn dev\`, \`bun run start\`
|
||||
- Build watchers: \`npm run watch\`, \`webpack --watch\`
|
||||
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
|
||||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
**Use foreground execution (is_background: false) for:**
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
- Installation commands: \`npm install\`, \`pip install\`
|
||||
- Git operations: \`git commit\`, \`git push\`
|
||||
- Test runs: \`npm test\`, \`pytest\`
|
||||
|
||||
The following information is returned:
|
||||
- Avoid using run_shell_command with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||
- File search: Use glob (NOT find or ls)
|
||||
- Content search: Use grep_search (NOT grep or rg)
|
||||
- Read files: Use read_file (NOT cat/head/tail)
|
||||
- Edit files: Use edit (NOT sed/awk)
|
||||
- Write files: Use write_file (NOT echo >/cat <<EOF)
|
||||
- Communication: Output text directly (NOT echo/printf)
|
||||
- When issuing multiple commands:
|
||||
- If the commands are independent and can run in parallel, make multiple run_shell_command tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two run_shell_command tool calls in parallel.
|
||||
- If the commands depend on each other and must run sequentially, use a single run_shell_command call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before run_shell_command for git operations, or git add before git commit), run these operations sequentially instead.
|
||||
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
||||
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
|
||||
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
|
||||
<good-example>
|
||||
pytest /foo/bar/tests
|
||||
</good-example>
|
||||
<bad-example>
|
||||
cd /foo/bar && pytest tests
|
||||
</bad-example>
|
||||
|
||||
Command: Executed command.
|
||||
Directory: Directory where command was executed, or \`(root)\`.
|
||||
Stdout: Output on stdout stream. Can be \`(empty)\` or partial on error and for any unwaited background processes.
|
||||
Stderr: Output on stderr stream. Can be \`(empty)\` or partial on error and for any unwaited background processes.
|
||||
Error: Error or \`(none)\` if no error was reported for the subprocess.
|
||||
Exit Code: Exit code or \`(none)\` if terminated by signal.
|
||||
Signal: Signal number or \`(none)\` if no signal was received.
|
||||
Background PIDs: List of background processes started or \`(none)\`.
|
||||
Process Group PGID: Process group started or \`(none)\`"
|
||||
**Background vs Foreground Execution:**
|
||||
You should decide whether commands should run in background or foreground based on their nature:
|
||||
|
||||
**Use background execution (is_background: true) for:**
|
||||
- Long-running development servers: \`npm run start\`, \`npm run dev\`, \`yarn dev\`, \`bun run start\`
|
||||
- Build watchers: \`npm run watch\`, \`webpack --watch\`
|
||||
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
|
||||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
**Use foreground execution (is_background: false) for:**
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
- Installation commands: \`npm install\`, \`pip install\`
|
||||
- Git operations: \`git commit\`, \`git push\`
|
||||
- Test runs: \`npm test\`, \`pytest\`"
|
||||
`;
|
||||
|
||||
@@ -59,6 +59,9 @@ describe('ShellTool', () => {
|
||||
getWorkspaceContext: vi
|
||||
.fn()
|
||||
.mockReturnValue(createMockWorkspaceContext('/test/dir')),
|
||||
storage: {
|
||||
getUserSkillsDir: vi.fn().mockReturnValue('/test/dir/.qwen/skills'),
|
||||
},
|
||||
getGeminiClient: vi.fn(),
|
||||
getGitCoAuthor: vi.fn().mockReturnValue({
|
||||
enabled: true,
|
||||
@@ -142,6 +145,42 @@ describe('ShellTool', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error for a directory within the user skills directory', () => {
|
||||
expect(() =>
|
||||
shellTool.build({
|
||||
command: 'ls',
|
||||
directory: '/test/dir/.qwen/skills/my-skill',
|
||||
is_background: false,
|
||||
}),
|
||||
).toThrow(
|
||||
'Explicitly running shell commands from within the user skills directory is not allowed. Please use absolute paths for command parameter instead.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error for the user skills directory itself', () => {
|
||||
expect(() =>
|
||||
shellTool.build({
|
||||
command: 'ls',
|
||||
directory: '/test/dir/.qwen/skills',
|
||||
is_background: false,
|
||||
}),
|
||||
).toThrow(
|
||||
'Explicitly running shell commands from within the user skills directory is not allowed. Please use absolute paths for command parameter instead.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve directory path before checking user skills directory', () => {
|
||||
expect(() =>
|
||||
shellTool.build({
|
||||
command: 'ls',
|
||||
directory: '/test/dir/.qwen/skills/../skills/my-skill',
|
||||
is_background: false,
|
||||
}),
|
||||
).toThrow(
|
||||
'Explicitly running shell commands from within the user skills directory is not allowed. Please use absolute paths for command parameter instead.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an invocation for a valid absolute directory path', () => {
|
||||
(mockConfig.getWorkspaceContext as Mock).mockReturnValue(
|
||||
createMockWorkspaceContext('/test/dir', ['/another/workspace']),
|
||||
@@ -248,7 +287,7 @@ describe('ShellTool', () => {
|
||||
wrappedCommand,
|
||||
'/test/dir',
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -275,7 +314,7 @@ describe('ShellTool', () => {
|
||||
wrappedCommand,
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -300,7 +339,7 @@ describe('ShellTool', () => {
|
||||
wrappedCommand,
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -325,7 +364,7 @@ describe('ShellTool', () => {
|
||||
wrappedCommand,
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -350,7 +389,7 @@ describe('ShellTool', () => {
|
||||
wrappedCommand,
|
||||
'/test/dir/subdir',
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -378,7 +417,7 @@ describe('ShellTool', () => {
|
||||
'dir',
|
||||
'/test/dir',
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -471,7 +510,7 @@ describe('ShellTool', () => {
|
||||
expect(summarizer.summarizeToolOutput).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
mockConfig.getGeminiClient(),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
1000,
|
||||
);
|
||||
expect(result.llmContent).toBe('summarized output');
|
||||
@@ -580,7 +619,7 @@ describe('ShellTool', () => {
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -610,7 +649,7 @@ describe('ShellTool', () => {
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -640,7 +679,7 @@ describe('ShellTool', () => {
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -699,7 +738,7 @@ describe('ShellTool', () => {
|
||||
expect.stringContaining('npm install'),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -728,7 +767,7 @@ describe('ShellTool', () => {
|
||||
expect.stringContaining('git commit'),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -758,7 +797,7 @@ describe('ShellTool', () => {
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -794,7 +833,7 @@ describe('ShellTool', () => {
|
||||
expect.stringContaining('git commit -m "Initial commit"'),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -831,7 +870,7 @@ describe('ShellTool', () => {
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
{},
|
||||
);
|
||||
@@ -962,4 +1001,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,
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
import { ShellExecutionService } from '../services/shellExecutionService.js';
|
||||
import { formatMemoryUsage } from '../utils/formatters.js';
|
||||
import type { AnsiOutput } from '../utils/terminalSerializer.js';
|
||||
import { isSubpath } from '../utils/paths.js';
|
||||
import {
|
||||
getCommandRoots,
|
||||
isCommandAllowed,
|
||||
@@ -143,11 +144,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 +183,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
commandToExecute,
|
||||
cwd,
|
||||
(event: ShellOutputEvent) => {
|
||||
if (!updateOutput) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldUpdate = false;
|
||||
|
||||
switch (event.type) {
|
||||
@@ -201,7 +211,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
if (shouldUpdate && updateOutput) {
|
||||
updateOutput(
|
||||
typeof cumulativeOutput === 'string'
|
||||
? cumulativeOutput
|
||||
@@ -219,6 +229,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[] = [];
|
||||
@@ -383,35 +408,46 @@ Co-authored-by: ${gitCoAuthorSettings.name} <${gitCoAuthorSettings.email}>`;
|
||||
|
||||
function getShellToolDescription(): string {
|
||||
const toolDescription = `
|
||||
**Usage notes**:
|
||||
- The command argument is required.
|
||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||
|
||||
**Background vs Foreground Execution:**
|
||||
You should decide whether commands should run in background or foreground based on their nature:
|
||||
|
||||
**Use background execution (is_background: true) for:**
|
||||
- Long-running development servers: \`npm run start\`, \`npm run dev\`, \`yarn dev\`, \`bun run start\`
|
||||
- Build watchers: \`npm run watch\`, \`webpack --watch\`
|
||||
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
|
||||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
**Use foreground execution (is_background: false) for:**
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
- Installation commands: \`npm install\`, \`pip install\`
|
||||
- Git operations: \`git commit\`, \`git push\`
|
||||
- Test runs: \`npm test\`, \`pytest\`
|
||||
|
||||
The following information is returned:
|
||||
- Avoid using run_shell_command with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||
- File search: Use glob (NOT find or ls)
|
||||
- Content search: Use grep_search (NOT grep or rg)
|
||||
- Read files: Use read_file (NOT cat/head/tail)
|
||||
- Edit files: Use edit (NOT sed/awk)
|
||||
- Write files: Use write_file (NOT echo >/cat <<EOF)
|
||||
- Communication: Output text directly (NOT echo/printf)
|
||||
- When issuing multiple commands:
|
||||
- If the commands are independent and can run in parallel, make multiple run_shell_command tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two run_shell_command tool calls in parallel.
|
||||
- If the commands depend on each other and must run sequentially, use a single run_shell_command call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before run_shell_command for git operations, or git add before git commit), run these operations sequentially instead.
|
||||
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
||||
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
|
||||
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
|
||||
<good-example>
|
||||
pytest /foo/bar/tests
|
||||
</good-example>
|
||||
<bad-example>
|
||||
cd /foo/bar && pytest tests
|
||||
</bad-example>
|
||||
|
||||
Command: Executed command.
|
||||
Directory: Directory where command was executed, or \`(root)\`.
|
||||
Stdout: Output on stdout stream. Can be \`(empty)\` or partial on error and for any unwaited background processes.
|
||||
Stderr: Output on stderr stream. Can be \`(empty)\` or partial on error and for any unwaited background processes.
|
||||
Error: Error or \`(none)\` if no error was reported for the subprocess.
|
||||
Exit Code: Exit code or \`(none)\` if terminated by signal.
|
||||
Signal: Signal number or \`(none)\` if no signal was received.
|
||||
Background PIDs: List of background processes started or \`(none)\`.
|
||||
Process Group PGID: Process group started or \`(none)\``;
|
||||
**Background vs Foreground Execution:**
|
||||
You should decide whether commands should run in background or foreground based on their nature:
|
||||
|
||||
**Use background execution (is_background: true) for:**
|
||||
- Long-running development servers: \`npm run start\`, \`npm run dev\`, \`yarn dev\`, \`bun run start\`
|
||||
- Build watchers: \`npm run watch\`, \`webpack --watch\`
|
||||
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
|
||||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
**Use foreground execution (is_background: false) for:**
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
- Installation commands: \`npm install\`, \`pip install\`
|
||||
- Git operations: \`git commit\`, \`git push\`
|
||||
- Test runs: \`npm test\`, \`pytest\``;
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
return `This tool executes a given shell command as \`cmd.exe /c <command>\`. Command can start background processes using \`start /b\`.${toolDescription}`;
|
||||
@@ -502,6 +538,17 @@ export class ShellTool extends BaseDeclarativeTool<
|
||||
if (!path.isAbsolute(params.directory)) {
|
||||
return 'Directory must be an absolute path.';
|
||||
}
|
||||
|
||||
const userSkillsDir = this.config.storage.getUserSkillsDir();
|
||||
const resolvedDirectoryPath = path.resolve(params.directory);
|
||||
const isWithinUserSkills = isSubpath(
|
||||
userSkillsDir,
|
||||
resolvedDirectoryPath,
|
||||
);
|
||||
if (isWithinUserSkills) {
|
||||
return `Explicitly running shell commands from within the user skills directory is not allowed. Please use absolute paths for command parameter instead.`;
|
||||
}
|
||||
|
||||
const workspaceDirs = this.config.getWorkspaceContext().getDirectories();
|
||||
const isWithinWorkspace = workspaceDirs.some((wsDir) =>
|
||||
params.directory!.startsWith(wsDir),
|
||||
|
||||
@@ -324,7 +324,9 @@ describe('SkillTool', () => {
|
||||
'Review code for quality and best practices.',
|
||||
);
|
||||
|
||||
expect(result.returnDisplay).toBe('Launching skill: code-review');
|
||||
expect(result.returnDisplay).toBe(
|
||||
'Specialized skill for reviewing code quality',
|
||||
);
|
||||
});
|
||||
|
||||
it('should include allowedTools in result when present', async () => {
|
||||
@@ -349,7 +351,7 @@ describe('SkillTool', () => {
|
||||
// Base description is omitted from llmContent; ensure body is present.
|
||||
expect(llmText).toContain('Help write comprehensive tests.');
|
||||
|
||||
expect(result.returnDisplay).toBe('Launching skill: testing');
|
||||
expect(result.returnDisplay).toBe('Skill for writing and running tests');
|
||||
});
|
||||
|
||||
it('should handle skill not found error', async () => {
|
||||
@@ -416,7 +418,7 @@ describe('SkillTool', () => {
|
||||
).createInvocation(params);
|
||||
const description = invocation.getDescription();
|
||||
|
||||
expect(description).toBe('Launching skill: "code-review"');
|
||||
expect(description).toBe('Use skill: "code-review"');
|
||||
});
|
||||
|
||||
it('should handle skill without additional files', async () => {
|
||||
@@ -436,7 +438,9 @@ describe('SkillTool', () => {
|
||||
const llmText = partToString(result.llmContent);
|
||||
expect(llmText).not.toContain('## Additional Files');
|
||||
|
||||
expect(result.returnDisplay).toBe('Launching skill: code-review');
|
||||
expect(result.returnDisplay).toBe(
|
||||
'Specialized skill for reviewing code quality',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ export class SkillTool extends BaseDeclarativeTool<SkillParams, ToolResult> {
|
||||
'Execute a skill within the main conversation. Loading available skills...', // Initial description
|
||||
Kind.Read,
|
||||
initialSchema,
|
||||
true, // isOutputMarkdown
|
||||
false, // isOutputMarkdown
|
||||
false, // canUpdateOutput
|
||||
);
|
||||
|
||||
@@ -128,6 +128,10 @@ Important:
|
||||
- Only use skills listed in <available_skills> below
|
||||
- Do not invoke a skill that is already running
|
||||
- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
|
||||
- When executing scripts or loading referenced files, ALWAYS resolve absolute paths from skill's base directory. Examples:
|
||||
- \`bash scripts/init.sh\` -> \`bash /path/to/skill/scripts/init.sh\`
|
||||
- \`python scripts/helper.py\` -> \`python /path/to/skill/scripts/helper.py\`
|
||||
- \`reference.md\` -> \`/path/to/skill/reference.md\`
|
||||
</skills_instructions>
|
||||
|
||||
<available_skills>
|
||||
@@ -183,7 +187,7 @@ class SkillToolInvocation extends BaseToolInvocation<SkillParams, ToolResult> {
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Launching skill: "${this.params.skill}"`;
|
||||
return `Use skill: "${this.params.skill}"`;
|
||||
}
|
||||
|
||||
override async shouldConfirmExecute(): Promise<false> {
|
||||
@@ -238,16 +242,16 @@ class SkillToolInvocation extends BaseToolInvocation<SkillParams, ToolResult> {
|
||||
const baseDir = path.dirname(skill.filePath);
|
||||
|
||||
// Build markdown content for LLM (show base dir, then body)
|
||||
const llmContent = `Base directory for this skill: ${baseDir}\n\n${skill.body}\n`;
|
||||
const llmContent = `Base directory for this skill: ${baseDir}\nImportant: ALWAYS resolve absolute paths from this base directory when working with skills.\n\n${skill.body}\n`;
|
||||
|
||||
return {
|
||||
llmContent: [{ text: llmContent }],
|
||||
returnDisplay: `Launching skill: ${skill.name}`,
|
||||
returnDisplay: skill.description,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(`[SkillsTool] Error launching skill: ${errorMessage}`);
|
||||
console.error(`[SkillsTool] Error using skill: ${errorMessage}`);
|
||||
|
||||
// Log failed skill launch
|
||||
logSkillLaunch(
|
||||
|
||||
24
packages/sdk-java/.editorconfig
Normal file
24
packages/sdk-java/.editorconfig
Normal 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
14
packages/sdk-java/.gitignore
vendored
Normal 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
201
packages/sdk-java/LICENSE
Normal 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
378
packages/sdk-java/QWEN.md
Normal 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
312
packages/sdk-java/README.md
Normal 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.
|
||||
131
packages/sdk-java/checkstyle.xml
Normal file
131
packages/sdk-java/checkstyle.xml
Normal 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
193
packages/sdk-java/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>>() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user