diff --git a/.gitignore b/.gitignore index fac00d412..705216c80 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ package-lock.json .idea *.iml .cursor +.qoder # OS metadata .DS_Store diff --git a/docs/developers/_meta.ts b/docs/developers/_meta.ts index 956e1ad98..154ce1848 100644 --- a/docs/developers/_meta.ts +++ b/docs/developers/_meta.ts @@ -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', diff --git a/docs/developers/sdk-java.md b/docs/developers/sdk-java.md new file mode 100644 index 000000000..0b16e60a5 --- /dev/null +++ b/docs/developers/sdk-java.md @@ -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 + + com.alibaba + qwencode-sdk + {$version} + +``` + +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 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() {{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 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, 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. diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 3e87985b8..9cf704dae 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -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. | | | diff --git a/docs/users/integration-zed.md b/docs/users/integration-zed.md index cd4cb2ae4..663e23e80 100644 --- a/docs/users/integration-zed.md +++ b/docs/users/integration-zed.md @@ -32,7 +32,7 @@ "Qwen Code": { "type": "custom", "command": "qwen", - "args": ["--experimental-acp"], + "args": ["--acp"], "env": {} } ``` diff --git a/integration-tests/acp-integration.test.ts b/integration-tests/acp-integration.test.ts index 31e32da76..b89292d87 100644 --- a/integration-tests/acp-integration.test.ts +++ b/integration-tests/acp-integration.test.ts @@ -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(); 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(); + } + }); + }, +); diff --git a/package-lock.json b/package-lock.json index 330b90e08..0ed7071f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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/*" ], @@ -17316,7 +17316,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 +17953,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", @@ -21413,7 +21413,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 +21425,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", diff --git a/package.json b/package.json index c239067ff..107b9e9b0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/cli/package.json b/packages/cli/package.json index f2083fe19..2154e4683 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 0b95f7857..6f2019e75 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -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', diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 3f781c46e..cfeb3560c 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -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; @@ -314,10 +317,16 @@ export async function parseArguments(settings: Settings): Promise { 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', @@ -599,8 +608,19 @@ export async function parseArguments(settings: Settings): Promise { // 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)['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)['channel'] = 'ACP'; } @@ -828,6 +848,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 && @@ -836,12 +878,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. @@ -991,7 +1036,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, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 2fe467ba9..5159613b6 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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: { diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 9fa0b8261..9e0137fb7 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -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(); + }); }); diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index b05f12453..da945546d 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -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()); } diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js new file mode 100644 index 000000000..832dd1333 --- /dev/null +++ b/packages/cli/src/i18n/locales/de.js @@ -0,0 +1,1073 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +// German translations for Qwen Code CLI +// Deutsche Übersetzungen für Qwen Code CLI + +export default { + // ============================================================================ + // Help / UI Components + // ============================================================================ + 'Basics:': 'Grundlagen:', + 'Add context': 'Kontext hinzufügen', + 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.': + 'Verwenden Sie {{symbol}}, um Dateien als Kontext anzugeben (z.B. {{example}}), um bestimmte Dateien oder Ordner auszuwählen.', + '@': '@', + '@src/myFile.ts': '@src/myFile.ts', + 'Shell mode': 'Shell-Modus', + 'YOLO mode': 'YOLO-Modus', + 'plan mode': 'Planungsmodus', + 'auto-accept edits': 'Änderungen automatisch akzeptieren', + 'Accepting edits': 'Änderungen werden akzeptiert', + '(shift + tab to cycle)': '(Umschalt + Tab zum Wechseln)', + 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': + 'Shell-Befehle über {{symbol}} ausführen (z.B. {{example1}}) oder natürliche Sprache verwenden (z.B. {{example2}}).', + '!': '!', + '!npm run start': '!npm run start', + 'start server': 'Server starten', + 'Commands:': 'Befehle:', + 'shell command': 'Shell-Befehl', + 'Model Context Protocol command (from external servers)': + 'Model Context Protocol Befehl (von externen Servern)', + 'Keyboard Shortcuts:': 'Tastenkürzel:', + 'Jump through words in the input': 'Wörter in der Eingabe überspringen', + 'Close dialogs, cancel requests, or quit application': + 'Dialoge schließen, Anfragen abbrechen oder Anwendung beenden', + 'New line': 'Neue Zeile', + 'New line (Alt+Enter works for certain linux distros)': + 'Neue Zeile (Alt+Enter funktioniert bei bestimmten Linux-Distributionen)', + 'Clear the screen': 'Bildschirm löschen', + 'Open input in external editor': 'Eingabe in externem Editor öffnen', + 'Send message': 'Nachricht senden', + 'Initializing...': 'Initialisierung...', + 'Connecting to MCP servers... ({{connected}}/{{total}})': + 'Verbindung zu MCP-Servern wird hergestellt... ({{connected}}/{{total}})', + 'Type your message or @path/to/file': 'Nachricht eingeben oder @Pfad/zur/Datei', + "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.": + "Drücken Sie 'i' für den EINFÜGE-Modus und 'Esc' für den NORMAL-Modus.", + 'Cancel operation / Clear input (double press)': + 'Vorgang abbrechen / Eingabe löschen (doppelt drücken)', + 'Cycle approval modes': 'Genehmigungsmodi durchschalten', + 'Cycle through your prompt history': 'Eingabeverlauf durchblättern', + 'For a full list of shortcuts, see {{docPath}}': + 'Eine vollständige Liste der Tastenkürzel finden Sie unter {{docPath}}', + 'docs/keyboard-shortcuts.md': 'docs/keyboard-shortcuts.md', + 'for help on Qwen Code': 'für Hilfe zu Qwen Code', + 'show version info': 'Versionsinformationen anzeigen', + 'submit a bug report': 'Fehlerbericht einreichen', + 'About Qwen Code': 'Über Qwen Code', + + // ============================================================================ + // System Information Fields + // ============================================================================ + 'CLI Version': 'CLI-Version', + 'Git Commit': 'Git-Commit', + Model: 'Modell', + Sandbox: 'Sandbox', + 'OS Platform': 'Betriebssystem', + 'OS Arch': 'OS-Architektur', + 'OS Release': 'OS-Version', + 'Node.js Version': 'Node.js-Version', + 'NPM Version': 'NPM-Version', + 'Session ID': 'Sitzungs-ID', + 'Auth Method': 'Authentifizierungsmethode', + 'Base URL': 'Basis-URL', + 'Memory Usage': 'Speichernutzung', + 'IDE Client': 'IDE-Client', + + // ============================================================================ + // Commands - General + // ============================================================================ + 'Analyzes the project and creates a tailored QWEN.md file.': + 'Analysiert das Projekt und erstellt eine maßgeschneiderte QWEN.md-Datei.', + 'list available Qwen Code tools. Usage: /tools [desc]': + 'Verfügbare Qwen Code Werkzeuge auflisten. Verwendung: /tools [desc]', + 'Available Qwen Code CLI tools:': 'Verfügbare Qwen Code CLI-Werkzeuge:', + 'No tools available': 'Keine Werkzeuge verfügbar', + 'View or change the approval mode for tool usage': + 'Genehmigungsmodus für Werkzeugnutzung anzeigen oder ändern', + 'View or change the language setting': 'Spracheinstellung anzeigen oder ändern', + 'change the theme': 'Design ändern', + 'Select Theme': 'Design auswählen', + Preview: 'Vorschau', + '(Use Enter to select, Tab to configure scope)': + '(Enter zum Auswählen, Tab zum Konfigurieren des Bereichs)', + '(Use Enter to apply scope, Tab to select theme)': + '(Enter zum Anwenden des Bereichs, Tab zum Auswählen des Designs)', + 'Theme configuration unavailable due to NO_COLOR env variable.': + 'Design-Konfiguration aufgrund der NO_COLOR-Umgebungsvariable nicht verfügbar.', + 'Theme "{{themeName}}" not found.': 'Design "{{themeName}}" nicht gefunden.', + 'Theme "{{themeName}}" not found in selected scope.': + 'Design "{{themeName}}" im ausgewählten Bereich nicht gefunden.', + 'Clear conversation history and free up context': + 'Gesprächsverlauf löschen und Kontext freigeben', + 'Compresses the context by replacing it with a summary.': + 'Komprimiert den Kontext durch Ersetzen mit einer Zusammenfassung.', + 'open full Qwen Code documentation in your browser': + 'Vollständige Qwen Code Dokumentation im Browser öffnen', + 'Configuration not available.': 'Konfiguration nicht verfügbar.', + 'change the auth method': 'Authentifizierungsmethode ändern', + 'Copy the last result or code snippet to clipboard': + 'Letztes Ergebnis oder Codeausschnitt in die Zwischenablage kopieren', + + // ============================================================================ + // Commands - Agents + // ============================================================================ + 'Manage subagents for specialized task delegation.': + 'Unteragenten für spezialisierte Aufgabendelegation verwalten.', + 'Manage existing subagents (view, edit, delete).': + 'Bestehende Unteragenten verwalten (anzeigen, bearbeiten, löschen).', + 'Create a new subagent with guided setup.': + 'Neuen Unteragenten mit geführter Einrichtung erstellen.', + + // ============================================================================ + // Agents - Management Dialog + // ============================================================================ + Agents: 'Agenten', + 'Choose Action': 'Aktion wählen', + 'Edit {{name}}': '{{name}} bearbeiten', + 'Edit Tools: {{name}}': 'Werkzeuge bearbeiten: {{name}}', + 'Edit Color: {{name}}': 'Farbe bearbeiten: {{name}}', + 'Delete {{name}}': '{{name}} löschen', + 'Unknown Step': 'Unbekannter Schritt', + 'Esc to close': 'Esc zum Schließen', + 'Enter to select, ↑↓ to navigate, Esc to close': + 'Enter zum Auswählen, ↑↓ zum Navigieren, Esc zum Schließen', + 'Esc to go back': 'Esc zum Zurückgehen', + 'Enter to confirm, Esc to cancel': 'Enter zum Bestätigen, Esc zum Abbrechen', + 'Enter to select, ↑↓ to navigate, Esc to go back': + 'Enter zum Auswählen, ↑↓ zum Navigieren, Esc zum Zurückgehen', + 'Invalid step: {{step}}': 'Ungültiger Schritt: {{step}}', + 'No subagents found.': 'Keine Unteragenten gefunden.', + "Use '/agents create' to create your first subagent.": + "Verwenden Sie '/agents create', um Ihren ersten Unteragenten zu erstellen.", + '(built-in)': '(integriert)', + '(overridden by project level agent)': '(überschrieben durch Projektagent)', + 'Project Level ({{path}})': 'Projektebene ({{path}})', + 'User Level ({{path}})': 'Benutzerebene ({{path}})', + 'Built-in Agents': 'Integrierte Agenten', + 'Using: {{count}} agents': 'Verwendet: {{count}} Agenten', + 'View Agent': 'Agent anzeigen', + 'Edit Agent': 'Agent bearbeiten', + 'Delete Agent': 'Agent löschen', + Back: 'Zurück', + 'No agent selected': 'Kein Agent ausgewählt', + 'File Path: ': 'Dateipfad: ', + 'Tools: ': 'Werkzeuge: ', + 'Color: ': 'Farbe: ', + 'Description:': 'Beschreibung:', + 'System Prompt:': 'System-Prompt:', + 'Open in editor': 'Im Editor öffnen', + 'Edit tools': 'Werkzeuge bearbeiten', + 'Edit color': 'Farbe bearbeiten', + '❌ Error:': '❌ Fehler:', + 'Are you sure you want to delete agent "{{name}}"?': + 'Sind Sie sicher, dass Sie den Agenten "{{name}}" löschen möchten?', + // ============================================================================ + // Agents - Creation Wizard + // ============================================================================ + 'Project Level (.qwen/agents/)': 'Projektebene (.qwen/agents/)', + 'User Level (~/.qwen/agents/)': 'Benutzerebene (~/.qwen/agents/)', + '✅ Subagent Created Successfully!': '✅ Unteragent erfolgreich erstellt!', + 'Subagent "{{name}}" has been saved to {{level}} level.': + 'Unteragent "{{name}}" wurde auf {{level}}-Ebene gespeichert.', + 'Name: ': 'Name: ', + 'Location: ': 'Speicherort: ', + '❌ Error saving subagent:': '❌ Fehler beim Speichern des Unteragenten:', + 'Warnings:': 'Warnungen:', + 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent': + 'Name "{{name}}" existiert bereits auf {{level}}-Ebene - bestehender Unteragent wird überschrieben', + 'Name "{{name}}" exists at user level - project level will take precedence': + 'Name "{{name}}" existiert auf Benutzerebene - Projektebene hat Vorrang', + 'Name "{{name}}" exists at project level - existing subagent will take precedence': + 'Name "{{name}}" existiert auf Projektebene - bestehender Unteragent hat Vorrang', + 'Description is over {{length}} characters': + 'Beschreibung ist über {{length}} Zeichen', + 'System prompt is over {{length}} characters': + 'System-Prompt ist über {{length}} Zeichen', + // Agents - Creation Wizard Steps + 'Step {{n}}: Choose Location': 'Schritt {{n}}: Speicherort wählen', + 'Step {{n}}: Choose Generation Method': + 'Schritt {{n}}: Generierungsmethode wählen', + 'Generate with Qwen Code (Recommended)': + 'Mit Qwen Code generieren (Empfohlen)', + 'Manual Creation': 'Manuelle Erstellung', + 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)': + 'Beschreiben Sie, was dieser Unteragent tun soll und wann er verwendet werden soll. (Ausführliche Beschreibung für beste Ergebnisse)', + 'e.g., Expert code reviewer that reviews code based on best practices...': + 'z.B. Experte für Code-Reviews, der Code nach Best Practices überprüft...', + 'Generating subagent configuration...': + 'Unteragent-Konfiguration wird generiert...', + 'Failed to generate subagent: {{error}}': + 'Fehler beim Generieren des Unteragenten: {{error}}', + 'Step {{n}}: Describe Your Subagent': 'Schritt {{n}}: Unteragent beschreiben', + 'Step {{n}}: Enter Subagent Name': 'Schritt {{n}}: Unteragent-Name eingeben', + 'Step {{n}}: Enter System Prompt': 'Schritt {{n}}: System-Prompt eingeben', + 'Step {{n}}: Enter Description': 'Schritt {{n}}: Beschreibung eingeben', + // Agents - Tool Selection + 'Step {{n}}: Select Tools': 'Schritt {{n}}: Werkzeuge auswählen', + 'All Tools (Default)': 'Alle Werkzeuge (Standard)', + 'All Tools': 'Alle Werkzeuge', + 'Read-only Tools': 'Nur-Lese-Werkzeuge', + 'Read & Edit Tools': 'Lese- und Bearbeitungswerkzeuge', + 'Read & Edit & Execution Tools': 'Lese-, Bearbeitungs- und Ausführungswerkzeuge', + 'All tools selected, including MCP tools': + 'Alle Werkzeuge ausgewählt, einschließlich MCP-Werkzeuge', + 'Selected tools:': 'Ausgewählte Werkzeuge:', + 'Read-only tools:': 'Nur-Lese-Werkzeuge:', + 'Edit tools:': 'Bearbeitungswerkzeuge:', + 'Execution tools:': 'Ausführungswerkzeuge:', + 'Step {{n}}: Choose Background Color': 'Schritt {{n}}: Hintergrundfarbe wählen', + 'Step {{n}}: Confirm and Save': 'Schritt {{n}}: Bestätigen und Speichern', + // Agents - Navigation & Instructions + 'Esc to cancel': 'Esc zum Abbrechen', + 'Press Enter to save, e to save and edit, Esc to go back': + 'Enter zum Speichern, e zum Speichern und Bearbeiten, Esc zum Zurückgehen', + 'Press Enter to continue, {{navigation}}Esc to {{action}}': + 'Enter zum Fortfahren, {{navigation}}Esc zum {{action}}', + cancel: 'Abbrechen', + 'go back': 'Zurückgehen', + '↑↓ to navigate, ': '↑↓ zum Navigieren, ', + 'Enter a clear, unique name for this subagent.': + 'Geben Sie einen eindeutigen Namen für diesen Unteragenten ein.', + 'e.g., Code Reviewer': 'z.B. Code-Reviewer', + 'Name cannot be empty.': 'Name darf nicht leer sein.', + "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.": + 'Schreiben Sie den System-Prompt, der das Verhalten dieses Unteragenten definiert. Ausführlich für beste Ergebnisse.', + 'e.g., You are an expert code reviewer...': + 'z.B. Sie sind ein Experte für Code-Reviews...', + 'System prompt cannot be empty.': 'System-Prompt darf nicht leer sein.', + 'Describe when and how this subagent should be used.': + 'Beschreiben Sie, wann und wie dieser Unteragent verwendet werden soll.', + 'e.g., Reviews code for best practices and potential bugs.': + 'z.B. Überprüft Code auf Best Practices und mögliche Fehler.', + 'Description cannot be empty.': 'Beschreibung darf nicht leer sein.', + 'Failed to launch editor: {{error}}': 'Fehler beim Starten des Editors: {{error}}', + 'Failed to save and edit subagent: {{error}}': + 'Fehler beim Speichern und Bearbeiten des Unteragenten: {{error}}', + + // ============================================================================ + // Commands - General (continued) + // ============================================================================ + 'View and edit Qwen Code settings': 'Qwen Code Einstellungen anzeigen und bearbeiten', + Settings: 'Einstellungen', + '(Use Enter to select{{tabText}})': '(Enter zum Auswählen{{tabText}})', + ', Tab to change focus': ', Tab zum Fokuswechsel', + 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': + 'Um Änderungen zu sehen, muss Qwen Code neu gestartet werden. Drücken Sie r, um jetzt zu beenden und Änderungen anzuwenden.', + 'The command "/{{command}}" is not supported in non-interactive mode.': + 'Der Befehl "/{{command}}" wird im nicht-interaktiven Modus nicht unterstützt.', + // ============================================================================ + // Settings Labels + // ============================================================================ + 'Vim Mode': 'Vim-Modus', + 'Disable Auto Update': 'Automatische Updates deaktivieren', + 'Enable Prompt Completion': 'Eingabevervollständigung aktivieren', + 'Debug Keystroke Logging': 'Debug-Protokollierung von Tastatureingaben', + Language: 'Sprache', + 'Output Format': 'Ausgabeformat', + 'Hide Window Title': 'Fenstertitel ausblenden', + 'Show Status in Title': 'Status im Titel anzeigen', + 'Hide Tips': 'Tipps ausblenden', + 'Hide Banner': 'Banner ausblenden', + 'Hide Context Summary': 'Kontextzusammenfassung ausblenden', + 'Hide CWD': 'Arbeitsverzeichnis ausblenden', + 'Hide Sandbox Status': 'Sandbox-Status ausblenden', + 'Hide Model Info': 'Modellinformationen ausblenden', + 'Hide Footer': 'Fußzeile ausblenden', + 'Show Memory Usage': 'Speichernutzung anzeigen', + 'Show Line Numbers': 'Zeilennummern anzeigen', + 'Show Citations': 'Quellenangaben anzeigen', + 'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche', + 'Enable Welcome Back': 'Willkommen-zurück aktivieren', + 'Disable Loading Phrases': 'Ladesprüche deaktivieren', + 'Screen Reader Mode': 'Bildschirmleser-Modus', + 'IDE Mode': 'IDE-Modus', + 'Max Session Turns': 'Maximale Sitzungsrunden', + 'Skip Next Speaker Check': 'Nächste-Sprecher-Prüfung überspringen', + 'Skip Loop Detection': 'Schleifenerkennung überspringen', + 'Skip Startup Context': 'Startkontext überspringen', + 'Enable OpenAI Logging': 'OpenAI-Protokollierung aktivieren', + 'OpenAI Logging Directory': 'OpenAI-Protokollierungsverzeichnis', + Timeout: 'Zeitlimit', + 'Max Retries': 'Maximale Wiederholungen', + 'Disable Cache Control': 'Cache-Steuerung deaktivieren', + 'Memory Discovery Max Dirs': 'Maximale Verzeichnisse für Speichererkennung', + 'Load Memory From Include Directories': + 'Speicher aus Include-Verzeichnissen laden', + 'Respect .gitignore': '.gitignore beachten', + 'Respect .qwenignore': '.qwenignore beachten', + 'Enable Recursive File Search': 'Rekursive Dateisuche aktivieren', + 'Disable Fuzzy Search': 'Unscharfe Suche deaktivieren', + 'Enable Interactive Shell': 'Interaktive Shell aktivieren', + 'Show Color': 'Farbe anzeigen', + 'Auto Accept': 'Automatisch akzeptieren', + 'Use Ripgrep': 'Ripgrep verwenden', + 'Use Builtin Ripgrep': 'Integriertes Ripgrep verwenden', + 'Enable Tool Output Truncation': 'Werkzeugausgabe-Kürzung aktivieren', + 'Tool Output Truncation Threshold': 'Schwellenwert für Werkzeugausgabe-Kürzung', + 'Tool Output Truncation Lines': 'Zeilen für Werkzeugausgabe-Kürzung', + 'Folder Trust': 'Ordnervertrauen', + 'Vision Model Preview': 'Vision-Modell-Vorschau', + 'Tool Schema Compliance': 'Werkzeug-Schema-Konformität', + // Settings enum options + 'Auto (detect from system)': 'Automatisch (vom System erkennen)', + Text: 'Text', + JSON: 'JSON', + Plan: 'Plan', + Default: 'Standard', + 'Auto Edit': 'Automatisch bearbeiten', + YOLO: 'YOLO', + 'toggle vim mode on/off': 'Vim-Modus ein-/ausschalten', + 'check session stats. Usage: /stats [model|tools]': + 'Sitzungsstatistiken prüfen. Verwendung: /stats [model|tools]', + 'Show model-specific usage statistics.': + 'Modellspezifische Nutzungsstatistiken anzeigen.', + 'Show tool-specific usage statistics.': + 'Werkzeugspezifische Nutzungsstatistiken anzeigen.', + 'exit the cli': 'CLI beenden', + 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers': + 'Konfigurierte MCP-Server und Werkzeuge auflisten oder mit OAuth-fähigen Servern authentifizieren', + 'Manage workspace directories': 'Arbeitsbereichsverzeichnisse verwalten', + 'Add directories to the workspace. Use comma to separate multiple paths': + 'Verzeichnisse zum Arbeitsbereich hinzufügen. Komma zum Trennen mehrerer Pfade verwenden', + 'Show all directories in the workspace': + 'Alle Verzeichnisse im Arbeitsbereich anzeigen', + 'set external editor preference': 'Externen Editor festlegen', + 'Manage extensions': 'Erweiterungen verwalten', + 'List active extensions': 'Aktive Erweiterungen auflisten', + 'Update extensions. Usage: update |--all': + 'Erweiterungen aktualisieren. Verwendung: update |--all', + 'manage IDE integration': 'IDE-Integration verwalten', + 'check status of IDE integration': 'Status der IDE-Integration prüfen', + 'install required IDE companion for {{ideName}}': + 'Erforderlichen IDE-Begleiter für {{ideName}} installieren', + 'enable IDE integration': 'IDE-Integration aktivieren', + 'disable IDE integration': 'IDE-Integration deaktivieren', + 'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.': + 'IDE-Integration wird in Ihrer aktuellen Umgebung nicht unterstützt. Um diese Funktion zu nutzen, führen Sie Qwen Code in einer dieser unterstützten IDEs aus: VS Code oder VS Code-Forks.', + 'Set up GitHub Actions': 'GitHub Actions einrichten', + 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)': + 'Terminal-Tastenbelegungen für mehrzeilige Eingabe konfigurieren (VS Code, Cursor, Windsurf, Trae)', + 'Please restart your terminal for the changes to take effect.': + 'Bitte starten Sie Ihr Terminal neu, damit die Änderungen wirksam werden.', + 'Failed to configure terminal: {{error}}': + 'Fehler beim Konfigurieren des Terminals: {{error}}', + 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.': + 'Konnte {{terminalName}}-Konfigurationspfad unter Windows nicht ermitteln: APPDATA-Umgebungsvariable ist nicht gesetzt.', + '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.': + '{{terminalName}} keybindings.json existiert, ist aber kein gültiges JSON-Array. Bitte korrigieren Sie die Datei manuell oder löschen Sie sie, um automatische Konfiguration zu ermöglichen.', + 'File: {{file}}': 'Datei: {{file}}', + 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.': + 'Fehler beim Parsen von {{terminalName}} keybindings.json. Die Datei enthält ungültiges JSON. Bitte korrigieren Sie die Datei manuell oder löschen Sie sie, um automatische Konfiguration zu ermöglichen.', + 'Error: {{error}}': 'Fehler: {{error}}', + 'Shift+Enter binding already exists': 'Umschalt+Enter-Belegung existiert bereits', + 'Ctrl+Enter binding already exists': 'Strg+Enter-Belegung existiert bereits', + 'Existing keybindings detected. Will not modify to avoid conflicts.': + 'Bestehende Tastenbelegungen erkannt. Keine Änderungen, um Konflikte zu vermeiden.', + 'Please check and modify manually if needed: {{file}}': + 'Bitte prüfen und bei Bedarf manuell ändern: {{file}}', + 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.': + 'Umschalt+Enter und Strg+Enter Tastenbelegungen zu {{terminalName}} hinzugefügt.', + 'Modified: {{file}}': 'Geändert: {{file}}', + '{{terminalName}} keybindings already configured.': + '{{terminalName}}-Tastenbelegungen bereits konfiguriert.', + 'Failed to configure {{terminalName}}.': + 'Fehler beim Konfigurieren von {{terminalName}}.', + 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).': + 'Ihr Terminal ist bereits für optimale Erfahrung mit mehrzeiliger Eingabe konfiguriert (Umschalt+Enter und Strg+Enter).', + 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.': + 'Terminal-Typ konnte nicht erkannt werden. Unterstützte Terminals: VS Code, Cursor, Windsurf und Trae.', + 'Terminal "{{terminal}}" is not supported yet.': + 'Terminal "{{terminal}}" wird noch nicht unterstützt.', + + // ============================================================================ + // Commands - Language + // ============================================================================ + 'Invalid language. Available: en-US, zh-CN': + 'Ungültige Sprache. Verfügbar: en-US, zh-CN', + 'Language subcommands do not accept additional arguments.': + 'Sprach-Unterbefehle akzeptieren keine zusätzlichen Argumente.', + 'Current UI language: {{lang}}': 'Aktuelle UI-Sprache: {{lang}}', + 'Current LLM output language: {{lang}}': + 'Aktuelle LLM-Ausgabesprache: {{lang}}', + 'LLM output language not set': 'LLM-Ausgabesprache nicht festgelegt', + 'Set UI language': 'UI-Sprache festlegen', + 'Set LLM output language': 'LLM-Ausgabesprache festlegen', + 'Usage: /language ui [zh-CN|en-US]': 'Verwendung: /language ui [zh-CN|en-US]', + 'Usage: /language output ': 'Verwendung: /language output ', + 'Example: /language output 中文': 'Beispiel: /language output Deutsch', + 'Example: /language output English': 'Beispiel: /language output English', + 'Example: /language output 日本語': 'Beispiel: /language output Japanisch', + 'UI language changed to {{lang}}': 'UI-Sprache geändert zu {{lang}}', + 'LLM output language rule file generated at {{path}}': + 'LLM-Ausgabesprach-Regeldatei generiert unter {{path}}', + 'Please restart the application for the changes to take effect.': + 'Bitte starten Sie die Anwendung neu, damit die Änderungen wirksam werden.', + 'Failed to generate LLM output language rule file: {{error}}': + 'Fehler beim Generieren der LLM-Ausgabesprach-Regeldatei: {{error}}', + 'Invalid command. Available subcommands:': + 'Ungültiger Befehl. Verfügbare Unterbefehle:', + 'Available subcommands:': 'Verfügbare Unterbefehle:', + 'To request additional UI language packs, please open an issue on GitHub.': + 'Um zusätzliche UI-Sprachpakete anzufordern, öffnen Sie bitte ein Issue auf GitHub.', + 'Available options:': 'Verfügbare Optionen:', + ' - zh-CN: Simplified Chinese': ' - zh-CN: Vereinfachtes Chinesisch', + ' - en-US: English': ' - en-US: Englisch', + 'Set UI language to Simplified Chinese (zh-CN)': + 'UI-Sprache auf Vereinfachtes Chinesisch (zh-CN) setzen', + 'Set UI language to English (en-US)': 'UI-Sprache auf Englisch (en-US) setzen', + + // ============================================================================ + // Commands - Approval Mode + // ============================================================================ + 'Approval Mode': 'Genehmigungsmodus', + 'Current approval mode: {{mode}}': 'Aktueller Genehmigungsmodus: {{mode}}', + 'Available approval modes:': 'Verfügbare Genehmigungsmodi:', + 'Approval mode changed to: {{mode}}': 'Genehmigungsmodus geändert zu: {{mode}}', + 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})': + 'Genehmigungsmodus geändert zu: {{mode}} (gespeichert in {{scope}} Einstellungen{{location}})', + 'Usage: /approval-mode [--session|--user|--project]': + 'Verwendung: /approval-mode [--session|--user|--project]', + + 'Scope subcommands do not accept additional arguments.': + 'Bereichs-Unterbefehle akzeptieren keine zusätzlichen Argumente.', + 'Plan mode - Analyze only, do not modify files or execute commands': + 'Planungsmodus - Nur analysieren, keine Dateien ändern oder Befehle ausführen', + 'Default mode - Require approval for file edits or shell commands': + 'Standardmodus - Genehmigung für Dateibearbeitungen oder Shell-Befehle erforderlich', + 'Auto-edit mode - Automatically approve file edits': + 'Automatischer Bearbeitungsmodus - Dateibearbeitungen automatisch genehmigen', + 'YOLO mode - Automatically approve all tools': + 'YOLO-Modus - Alle Werkzeuge automatisch genehmigen', + '{{mode}} mode': '{{mode}}-Modus', + 'Settings service is not available; unable to persist the approval mode.': + 'Einstellungsdienst nicht verfügbar; Genehmigungsmodus kann nicht gespeichert werden.', + 'Failed to save approval mode: {{error}}': + 'Fehler beim Speichern des Genehmigungsmodus: {{error}}', + 'Failed to change approval mode: {{error}}': + 'Fehler beim Ändern des Genehmigungsmodus: {{error}}', + 'Apply to current session only (temporary)': + 'Nur auf aktuelle Sitzung anwenden (temporär)', + 'Persist for this project/workspace': 'Für dieses Projekt/Arbeitsbereich speichern', + 'Persist for this user on this machine': + 'Für diesen Benutzer auf diesem Computer speichern', + 'Analyze only, do not modify files or execute commands': + 'Nur analysieren, keine Dateien ändern oder Befehle ausführen', + 'Require approval for file edits or shell commands': + 'Genehmigung für Dateibearbeitungen oder Shell-Befehle erforderlich', + 'Automatically approve file edits': 'Dateibearbeitungen automatisch genehmigen', + 'Automatically approve all tools': 'Alle Werkzeuge automatisch genehmigen', + 'Workspace approval mode exists and takes priority. User-level change will have no effect.': + 'Arbeitsbereich-Genehmigungsmodus existiert und hat Vorrang. Benutzerebene-Änderung hat keine Wirkung.', + '(Use Enter to select, Tab to change focus)': + '(Enter zum Auswählen, Tab zum Fokuswechsel)', + 'Apply To': 'Anwenden auf', + 'User Settings': 'Benutzereinstellungen', + 'Workspace Settings': 'Arbeitsbereich-Einstellungen', + + // ============================================================================ + // Commands - Memory + // ============================================================================ + 'Commands for interacting with memory.': + 'Befehle für die Interaktion mit dem Speicher.', + 'Show the current memory contents.': 'Aktuellen Speicherinhalt anzeigen.', + 'Show project-level memory contents.': 'Projektebene-Speicherinhalt anzeigen.', + 'Show global memory contents.': 'Globalen Speicherinhalt anzeigen.', + 'Add content to project-level memory.': + 'Inhalt zum Projektebene-Speicher hinzufügen.', + 'Add content to global memory.': 'Inhalt zum globalen Speicher hinzufügen.', + 'Refresh the memory from the source.': 'Speicher aus der Quelle aktualisieren.', + 'Usage: /memory add --project ': + 'Verwendung: /memory add --project ', + 'Usage: /memory add --global ': + 'Verwendung: /memory add --global ', + 'Attempting to save to project memory: "{{text}}"': + 'Versuche im Projektspeicher zu speichern: "{{text}}"', + 'Attempting to save to global memory: "{{text}}"': + 'Versuche im globalen Speicher zu speichern: "{{text}}"', + 'Current memory content from {{count}} file(s):': + 'Aktueller Speicherinhalt aus {{count}} Datei(en):', + 'Memory is currently empty.': 'Speicher ist derzeit leer.', + 'Project memory file not found or is currently empty.': + 'Projektspeicherdatei nicht gefunden oder derzeit leer.', + 'Global memory file not found or is currently empty.': + 'Globale Speicherdatei nicht gefunden oder derzeit leer.', + 'Global memory is currently empty.': 'Globaler Speicher ist derzeit leer.', + 'Global memory content:\n\n---\n{{content}}\n---': + 'Globaler Speicherinhalt:\n\n---\n{{content}}\n---', + 'Project memory content from {{path}}:\n\n---\n{{content}}\n---': + 'Projektspeicherinhalt von {{path}}:\n\n---\n{{content}}\n---', + 'Project memory is currently empty.': 'Projektspeicher ist derzeit leer.', + 'Refreshing memory from source files...': + 'Speicher wird aus Quelldateien aktualisiert...', + 'Add content to the memory. Use --global for global memory or --project for project memory.': + 'Inhalt zum Speicher hinzufügen. --global für globalen Speicher oder --project für Projektspeicher verwenden.', + 'Usage: /memory add [--global|--project] ': + 'Verwendung: /memory add [--global|--project] ', + 'Attempting to save to memory {{scope}}: "{{fact}}"': + 'Versuche im Speicher {{scope}} zu speichern: "{{fact}}"', + + // ============================================================================ + // Commands - MCP + // ============================================================================ + 'Authenticate with an OAuth-enabled MCP server': + 'Mit einem OAuth-fähigen MCP-Server authentifizieren', + 'List configured MCP servers and tools': + 'Konfigurierte MCP-Server und Werkzeuge auflisten', + 'Restarts MCP servers.': 'MCP-Server neu starten.', + 'Config not loaded.': 'Konfiguration nicht geladen.', + 'Could not retrieve tool registry.': 'Werkzeugregister konnte nicht abgerufen werden.', + 'No MCP servers configured with OAuth authentication.': + 'Keine MCP-Server mit OAuth-Authentifizierung konfiguriert.', + 'MCP servers with OAuth authentication:': + 'MCP-Server mit OAuth-Authentifizierung:', + 'Use /mcp auth to authenticate.': + 'Verwenden Sie /mcp auth zur Authentifizierung.', + "MCP server '{{name}}' not found.": "MCP-Server '{{name}}' nicht gefunden.", + "Successfully authenticated and refreshed tools for '{{name}}'.": + "Erfolgreich authentifiziert und Werkzeuge für '{{name}}' aktualisiert.", + "Failed to authenticate with MCP server '{{name}}': {{error}}": + "Authentifizierung mit MCP-Server '{{name}}' fehlgeschlagen: {{error}}", + "Re-discovering tools from '{{name}}'...": + "Werkzeuge von '{{name}}' werden neu erkannt...", + + // ============================================================================ + // Commands - Chat + // ============================================================================ + 'Manage conversation history.': 'Gesprächsverlauf verwalten.', + 'List saved conversation checkpoints': 'Gespeicherte Gesprächsprüfpunkte auflisten', + 'No saved conversation checkpoints found.': + 'Keine gespeicherten Gesprächsprüfpunkte gefunden.', + 'List of saved conversations:': 'Liste gespeicherter Gespräche:', + 'Note: Newest last, oldest first': 'Hinweis: Neueste zuletzt, älteste zuerst', + 'Save the current conversation as a checkpoint. Usage: /chat save ': + 'Aktuelles Gespräch als Prüfpunkt speichern. Verwendung: /chat save ', + 'Missing tag. Usage: /chat save ': + 'Tag fehlt. Verwendung: /chat save ', + 'Delete a conversation checkpoint. Usage: /chat delete ': + 'Gesprächsprüfpunkt löschen. Verwendung: /chat delete ', + 'Missing tag. Usage: /chat delete ': + 'Tag fehlt. Verwendung: /chat delete ', + "Conversation checkpoint '{{tag}}' has been deleted.": + "Gesprächsprüfpunkt '{{tag}}' wurde gelöscht.", + "Error: No checkpoint found with tag '{{tag}}'.": + "Fehler: Kein Prüfpunkt mit Tag '{{tag}}' gefunden.", + 'Resume a conversation from a checkpoint. Usage: /chat resume ': + 'Gespräch von einem Prüfpunkt fortsetzen. Verwendung: /chat resume ', + 'Missing tag. Usage: /chat resume ': + 'Tag fehlt. Verwendung: /chat resume ', + 'No saved checkpoint found with tag: {{tag}}.': + 'Kein gespeicherter Prüfpunkt mit Tag gefunden: {{tag}}.', + 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?': + 'Ein Prüfpunkt mit dem Tag {{tag}} existiert bereits. Möchten Sie ihn überschreiben?', + 'No chat client available to save conversation.': + 'Kein Chat-Client verfügbar, um Gespräch zu speichern.', + 'Conversation checkpoint saved with tag: {{tag}}.': + 'Gesprächsprüfpunkt gespeichert mit Tag: {{tag}}.', + 'No conversation found to save.': 'Kein Gespräch zum Speichern gefunden.', + 'No chat client available to share conversation.': + 'Kein Chat-Client verfügbar, um Gespräch zu teilen.', + 'Invalid file format. Only .md and .json are supported.': + 'Ungültiges Dateiformat. Nur .md und .json werden unterstützt.', + 'Error sharing conversation: {{error}}': + 'Fehler beim Teilen des Gesprächs: {{error}}', + 'Conversation shared to {{filePath}}': 'Gespräch geteilt nach {{filePath}}', + 'No conversation found to share.': 'Kein Gespräch zum Teilen gefunden.', + 'Share the current conversation to a markdown or json file. Usage: /chat share ': + 'Aktuelles Gespräch in eine Markdown- oder JSON-Datei teilen. Verwendung: /chat share ', + + // ============================================================================ + // Commands - Summary + // ============================================================================ + 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md': + 'Projektzusammenfassung generieren und in .qwen/PROJECT_SUMMARY.md speichern', + 'No chat client available to generate summary.': + 'Kein Chat-Client verfügbar, um Zusammenfassung zu generieren.', + 'Already generating summary, wait for previous request to complete': + 'Zusammenfassung wird bereits generiert, warten Sie auf Abschluss der vorherigen Anfrage', + 'No conversation found to summarize.': 'Kein Gespräch zum Zusammenfassen gefunden.', + 'Failed to generate project context summary: {{error}}': + 'Fehler beim Generieren der Projektkontextzusammenfassung: {{error}}', + 'Saved project summary to {{filePathForDisplay}}.': + 'Projektzusammenfassung gespeichert unter {{filePathForDisplay}}.', + 'Saving project summary...': 'Projektzusammenfassung wird gespeichert...', + 'Generating project summary...': 'Projektzusammenfassung wird generiert...', + 'Failed to generate summary - no text content received from LLM response': + 'Fehler beim Generieren der Zusammenfassung - kein Textinhalt von LLM-Antwort erhalten', + + // ============================================================================ + // Commands - Model + // ============================================================================ + 'Switch the model for this session': 'Modell für diese Sitzung wechseln', + 'Content generator configuration not available.': + 'Inhaltsgenerator-Konfiguration nicht verfügbar.', + 'Authentication type not available.': 'Authentifizierungstyp nicht verfügbar.', + 'No models available for the current authentication type ({{authType}}).': + 'Keine Modelle für den aktuellen Authentifizierungstyp ({{authType}}) verfügbar.', + + // ============================================================================ + // Commands - Clear + // ============================================================================ + 'Starting a new session, resetting chat, and clearing terminal.': + 'Neue Sitzung wird gestartet, Chat wird zurückgesetzt und Terminal wird gelöscht.', + 'Starting a new session and clearing.': + 'Neue Sitzung wird gestartet und gelöscht.', + + // ============================================================================ + // Commands - Compress + // ============================================================================ + 'Already compressing, wait for previous request to complete': + 'Komprimierung läuft bereits, warten Sie auf Abschluss der vorherigen Anfrage', + 'Failed to compress chat history.': 'Fehler beim Komprimieren des Chatverlaufs.', + 'Failed to compress chat history: {{error}}': + 'Fehler beim Komprimieren des Chatverlaufs: {{error}}', + 'Compressing chat history': 'Chatverlauf wird komprimiert', + 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.': + 'Chatverlauf komprimiert von {{originalTokens}} auf {{newTokens}} Token.', + 'Compression was not beneficial for this history size.': + 'Komprimierung war für diese Verlaufsgröße nicht vorteilhaft.', + 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.': + 'Chatverlauf-Komprimierung hat die Größe nicht reduziert. Dies kann auf Probleme mit dem Komprimierungs-Prompt hindeuten.', + 'Could not compress chat history due to a token counting error.': + 'Chatverlauf konnte aufgrund eines Token-Zählfehlers nicht komprimiert werden.', + 'Chat history is already compressed.': 'Chatverlauf ist bereits komprimiert.', + + // ============================================================================ + // Commands - Directory + // ============================================================================ + 'Configuration is not available.': 'Konfiguration ist nicht verfügbar.', + 'Please provide at least one path to add.': + 'Bitte geben Sie mindestens einen Pfad zum Hinzufügen an.', + 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.': + 'Der Befehl /directory add wird in restriktiven Sandbox-Profilen nicht unterstützt. Bitte verwenden Sie --include-directories beim Starten der Sitzung.', + "Error adding '{{path}}': {{error}}": "Fehler beim Hinzufügen von '{{path}}': {{error}}", + 'Successfully added QWEN.md files from the following directories if there are:\n- {{directories}}': + 'QWEN.md-Dateien aus folgenden Verzeichnissen erfolgreich hinzugefügt, falls vorhanden:\n- {{directories}}', + 'Error refreshing memory: {{error}}': 'Fehler beim Aktualisieren des Speichers: {{error}}', + 'Successfully added directories:\n- {{directories}}': + 'Verzeichnisse erfolgreich hinzugefügt:\n- {{directories}}', + 'Current workspace directories:\n{{directories}}': + 'Aktuelle Arbeitsbereichsverzeichnisse:\n{{directories}}', + + // ============================================================================ + // Commands - Docs + // ============================================================================ + 'Please open the following URL in your browser to view the documentation:\n{{url}}': + 'Bitte öffnen Sie folgende URL in Ihrem Browser, um die Dokumentation anzusehen:\n{{url}}', + 'Opening documentation in your browser: {{url}}': + 'Dokumentation wird in Ihrem Browser geöffnet: {{url}}', + + // ============================================================================ + // Dialogs - Tool Confirmation + // ============================================================================ + 'Do you want to proceed?': 'Möchten Sie fortfahren?', + 'Yes, allow once': 'Ja, einmal erlauben', + 'Allow always': 'Immer erlauben', + No: 'Nein', + 'No (esc)': 'Nein (Esc)', + 'Yes, allow always for this session': 'Ja, für diese Sitzung immer erlauben', + 'Modify in progress:': 'Änderung in Bearbeitung:', + 'Save and close external editor to continue': + 'Speichern und externen Editor schließen, um fortzufahren', + 'Apply this change?': 'Diese Änderung anwenden?', + 'Yes, allow always': 'Ja, immer erlauben', + 'Modify with external editor': 'Mit externem Editor bearbeiten', + 'No, suggest changes (esc)': 'Nein, Änderungen vorschlagen (Esc)', + "Allow execution of: '{{command}}'?": "Ausführung erlauben von: '{{command}}'?", + 'Yes, allow always ...': 'Ja, immer erlauben ...', + 'Yes, and auto-accept edits': 'Ja, und Änderungen automatisch akzeptieren', + 'Yes, and manually approve edits': 'Ja, und Änderungen manuell genehmigen', + 'No, keep planning (esc)': 'Nein, weiter planen (Esc)', + 'URLs to fetch:': 'Abzurufende URLs:', + 'MCP Server: {{server}}': 'MCP-Server: {{server}}', + 'Tool: {{tool}}': 'Werkzeug: {{tool}}', + 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?': + 'Ausführung des MCP-Werkzeugs "{{tool}}" von Server "{{server}}" erlauben?', + 'Yes, always allow tool "{{tool}}" from server "{{server}}"': + 'Ja, Werkzeug "{{tool}}" von Server "{{server}}" immer erlauben', + 'Yes, always allow all tools from server "{{server}}"': + 'Ja, alle Werkzeuge von Server "{{server}}" immer erlauben', + + // ============================================================================ + // Dialogs - Shell Confirmation + // ============================================================================ + 'Shell Command Execution': 'Shell-Befehlsausführung', + 'A custom command wants to run the following shell commands:': + 'Ein benutzerdefinierter Befehl möchte folgende Shell-Befehle ausführen:', + + // ============================================================================ + // Dialogs - Pro Quota + // ============================================================================ + 'Pro quota limit reached for {{model}}.': + 'Pro-Kontingentlimit für {{model}} erreicht.', + 'Change auth (executes the /auth command)': + 'Authentifizierung ändern (führt den /auth-Befehl aus)', + 'Continue with {{model}}': 'Mit {{model}} fortfahren', + + // ============================================================================ + // Dialogs - Welcome Back + // ============================================================================ + 'Current Plan:': 'Aktueller Plan:', + 'Progress: {{done}}/{{total}} tasks completed': + 'Fortschritt: {{done}}/{{total}} Aufgaben abgeschlossen', + ', {{inProgress}} in progress': ', {{inProgress}} in Bearbeitung', + 'Pending Tasks:': 'Ausstehende Aufgaben:', + 'What would you like to do?': 'Was möchten Sie tun?', + 'Choose how to proceed with your session:': + 'Wählen Sie, wie Sie mit Ihrer Sitzung fortfahren möchten:', + 'Start new chat session': 'Neue Chat-Sitzung starten', + 'Continue previous conversation': 'Vorheriges Gespräch fortsetzen', + '👋 Welcome back! (Last updated: {{timeAgo}})': + '👋 Willkommen zurück! (Zuletzt aktualisiert: {{timeAgo}})', + '🎯 Overall Goal:': '🎯 Gesamtziel:', + + // ============================================================================ + // Dialogs - Auth + // ============================================================================ + 'Get started': 'Loslegen', + 'How would you like to authenticate for this project?': + 'Wie möchten Sie sich für dieses Projekt authentifizieren?', + 'OpenAI API key is required to use OpenAI authentication.': + 'OpenAI API-Schlüssel ist für die OpenAI-Authentifizierung erforderlich.', + 'You must select an auth method to proceed. Press Ctrl+C again to exit.': + 'Sie müssen eine Authentifizierungsmethode wählen, um fortzufahren. Drücken Sie erneut Strg+C zum Beenden.', + '(Use Enter to Set Auth)': '(Enter zum Festlegen der Authentifizierung)', + 'Terms of Services and Privacy Notice for Qwen Code': + 'Nutzungsbedingungen und Datenschutzhinweis für Qwen Code', + 'Qwen OAuth': 'Qwen OAuth', + OpenAI: 'OpenAI', + 'Failed to login. Message: {{message}}': + 'Anmeldung fehlgeschlagen. Meldung: {{message}}', + 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.': + 'Authentifizierung ist auf {{enforcedType}} festgelegt, aber Sie verwenden derzeit {{currentType}}.', + 'Qwen OAuth authentication timed out. Please try again.': + 'Qwen OAuth-Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.', + 'Qwen OAuth authentication cancelled.': + 'Qwen OAuth-Authentifizierung abgebrochen.', + 'Qwen OAuth Authentication': 'Qwen OAuth-Authentifizierung', + 'Please visit this URL to authorize:': 'Bitte besuchen Sie diese URL zur Autorisierung:', + 'Or scan the QR code below:': 'Oder scannen Sie den QR-Code unten:', + 'Waiting for authorization': 'Warten auf Autorisierung', + 'Time remaining:': 'Verbleibende Zeit:', + '(Press ESC or CTRL+C to cancel)': '(ESC oder STRG+C zum Abbrechen drücken)', + 'Qwen OAuth Authentication Timeout': 'Qwen OAuth-Authentifizierung abgelaufen', + 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.': + 'OAuth-Token abgelaufen (über {{seconds}} Sekunden). Bitte wählen Sie erneut eine Authentifizierungsmethode.', + 'Press any key to return to authentication type selection.': + 'Drücken Sie eine beliebige Taste, um zur Authentifizierungstypauswahl zurückzukehren.', + 'Waiting for Qwen OAuth authentication...': + 'Warten auf Qwen OAuth-Authentifizierung...', + 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.': + 'Hinweis: Ihr bestehender API-Schlüssel in settings.json wird bei Verwendung von Qwen OAuth nicht gelöscht. Sie können später bei Bedarf zur OpenAI-Authentifizierung zurückwechseln.', + 'Authentication timed out. Please try again.': + 'Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.', + 'Waiting for auth... (Press ESC or CTRL+C to cancel)': + 'Warten auf Authentifizierung... (ESC oder STRG+C zum Abbrechen drücken)', + 'Failed to authenticate. Message: {{message}}': + 'Authentifizierung fehlgeschlagen. Meldung: {{message}}', + 'Authenticated successfully with {{authType}} credentials.': + 'Erfolgreich mit {{authType}}-Anmeldedaten authentifiziert.', + 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}': + 'Ungültiger QWEN_DEFAULT_AUTH_TYPE-Wert: "{{value}}". Gültige Werte sind: {{validValues}}', + 'OpenAI Configuration Required': 'OpenAI-Konfiguration erforderlich', + 'Please enter your OpenAI configuration. You can get an API key from': + 'Bitte geben Sie Ihre OpenAI-Konfiguration ein. Sie können einen API-Schlüssel erhalten von', + 'API Key:': 'API-Schlüssel:', + 'Invalid credentials: {{errorMessage}}': + 'Ungültige Anmeldedaten: {{errorMessage}}', + 'Failed to validate credentials': 'Anmeldedaten konnten nicht validiert werden', + 'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel': + 'Enter zum Fortfahren, Tab/↑↓ zum Navigieren, Esc zum Abbrechen', + + // ============================================================================ + // Dialogs - Model + // ============================================================================ + 'Select Model': 'Modell auswählen', + '(Press Esc to close)': '(Esc zum Schließen drücken)', + 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)': + 'Das neueste Qwen Coder Modell von Alibaba Cloud ModelStudio (Version: qwen3-coder-plus-2025-09-23)', + 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)': + 'Das neueste Qwen Vision Modell von Alibaba Cloud ModelStudio (Version: qwen3-vl-plus-2025-09-23)', + + // ============================================================================ + // Dialogs - Permissions + // ============================================================================ + 'Manage folder trust settings': 'Ordnervertrauenseinstellungen verwalten', + + // ============================================================================ + // Status Bar + // ============================================================================ + 'Using:': 'Verwendet:', + '{{count}} open file': '{{count}} geöffnete Datei', + '{{count}} open files': '{{count}} geöffnete Dateien', + '(ctrl+g to view)': '(Strg+G zum Anzeigen)', + '{{count}} {{name}} file': '{{count}} {{name}}-Datei', + '{{count}} {{name}} files': '{{count}} {{name}}-Dateien', + '{{count}} MCP server': '{{count}} MCP-Server', + '{{count}} MCP servers': '{{count}} MCP-Server', + '{{count}} Blocked': '{{count}} blockiert', + '(ctrl+t to view)': '(Strg+T zum Anzeigen)', + '(ctrl+t to toggle)': '(Strg+T zum Umschalten)', + 'Press Ctrl+C again to exit.': 'Drücken Sie erneut Strg+C zum Beenden.', + 'Press Ctrl+D again to exit.': 'Drücken Sie erneut Strg+D zum Beenden.', + 'Press Esc again to clear.': 'Drücken Sie erneut Esc zum Löschen.', + + // ============================================================================ + // MCP Status + // ============================================================================ + 'No MCP servers configured.': 'Keine MCP-Server konfiguriert.', + 'Please view MCP documentation in your browser:': + 'Bitte sehen Sie die MCP-Dokumentation in Ihrem Browser:', + 'or use the cli /docs command': 'oder verwenden Sie den CLI-Befehl /docs', + '⏳ MCP servers are starting up ({{count}} initializing)...': + '⏳ MCP-Server werden gestartet ({{count}} werden initialisiert)...', + 'Note: First startup may take longer. Tool availability will update automatically.': + 'Hinweis: Der erste Start kann länger dauern. Werkzeugverfügbarkeit wird automatisch aktualisiert.', + 'Configured MCP servers:': 'Konfigurierte MCP-Server:', + Ready: 'Bereit', + 'Starting... (first startup may take longer)': + 'Wird gestartet... (erster Start kann länger dauern)', + Disconnected: 'Getrennt', + '{{count}} tool': '{{count}} Werkzeug', + '{{count}} tools': '{{count}} Werkzeuge', + '{{count}} prompt': '{{count}} Prompt', + '{{count}} prompts': '{{count}} Prompts', + '(from {{extensionName}})': '(von {{extensionName}})', + OAuth: 'OAuth', + 'OAuth expired': 'OAuth abgelaufen', + 'OAuth not authenticated': 'OAuth nicht authentifiziert', + 'tools and prompts will appear when ready': + 'Werkzeuge und Prompts werden angezeigt, wenn bereit', + '{{count}} tools cached': '{{count}} Werkzeuge zwischengespeichert', + 'Tools:': 'Werkzeuge:', + 'Parameters:': 'Parameter:', + 'Prompts:': 'Prompts:', + Blocked: 'Blockiert', + '💡 Tips:': '💡 Tipps:', + Use: 'Verwenden', + 'to show server and tool descriptions': + 'um Server- und Werkzeugbeschreibungen anzuzeigen', + 'to show tool parameter schemas': 'um Werkzeug-Parameter-Schemas anzuzeigen', + 'to hide descriptions': 'um Beschreibungen auszublenden', + 'to authenticate with OAuth-enabled servers': + 'um sich bei OAuth-fähigen Servern zu authentifizieren', + Press: 'Drücken Sie', + 'to toggle tool descriptions on/off': + 'um Werkzeugbeschreibungen ein-/auszuschalten', + "Starting OAuth authentication for MCP server '{{name}}'...": + "OAuth-Authentifizierung für MCP-Server '{{name}}' wird gestartet...", + 'Restarting MCP servers...': 'MCP-Server werden neu gestartet...', + + // ============================================================================ + // Startup Tips + // ============================================================================ + 'Tips for getting started:': 'Tipps zum Einstieg:', + '1. Ask questions, edit files, or run commands.': + '1. Stellen Sie Fragen, bearbeiten Sie Dateien oder führen Sie Befehle aus.', + '2. Be specific for the best results.': + '2. Seien Sie spezifisch für die besten Ergebnisse.', + 'files to customize your interactions with Qwen Code.': + 'Dateien, um Ihre Interaktionen mit Qwen Code anzupassen.', + 'for more information.': 'für weitere Informationen.', + + // ============================================================================ + // Exit Screen / Stats + // ============================================================================ + 'Agent powering down. Goodbye!': 'Agent wird heruntergefahren. Auf Wiedersehen!', + 'To continue this session, run': 'Um diese Sitzung fortzusetzen, führen Sie aus', + 'Interaction Summary': 'Interaktionszusammenfassung', + 'Session ID:': 'Sitzungs-ID:', + 'Tool Calls:': 'Werkzeugaufrufe:', + 'Success Rate:': 'Erfolgsrate:', + 'User Agreement:': 'Benutzerzustimmung:', + reviewed: 'überprüft', + 'Code Changes:': 'Codeänderungen:', + Performance: 'Leistung', + 'Wall Time:': 'Gesamtzeit:', + 'Agent Active:': 'Agent aktiv:', + 'API Time:': 'API-Zeit:', + 'Tool Time:': 'Werkzeugzeit:', + 'Session Stats': 'Sitzungsstatistiken', + 'Model Usage': 'Modellnutzung', + Reqs: 'Anfragen', + 'Input Tokens': 'Eingabe-Token', + 'Output Tokens': 'Ausgabe-Token', + 'Savings Highlight:': 'Einsparungen:', + 'of input tokens were served from the cache, reducing costs.': + 'der Eingabe-Token wurden aus dem Cache bedient, was die Kosten reduziert.', + 'Tip: For a full token breakdown, run `/stats model`.': + 'Tipp: Für eine vollständige Token-Aufschlüsselung führen Sie `/stats model` aus.', + 'Model Stats For Nerds': 'Modellstatistiken für Nerds', + 'Tool Stats For Nerds': 'Werkzeugstatistiken für Nerds', + Metric: 'Metrik', + API: 'API', + Requests: 'Anfragen', + Errors: 'Fehler', + 'Avg Latency': 'Durchschn. Latenz', + Tokens: 'Token', + Total: 'Gesamt', + Prompt: 'Prompt', + Cached: 'Zwischengespeichert', + Thoughts: 'Gedanken', + Tool: 'Werkzeug', + Output: 'Ausgabe', + 'No API calls have been made in this session.': + 'In dieser Sitzung wurden keine API-Aufrufe gemacht.', + 'Tool Name': 'Werkzeugname', + Calls: 'Aufrufe', + 'Success Rate': 'Erfolgsrate', + 'Avg Duration': 'Durchschn. Dauer', + 'User Decision Summary': 'Benutzerentscheidungs-Zusammenfassung', + 'Total Reviewed Suggestions:': 'Insgesamt überprüfter Vorschläge:', + ' » Accepted:': ' » Akzeptiert:', + ' » Rejected:': ' » Abgelehnt:', + ' » Modified:': ' » Geändert:', + ' Overall Agreement Rate:': ' Gesamtzustimmungsrate:', + 'No tool calls have been made in this session.': + 'In dieser Sitzung wurden keine Werkzeugaufrufe gemacht.', + 'Session start time is unavailable, cannot calculate stats.': + 'Sitzungsstartzeit nicht verfügbar, Statistiken können nicht berechnet werden.', + + // ============================================================================ + // Loading Phrases + // ============================================================================ + 'Waiting for user confirmation...': 'Warten auf Benutzerbestätigung...', + '(esc to cancel, {{time}})': '(Esc zum Abbrechen, {{time}})', + + // ============================================================================ + // Loading Phrases + // ============================================================================ + WITTY_LOADING_PHRASES: [ + 'Auf gut Glück!', + 'Genialität wird ausgeliefert...', + 'Die Serifen werden aufgemalt...', + 'Durch den Schleimpilz navigieren...', + 'Die digitalen Geister werden befragt...', + 'Splines werden retikuliert...', + 'Die KI-Hamster werden aufgewärmt...', + 'Die Zaubermuschel wird befragt...', + 'Witzige Erwiderung wird generiert...', + 'Die Algorithmen werden poliert...', + 'Perfektion braucht Zeit (mein Code auch)...', + 'Frische Bytes werden gebrüht...', + 'Elektronen werden gezählt...', + 'Kognitive Prozessoren werden aktiviert...', + 'Auf Syntaxfehler im Universum wird geprüft...', + 'Einen Moment, Humor wird optimiert...', + 'Pointen werden gemischt...', + 'Neuronale Netze werden entwirrt...', + 'Brillanz wird kompiliert...', + 'wit.exe wird geladen...', + 'Die Wolke der Weisheit wird beschworen...', + 'Eine witzige Antwort wird vorbereitet...', + 'Einen Moment, ich debugge die Realität...', + 'Die Optionen werden verwirrt...', + 'Kosmische Frequenzen werden eingestellt...', + 'Eine Antwort wird erstellt, die Ihrer Geduld würdig ist...', + 'Die Einsen und Nullen werden kompiliert...', + 'Abhängigkeiten werden aufgelöst... und existenzielle Krisen...', + 'Erinnerungen werden defragmentiert... sowohl RAM als auch persönliche...', + 'Das Humor-Modul wird neu gestartet...', + 'Das Wesentliche wird zwischengespeichert (hauptsächlich Katzen-Memes)...', + 'Für lächerliche Geschwindigkeit wird optimiert', + 'Bits werden getauscht... sagen Sie es nicht den Bytes...', + 'Garbage Collection läuft... bin gleich zurück...', + 'Das Internet wird zusammengebaut...', + 'Kaffee wird in Code umgewandelt...', + 'Die Syntax der Realität wird aktualisiert...', + 'Die Synapsen werden neu verdrahtet...', + 'Ein verlegtes Semikolon wird gesucht...', + 'Die Zahnräder werden geschmiert...', + 'Die Server werden vorgeheizt...', + 'Der Fluxkompensator wird kalibriert...', + 'Der Unwahrscheinlichkeitsantrieb wird aktiviert...', + 'Die Macht wird kanalisiert...', + 'Die Sterne werden für optimale Antwort ausgerichtet...', + 'So sagen wir alle...', + 'Die nächste große Idee wird geladen...', + 'Einen Moment, ich bin in der Zone...', + 'Bereite mich vor, Sie mit Brillanz zu blenden...', + 'Einen Augenblick, ich poliere meinen Witz...', + 'Halten Sie durch, ich erschaffe ein Meisterwerk...', + 'Einen Moment, ich debugge das Universum...', + 'Einen Moment, ich richte die Pixel aus...', + 'Einen Moment, ich optimiere den Humor...', + 'Einen Moment, ich tune die Algorithmen...', + 'Warp-Geschwindigkeit aktiviert...', + 'Mehr Dilithium-Kristalle werden gesucht...', + 'Keine Panik...', + 'Dem weißen Kaninchen wird gefolgt...', + 'Die Wahrheit ist hier drin... irgendwo...', + 'Auf die Kassette wird gepustet...', + 'Ladevorgang... Machen Sie eine Fassrolle!', + 'Auf den Respawn wird gewartet...', + 'Der Kessel-Flug wird in weniger als 12 Parsec beendet...', + 'Der Kuchen ist keine Lüge, er lädt nur noch...', + 'Am Charaktererstellungsbildschirm wird herumgefummelt...', + 'Einen Moment, ich suche das richtige Meme...', + "'A' wird zum Fortfahren gedrückt...", + 'Digitale Katzen werden gehütet...', + 'Die Pixel werden poliert...', + 'Ein passender Ladebildschirm-Witz wird gesucht...', + 'Ich lenke Sie mit diesem witzigen Spruch ab...', + 'Fast da... wahrscheinlich...', + 'Unsere Hamster arbeiten so schnell sie können...', + 'Cloudy wird am Kopf gestreichelt...', + 'Die Katze wird gestreichelt...', + 'Meinen Chef rickrollen...', + 'Never gonna give you up, never gonna let you down...', + 'Auf den Bass wird geschlagen...', + 'Die Schnozbeeren werden probiert...', + "I'm going the distance, I'm going for speed...", + 'Ist dies das wahre Leben? Ist dies nur Fantasie?...', + 'Ich habe ein gutes Gefühl dabei...', + 'Den Bären wird gestupst...', + 'Recherche zu den neuesten Memes...', + 'Überlege, wie ich das witziger machen kann...', + 'Hmmm... lassen Sie mich nachdenken...', + 'Wie nennt man einen Fisch ohne Augen? Ein Fsh...', + 'Warum ging der Computer zur Therapie? Er hatte zu viele Bytes...', + 'Warum mögen Programmierer keine Natur? Sie hat zu viele Bugs...', + 'Warum bevorzugen Programmierer den Dunkelmodus? Weil Licht Bugs anzieht...', + 'Warum ging der Entwickler pleite? Er hat seinen ganzen Cache aufgebraucht...', + 'Was kann man mit einem kaputten Bleistift machen? Nichts, er ist sinnlos...', + 'Perkussive Wartung wird angewendet...', + 'Die richtige USB-Ausrichtung wird gesucht...', + 'Es wird sichergestellt, dass der magische Rauch in den Kabeln bleibt...', + 'Versuche Vim zu beenden...', + 'Das Hamsterrad wird angeworfen...', + 'Das ist kein Bug, das ist ein undokumentiertes Feature...', + 'Engage.', + 'Ich komme wieder... mit einer Antwort.', + 'Mein anderer Prozess ist eine TARDIS...', + 'Mit dem Maschinengeist wird kommuniziert...', + 'Die Gedanken marinieren lassen...', + 'Gerade erinnert, wo ich meine Schlüssel hingelegt habe...', + 'Über die Kugel wird nachgedacht...', + 'Ich habe Dinge gesehen, die Sie nicht glauben würden... wie einen Benutzer, der Lademeldungen liest.', + 'Nachdenklicher Blick wird initiiert...', + 'Was ist der Lieblingssnack eines Computers? Mikrochips.', + 'Warum tragen Java-Entwickler Brillen? Weil sie nicht C#.', + 'Der Laser wird aufgeladen... pew pew!', + 'Durch Null wird geteilt... nur Spaß!', + 'Suche nach einem erwachsenen Aufseh... ich meine, Verarbeitung.', + 'Es piept und boopt.', + 'Pufferung... weil auch KIs einen Moment brauchen.', + 'Quantenteilchen werden für schnellere Antwort verschränkt...', + 'Das Chrom wird poliert... an den Algorithmen.', + 'Sind Sie nicht unterhalten? (Arbeite daran!)', + 'Die Code-Gremlins werden beschworen... zum Helfen, natürlich.', + 'Warte nur auf das Einwahlton-Ende...', + 'Das Humor-O-Meter wird neu kalibriert.', + 'Mein anderer Ladebildschirm ist noch lustiger.', + 'Ziemlich sicher, dass irgendwo eine Katze über die Tastatur läuft...', + 'Verbessern... Verbessern... Lädt noch.', + 'Das ist kein Bug, das ist ein Feature... dieses Ladebildschirms.', + 'Haben Sie versucht, es aus- und wieder einzuschalten? (Den Ladebildschirm, nicht mich.)', + 'Zusätzliche Pylonen werden gebaut...', + ], +}; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index fb9475426..5e8b16629 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -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...", diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index ee583e0f9..9685c104b 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -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...', 'Раскручиваем колесо для хомяка...', 'Это не баг, а фича...', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 5c5d21679..c3550f7e8 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -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': '选择主题', diff --git a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts index 0ba94cbb2..be04b7f2b 100644 --- a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts +++ b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts @@ -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', () => { diff --git a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts index ef6655370..072497000 100644 --- a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts +++ b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts @@ -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; } diff --git a/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts b/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts index d0bd23255..ff3aa1f5d 100644 --- a/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts +++ b/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts @@ -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, diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts index 07fd168fc..840ba69d5 100644 --- a/packages/cli/src/nonInteractiveCli.test.ts +++ b/packages/cli/src/nonInteractiveCli.test.ts @@ -298,7 +298,9 @@ describe('runNonInteractive', () => { mockConfig, expect.objectContaining({ name: 'testTool' }), expect.any(AbortSignal), - undefined, + expect.objectContaining({ + outputUpdateHandler: expect.any(Function), + }), ); // Verify first call has isContinuation: false expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith( @@ -771,6 +773,52 @@ describe('runNonInteractive', () => { ); }); + it('should handle API errors in text mode and exit with error code', async () => { + (mockConfig.getOutputFormat as Mock).mockReturnValue(OutputFormat.TEXT); + setupMetricsMock(); + + // Simulate an API error event (like 401 unauthorized) + const apiErrorEvent: ServerGeminiStreamEvent = { + type: GeminiEventType.Error, + value: { + error: { + message: '401 Incorrect API key provided', + status: 401, + }, + }, + }; + + mockGeminiClient.sendMessageStream.mockReturnValue( + createStreamFromEvents([apiErrorEvent]), + ); + + let thrownError: Error | null = null; + try { + await runNonInteractive( + mockConfig, + mockSettings, + 'Test input', + 'prompt-id-api-error', + ); + // Should not reach here + expect.fail('Expected error to be thrown'); + } catch (error) { + thrownError = error as Error; + } + + // Should throw with the API error message + expect(thrownError).toBeTruthy(); + expect(thrownError?.message).toContain('401'); + expect(thrownError?.message).toContain('Incorrect API key provided'); + + // Verify error was written to stderr + expect(processStderrSpy).toHaveBeenCalled(); + const stderrCalls = processStderrSpy.mock.calls; + const errorOutput = stderrCalls.map((call) => call[0]).join(''); + expect(errorOutput).toContain('401'); + expect(errorOutput).toContain('Incorrect API key provided'); + }); + it('should handle FatalInputError with custom exit code in JSON format', async () => { (mockConfig.getOutputFormat as Mock).mockReturnValue(OutputFormat.JSON); setupMetricsMock(); @@ -1777,4 +1825,84 @@ describe('runNonInteractive', () => { { isContinuation: false }, ); }); + + it('should print tool output to console in text mode (non-Task tools)', async () => { + // Test that tool output is printed to stdout in text mode + const toolCallEvent: ServerGeminiStreamEvent = { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'tool-1', + name: 'run_in_terminal', + args: { command: 'npm outdated' }, + isClientInitiated: false, + prompt_id: 'prompt-id-tool-output', + }, + }; + + // Mock tool execution with outputUpdateHandler being called + mockCoreExecuteToolCall.mockImplementation( + async (_config, _request, _signal, options) => { + // Simulate tool calling outputUpdateHandler with output chunks + if (options?.outputUpdateHandler) { + options.outputUpdateHandler('tool-1', 'Package outdated\n'); + options.outputUpdateHandler('tool-1', 'npm@1.0.0 -> npm@2.0.0\n'); + } + return { + responseParts: [ + { + functionResponse: { + id: 'tool-1', + name: 'run_in_terminal', + response: { + output: 'Package outdated\nnpm@1.0.0 -> npm@2.0.0', + }, + }, + }, + ], + }; + }, + ); + + const firstCallEvents: ServerGeminiStreamEvent[] = [ + toolCallEvent, + { + type: GeminiEventType.Finished, + value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } }, + }, + ]; + + const secondCallEvents: ServerGeminiStreamEvent[] = [ + { type: GeminiEventType.Content, value: 'Dependencies checked' }, + { + type: GeminiEventType.Finished, + value: { reason: undefined, usageMetadata: { totalTokenCount: 3 } }, + }, + ]; + + mockGeminiClient.sendMessageStream + .mockReturnValueOnce(createStreamFromEvents(firstCallEvents)) + .mockReturnValueOnce(createStreamFromEvents(secondCallEvents)); + + await runNonInteractive( + mockConfig, + mockSettings, + 'Check dependencies', + 'prompt-id-tool-output', + ); + + // Verify that executeToolCall was called with outputUpdateHandler + expect(mockCoreExecuteToolCall).toHaveBeenCalledWith( + mockConfig, + expect.objectContaining({ name: 'run_in_terminal' }), + expect.any(AbortSignal), + expect.objectContaining({ + outputUpdateHandler: expect.any(Function), + }), + ); + + // Verify tool output was written to stdout + expect(processStdoutSpy).toHaveBeenCalledWith('Package outdated\n'); + expect(processStdoutSpy).toHaveBeenCalledWith('npm@1.0.0 -> npm@2.0.0\n'); + expect(processStdoutSpy).toHaveBeenCalledWith('Dependencies checked'); + }); }); diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 067f190b9..4088c9283 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Config, ToolCallRequestInfo } from '@qwen-code/qwen-code-core'; +import type { + Config, + ToolCallRequestInfo, + ToolResultDisplay, +} from '@qwen-code/qwen-code-core'; import { isSlashCommand } from './ui/utils/commandUtils.js'; import type { LoadedSettings } from './config/settings.js'; import { @@ -308,6 +312,8 @@ export async function runNonInteractive( config.getContentGeneratorConfig()?.authType, ); process.stderr.write(`${errorText}\n`); + // Throw error to exit with non-zero code + throw new Error(errorText); } } } @@ -333,7 +339,7 @@ export async function runNonInteractive( ? options.controlService.permission.getToolCallUpdateCallback() : undefined; - // Only pass outputUpdateHandler for Task tool + // Create output handler for Task tool (for subagent execution) const isTaskTool = finalRequestInfo.name === 'task'; const taskToolProgress = isTaskTool ? createTaskToolProgressHandler( @@ -343,20 +349,41 @@ export async function runNonInteractive( ) : undefined; const taskToolProgressHandler = taskToolProgress?.handler; + + // Create output handler for non-Task tools in text mode (for console output) + const nonTaskOutputHandler = + !isTaskTool && !adapter + ? (callId: string, outputChunk: ToolResultDisplay) => { + // Print tool output to console in text mode + if (typeof outputChunk === 'string') { + process.stdout.write(outputChunk); + } else if ( + outputChunk && + typeof outputChunk === 'object' && + 'ansiOutput' in outputChunk + ) { + // Handle ANSI output - just print as string for now + process.stdout.write(String(outputChunk.ansiOutput)); + } + } + : undefined; + + // Combine output handlers + const outputUpdateHandler = + taskToolProgressHandler || nonTaskOutputHandler; + const toolResponse = await executeToolCall( config, finalRequestInfo, abortController.signal, - isTaskTool && taskToolProgressHandler + outputUpdateHandler || toolCallUpdateCallback ? { - outputUpdateHandler: taskToolProgressHandler, - onToolCallsUpdate: toolCallUpdateCallback, - } - : toolCallUpdateCallback - ? { + ...(outputUpdateHandler && { outputUpdateHandler }), + ...(toolCallUpdateCallback && { onToolCallsUpdate: toolCallUpdateCallback, - } - : undefined, + }), + } + : undefined, ); // Note: In JSON mode, subagent messages are automatically added to the main diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts index cbfca4d2d..151faf324 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts @@ -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( diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.ts b/packages/cli/src/services/prompt-processors/shellProcessor.ts index c10526e62..2a6df7161 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.ts @@ -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)); } } diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 7921b2039..38dad449c 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -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); diff --git a/packages/cli/src/ui/IdeIntegrationNudge.tsx b/packages/cli/src/ui/IdeIntegrationNudge.tsx index 8ab350064..9cd6d5311 100644 --- a/packages/cli/src/ui/IdeIntegrationNudge.tsx +++ b/packages/cli/src/ui/IdeIntegrationNudge.tsx @@ -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 ( { let mockContext: CommandContext; + let mockSetApprovalMode: ReturnType; 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(); }); diff --git a/packages/cli/src/ui/commands/approvalModeCommand.ts b/packages/cli/src/ui/commands/approvalModeCommand.ts index 90ae774bf..f41e4b1cf 100644 --- a/packages/cli/src/ui/commands/approvalModeCommand.ts +++ b/packages/cli/src/ui/commands/approvalModeCommand.ts @@ -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 => ({ - type: 'dialog', - dialog: 'approval-mode', - }), + context: CommandContext, + args: string, + ): Promise => { + 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 }), + }; + }, }; diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts index ebb7e3dc7..be3a12bc9 100644 --- a/packages/cli/src/ui/commands/ideCommand.ts +++ b/packages/cli/src/ui/commands/ideCommand.ts @@ -191,11 +191,23 @@ export const ideCommand = async (): Promise => { 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(), ); diff --git a/packages/cli/src/ui/components/StandaloneSessionPicker.tsx b/packages/cli/src/ui/components/StandaloneSessionPicker.tsx index bac7f23df..13a176c62 100644 --- a/packages/cli/src/ui/components/StandaloneSessionPicker.tsx +++ b/packages/cli/src/ui/components/StandaloneSessionPicker.tsx @@ -87,7 +87,13 @@ export async function showResumeSessionPicker( let selectedId: string | undefined; const { unmount, waitUntilExit } = render( - + { diff --git a/packages/cli/src/utils/errors.test.ts b/packages/cli/src/utils/errors.test.ts index 818c3ac39..e3a27bd42 100644 --- a/packages/cli/src/utils/errors.test.ts +++ b/packages/cli/src/utils/errors.test.ts @@ -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 + ).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 + ).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 + ).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 + ).mockReturnValue(OutputFormat.TEXT); + + handleToolError( + toolName, + toolError, + mockConfig, + ToolErrorType.FILE_NOT_FOUND, + ); + + expect(processStderrWriteSpy).not.toHaveBeenCalled(); + expect(processExitSpy).not.toHaveBeenCalled(); + }); + }); }); describe('handleCancellationError', () => { diff --git a/packages/cli/src/utils/errors.ts b/packages/cli/src/utils/errors.ts index 5338fa2fd..f804a630c 100644 --- a/packages/cli/src/utils/errors.ts +++ b/packages/cli/src/utils/errors.ts @@ -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}`, diff --git a/packages/core/package.json b/packages/core/package.json index de06c78bc..e7baa13b2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index aeffdfc78..c7e2806ac 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -824,7 +824,6 @@ export class CoreToolScheduler { */ const shouldAutoDeny = !this.config.isInteractive() && - !this.config.getIdeMode() && !this.config.getExperimentalZedIntegration() && this.config.getInputFormat() !== InputFormat.STREAM_JSON; diff --git a/packages/core/src/core/openaiContentGenerator/converter.ts b/packages/core/src/core/openaiContentGenerator/converter.ts index 07a8f1831..5ec7a4935 100644 --- a/packages/core/src/core/openaiContentGenerator/converter.ts +++ b/packages/core/src/core/openaiContentGenerator/converter.ts @@ -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, }; } diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts index 88ac38f6a..ef27a7798 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts @@ -317,15 +317,22 @@ export class ContentGenerationPipeline { } private buildReasoningConfig(): Record { - 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 {}; } /** diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts index c56069503..521a6768c 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts @@ -58,8 +58,6 @@ export class DefaultOpenAICompatibleProvider } getDefaultGenerationConfig(): GenerateContentConfig { - return { - topP: 0.95, - }; + return {}; } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 56680403b..7f7bd115b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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'; diff --git a/packages/core/src/services/shellExecutionService.test.ts b/packages/core/src/services/shellExecutionService.test.ts index 4dfc48918..e63fba28d 100644 --- a/packages/core/src/services/shellExecutionService.test.ts +++ b/packages/core/src/services/shellExecutionService.test.ts @@ -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, }), ); }); diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index be0c26ff7..c870b5f4e 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -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): 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(); + private static activeChildProcesses = new Set(); + + 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 { diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 8c98045dd..8b6788a70 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -248,7 +248,7 @@ describe('ShellTool', () => { wrappedCommand, '/test/dir', expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -275,7 +275,7 @@ describe('ShellTool', () => { wrappedCommand, expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -300,7 +300,7 @@ describe('ShellTool', () => { wrappedCommand, expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -325,7 +325,7 @@ describe('ShellTool', () => { wrappedCommand, expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -350,7 +350,7 @@ describe('ShellTool', () => { wrappedCommand, '/test/dir/subdir', expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -378,7 +378,7 @@ describe('ShellTool', () => { 'dir', '/test/dir', expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -471,7 +471,7 @@ describe('ShellTool', () => { expect(summarizer.summarizeToolOutput).toHaveBeenCalledWith( expect.any(String), mockConfig.getGeminiClient(), - mockAbortSignal, + expect.any(AbortSignal), 1000, ); expect(result.llmContent).toBe('summarized output'); @@ -580,7 +580,7 @@ describe('ShellTool', () => { ), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -610,7 +610,7 @@ describe('ShellTool', () => { ), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -640,7 +640,7 @@ describe('ShellTool', () => { ), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -699,7 +699,7 @@ describe('ShellTool', () => { expect.stringContaining('npm install'), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -728,7 +728,7 @@ describe('ShellTool', () => { expect.stringContaining('git commit'), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -758,7 +758,7 @@ describe('ShellTool', () => { ), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -794,7 +794,7 @@ describe('ShellTool', () => { expect.stringContaining('git commit -m "Initial commit"'), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -831,7 +831,7 @@ describe('ShellTool', () => { ), expect.any(String), expect.any(Function), - mockAbortSignal, + expect.any(AbortSignal), false, {}, ); @@ -962,4 +962,41 @@ spanning multiple lines"`; expect(shellTool.description).toMatchSnapshot(); }); }); + + describe('Windows background execution', () => { + it('should clean up trailing ampersand on Windows for background tasks', async () => { + vi.mocked(os.platform).mockReturnValue('win32'); + const mockAbortSignal = new AbortController().signal; + + const invocation = shellTool.build({ + command: 'npm start &', + is_background: true, + }); + + const promise = invocation.execute(mockAbortSignal); + + // Simulate immediate success (process started) + resolveExecutionPromise({ + rawOutput: Buffer.from(''), + output: '', + exitCode: 0, + signal: null, + error: null, + aborted: false, + pid: 12345, + executionMethod: 'child_process', + }); + + await promise; + + expect(mockShellExecutionService).toHaveBeenCalledWith( + 'npm start', + expect.any(String), + expect.any(Function), + expect.any(AbortSignal), + false, + {}, + ); + }); + }); }); diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 5354f9251..d7afae599 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -143,11 +143,24 @@ export class ShellToolInvocation extends BaseToolInvocation< const shouldRunInBackground = this.params.is_background; let finalCommand = processedCommand; - // If explicitly marked as background and doesn't already end with &, add it - if (shouldRunInBackground && !finalCommand.trim().endsWith('&')) { + // On non-Windows, use & to run in background. + // On Windows, we don't use start /B because it creates a detached process that + // doesn't die when the parent dies. Instead, we rely on the race logic below + // to return early while keeping the process attached (detached: false). + if ( + !isWindows && + shouldRunInBackground && + !finalCommand.trim().endsWith('&') + ) { finalCommand = finalCommand.trim() + ' &'; } + // On Windows, we rely on the race logic below to handle background tasks. + // We just ensure the command string is clean. + if (isWindows && shouldRunInBackground) { + finalCommand = finalCommand.trim().replace(/&+$/, '').trim(); + } + // pgrep is not available on Windows, so we can't get background PIDs const commandToExecute = isWindows ? finalCommand @@ -169,10 +182,6 @@ export class ShellToolInvocation extends BaseToolInvocation< commandToExecute, cwd, (event: ShellOutputEvent) => { - if (!updateOutput) { - return; - } - let shouldUpdate = false; switch (event.type) { @@ -201,7 +210,7 @@ export class ShellToolInvocation extends BaseToolInvocation< } } - if (shouldUpdate) { + if (shouldUpdate && updateOutput) { updateOutput( typeof cumulativeOutput === 'string' ? cumulativeOutput @@ -219,6 +228,21 @@ export class ShellToolInvocation extends BaseToolInvocation< setPidCallback(pid); } + if (shouldRunInBackground) { + // For background tasks, return immediately with PID info + // Note: We cannot reliably detect startup errors for background processes + // since their stdio is typically detached/ignored + const pidMsg = pid ? ` PID: ${pid}` : ''; + const killHint = isWindows + ? ' (Use taskkill /F /T /PID to stop)' + : ' (Use kill to stop)'; + + return { + llmContent: `Background command started.${pidMsg}${killHint}`, + returnDisplay: `Background command started.${pidMsg}${killHint}`, + }; + } + const result = await resultPromise; const backgroundPIDs: number[] = []; diff --git a/packages/sdk-java/.editorconfig b/packages/sdk-java/.editorconfig new file mode 100644 index 000000000..53a4241f9 --- /dev/null +++ b/packages/sdk-java/.editorconfig @@ -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 diff --git a/packages/sdk-java/.gitignore b/packages/sdk-java/.gitignore new file mode 100644 index 000000000..23cdb8c94 --- /dev/null +++ b/packages/sdk-java/.gitignore @@ -0,0 +1,14 @@ +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +# Mac +.DS_Store + +# Maven +log/ +target/ + +/docs/ diff --git a/packages/sdk-java/LICENSE b/packages/sdk-java/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/packages/sdk-java/LICENSE @@ -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. diff --git a/packages/sdk-java/QWEN.md b/packages/sdk-java/QWEN.md new file mode 100644 index 000000000..4fedee46f --- /dev/null +++ b/packages/sdk-java/QWEN.md @@ -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, 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 diff --git a/packages/sdk-java/README.md b/packages/sdk-java/README.md new file mode 100644 index 000000000..772c93742 --- /dev/null +++ b/packages/sdk-java/README.md @@ -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 + + com.alibaba + qwencode-sdk + {$version} + +``` + +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 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() {{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 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, 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. diff --git a/packages/sdk-java/checkstyle.xml b/packages/sdk-java/checkstyle.xml new file mode 100644 index 000000000..c67c1319f --- /dev/null +++ b/packages/sdk-java/checkstyle.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/sdk-java/pom.xml b/packages/sdk-java/pom.xml new file mode 100644 index 000000000..6a5fae4f4 --- /dev/null +++ b/packages/sdk-java/pom.xml @@ -0,0 +1,193 @@ + + 4.0.0 + com.alibaba + qwencode-sdk + jar + 0.0.1-alpha + qwencode-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. + + https://maven.apache.org + + + Apache 2 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + https://github.com/QwenLM/qwen-code + scm:git:https://github.com/QwenLM/qwen-code.git + + + 1.8 + 1.8 + UTF-8 + 3.6.0 + 0.8.12 + 5.14.1 + 1.3.16 + 2.0.60 + 3.13.0 + 0.8.0 + 2.2.1 + 2.9.1 + 1.5 + + + + + + org.junit + junit-bom + pom + ${junit5.version} + import + + + + + + ch.qos.logback + logback-classic + ${logback-classic.version} + + + org.apache.commons + commons-lang3 + 3.20.0 + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle-maven-plugin.version} + + checkstyle.xml + + + + + check + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + + prepare-agent + + + + report + test + + report + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + central + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + + + + Alibaba Group + https://github.com/alibaba + + + + skyfire + skyfire + gengwei.gw(at)alibaba-inc.com + + Developer + Designer + + +8 + https://github.com/gwinthis + + + + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + + central + https://central.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCodeCli.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCodeCli.java new file mode 100644 index 000000000..9a654034d --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCodeCli.java @@ -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 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 simpleQuery(String prompt, TransportOptions transportOptions) { + final List 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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantContent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantContent.java new file mode 100644 index 000000000..ba0356545 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantContent.java @@ -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 The type of content + * @author skyfire + * @version $Id: 0.0.1 + */ +public interface AssistantContent { + /** + * 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 { + /** + * Gets the text content. + * + * @return The text content + */ + String getText(); + } + + /** + * Represents thinking content from the assistant. + */ + interface ThingkingAssistantContent extends AssistantContent { + /** + * Gets the thinking content. + * + * @return The thinking content + */ + String getThinking(); + } + + /** + * Represents tool use content from the assistant. + */ + interface ToolUseAssistantContent extends AssistantContent> { + /** + * Gets the tool input. + * + * @return The tool input + */ + Map getInput(); + } + + /** + * Represents tool result content from the assistant. + */ + interface ToolResultAssistantContent extends AssistantContent { + /** + * 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(); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantUsage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantUsage.java new file mode 100644 index 000000000..8ecb2a5bf --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantUsage.java @@ -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; + } + + /** + *

toString.

+ * + * @return a {@link java.lang.String} object. + */ + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/CLIPermissionDenial.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/CLIPermissionDenial.java new file mode 100644 index 000000000..bc155b776 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/CLIPermissionDenial.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/Capabilities.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/Capabilities.java new file mode 100644 index 000000000..e6cbadfe9 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/Capabilities.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/ExtendedUsage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/ExtendedUsage.java new file mode 100644 index 000000000..7e67a629f --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/ExtendedUsage.java @@ -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; + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/InitializeConfig.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/InitializeConfig.java new file mode 100644 index 000000000..36296d053 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/InitializeConfig.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/ModelUsage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/ModelUsage.java new file mode 100644 index 000000000..33b426c4a --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/ModelUsage.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/PermissionMode.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/PermissionMode.java new file mode 100644 index 000000000..420dd760d --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/PermissionMode.java @@ -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); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/Usage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/Usage.java new file mode 100644 index 000000000..7fb430511 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/Usage.java @@ -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; + } + + /** + *

toString.

+ * + * @return a {@link java.lang.String} object. + */ + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Allow.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Allow.java new file mode 100644 index 000000000..5ed08f099 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Allow.java @@ -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 updatedInput; + + /** + * Gets the updated input. + * + * @return The updated input + */ + public Map getUpdatedInput() { + return updatedInput; + } + + /** + * Sets the updated input. + * + * @param updatedInput The updated input + * @return This instance for method chaining + */ + public Allow setUpdatedInput(Map updatedInput) { + this.updatedInput = updatedInput; + return this; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Behavior.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Behavior.java new file mode 100644 index 000000000..5adca830e --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Behavior.java @@ -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"); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Deny.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Deny.java new file mode 100644 index 000000000..042673e45 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/behavior/Deny.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/Message.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/Message.java new file mode 100644 index 000000000..855fb5de7 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/Message.java @@ -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(); +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/MessageBase.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/MessageBase.java new file mode 100644 index 000000000..37390164a --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/MessageBase.java @@ -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; + + /** + *

toString.

+ * + * @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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKResultMessage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKResultMessage.java new file mode 100644 index 000000000..58889630b --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKResultMessage.java @@ -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 modelUsage; + + /** + * List of permission denials. + */ + @JSONField(name = "permission_denials") + private List 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 getModelUsage() { + return modelUsage; + } + + /** + * Sets the model usage information. + * + * @param modelUsage The model usage information + */ + public void setModelUsage(Map modelUsage) { + this.modelUsage = modelUsage; + } + + /** + * Gets the list of permission denials. + * + * @return The list of permission denials + */ + public List getPermissionDenials() { + return permissionDenials; + } + + /** + * Sets the list of permission denials. + * + * @param permissionDenials The list of permission denials + */ + public void setPermissionDenials(List 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; + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKSystemMessage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKSystemMessage.java new file mode 100644 index 000000000..abdb0fe5b --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKSystemMessage.java @@ -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 tools; + /** + * List of MCP servers. + */ + @JSONField(name = "mcp_servers") + private List mcpServers; + /** + * Model information. + */ + private String model; + /** + * Permission mode. + */ + @JSONField(name = "permission_mode") + private String permissionMode; + /** + * Available slash commands. + */ + @JSONField(name = "slash_commands") + private List 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 agents; + /** + * Available skills. + */ + private List skills; + /** + * Capabilities information. + */ + private Map 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 getTools() { + return tools; + } + + /** + * Sets the list of available tools. + * + * @param tools The list of available tools + */ + public void setTools(List tools) { + this.tools = tools; + } + + /** + * Gets the list of MCP servers. + * + * @return The list of MCP servers + */ + public List getMcpServers() { + return mcpServers; + } + + /** + * Sets the list of MCP servers. + * + * @param mcpServers The list of MCP servers + */ + public void setMcpServers(List 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 getSlashCommands() { + return slashCommands; + } + + /** + * Sets the available slash commands. + * + * @param slashCommands The available slash commands + */ + public void setSlashCommands(List 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 getAgents() { + return agents; + } + + /** + * Sets the available agents. + * + * @param agents The available agents + */ + public void setAgents(List agents) { + this.agents = agents; + } + + /** + * Gets the available skills. + * + * @return The available skills + */ + public List getSkills() { + return skills; + } + + /** + * Sets the available skills. + * + * @param skills The available skills + */ + public void setSkills(List skills) { + this.skills = skills; + } + + /** + * Gets the capabilities information. + * + * @return The capabilities information + */ + public Map getCapabilities() { + return capabilities; + } + + /** + * Sets the capabilities information. + * + * @param capabilities The capabilities information + */ + public void setCapabilities(Map 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; + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKUserMessage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKUserMessage.java new file mode 100644 index 000000000..e2d9f1e2a --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/SDKUserMessage.java @@ -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 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 getOptions() { + return options; + } + + /** + * Sets the additional options. + * + * @param options The additional options + * @return This instance for method chaining + */ + public SDKUserMessage setOptions(Map 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; + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/APIAssistantMessage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/APIAssistantMessage.java new file mode 100644 index 000000000..b54bca443 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/APIAssistantMessage.java @@ -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> 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> getContent() { + return content; + } + + /** + * Sets the message content. + * + * @param content The message content + */ + public void setContent(List> content) { + this.content = content; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/SDKAssistantMessage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/SDKAssistantMessage.java new file mode 100644 index 000000000..b94fde07b --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/SDKAssistantMessage.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/SDKPartialAssistantMessage.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/SDKPartialAssistantMessage.java new file mode 100644 index 000000000..0ace1e3f0 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/SDKPartialAssistantMessage.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/Annotation.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/Annotation.java new file mode 100644 index 000000000..880bb12f8 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/Annotation.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ContentBlock.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ContentBlock.java new file mode 100644 index 000000000..fabee58b5 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ContentBlock.java @@ -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 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 implements AssistantContent { + /** + * The type of the content block. + */ + protected String type; + /** + * List of annotations. + */ + protected List 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 getAnnotations() { + return annotations; + } + + /** + * Sets the list of annotations. + * + * @param annotations The list of annotations + */ + public void setAnnotations(List 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; + } + + /** + *

toString.

+ * + * @return a {@link java.lang.String} object. + */ + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/TextBlock.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/TextBlock.java new file mode 100644 index 000000000..5b6953fda --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/TextBlock.java @@ -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 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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ThinkingBlock.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ThinkingBlock.java new file mode 100644 index 000000000..52967e67b --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ThinkingBlock.java @@ -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 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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ToolResultBlock.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ToolResultBlock.java new file mode 100644 index 000000000..35185a40d --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ToolResultBlock.java @@ -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 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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ToolUseBlock.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ToolUseBlock.java new file mode 100644 index 000000000..91cfacb36 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/block/ToolUseBlock.java @@ -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> implements ToolUseAssistantContent { + /** + * The tool use ID. + */ + private String id; + /** + * The tool name. + */ + private String name; + /** + * The tool input. + */ + private Map input; + /** + * List of annotations. + */ + private List 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 getInput() { + return input; + } + + /** + * Sets the tool input. + * + * @param input The tool input + */ + public void setInput(Map input) { + this.input = input; + } + + /** + * Gets the list of annotations. + * + * @return The list of annotations + */ + public List getAnnotations() { + return annotations; + } + + /** + * {@inheritDoc} + * + * Sets the list of annotations. + */ + @Override + public void setAnnotations(List annotations) { + this.annotations = annotations; + } + + /** + * {@inheritDoc} + * + * Gets the content of the assistant. + */ + @Override + public Map getContentOfAssistant() { + return Collections.emptyMap(); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockDeltaEvent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockDeltaEvent.java new file mode 100644 index 000000000..b7328b08e --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockDeltaEvent.java @@ -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 The type of content + */ + @JSONType(typeKey = "type", typeName = "ContentBlockDelta", + seeAlso = {ContentBlockDeltaText.class, ContentBlockDeltaThinking.class, ContentBlockDeltaInputJson.class}) + public abstract static class ContentBlockDelta implements AssistantContent { + /** + * 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 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 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> 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 getContentOfAssistant() { + return getInput(); + } + + @Override + public Map getInput() { + return JSON.parseObject(partialJson, new TypeReference>() {}); + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockStartEvent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockStartEvent.java new file mode 100644 index 000000000..758e59660 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockStartEvent.java @@ -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; +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockStopEvent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockStopEvent.java new file mode 100644 index 000000000..ed1241957 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/ContentBlockStopEvent.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/MessageStartStreamEvent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/MessageStartStreamEvent.java new file mode 100644 index 000000000..2377e6662 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/MessageStartStreamEvent.java @@ -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; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/MessageStopStreamEvent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/MessageStopStreamEvent.java new file mode 100644 index 000000000..cbf32c27a --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/MessageStopStreamEvent.java @@ -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{ +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/StreamEvent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/StreamEvent.java new file mode 100644 index 000000000..b45c852ca --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/assistant/event/StreamEvent.java @@ -0,0 +1,37 @@ +package com.alibaba.qwen.code.cli.protocol.message.assistant.event; + +import com.alibaba.fastjson2.annotation.JSONType; + +/** + * Base class for stream events during message streaming. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "type", typeName = "StreamEvent", + seeAlso = {MessageStartStreamEvent.class, MessageStopStreamEvent.class, ContentBlockStartEvent.class, ContentBlockStopEvent.class, + ContentBlockDeltaEvent.class}) +public class StreamEvent { + /** + * The type of the stream event. + */ + protected String type; + + /** + * Gets the type of the stream event. + * + * @return The type of the stream event + */ + public String getType() { + return type; + } + + /** + * Sets the type of the stream event. + * + * @param type The type of the stream event + */ + public void setType(String type) { + this.type = type; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlRequest.java new file mode 100644 index 000000000..58079bc6b --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlRequest.java @@ -0,0 +1,90 @@ +package com.alibaba.qwen.code.cli.protocol.message.control; + +import java.util.UUID; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONType; +import com.alibaba.qwen.code.cli.protocol.message.MessageBase; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload; + +/** + * Represents a control request to the CLI. + * + * @param The type of the request object + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "type", typeName = "control_request") +public class CLIControlRequest extends MessageBase { + /** + * The ID of the request. + */ + @JSONField(name = "request_id") + private String requestId = UUID.randomUUID().toString(); + + /** + * The actual request object. + */ + private R request; + + /** + * Creates a new CLIControlRequest instance and sets the type to "control_request". + */ + public CLIControlRequest() { + super(); + type = "control_request"; + } + + /** + * Creates a new control request with the specified request object. + * + * @param request The request object + * @param The type of the request object + * @return A new control request instance + */ + public static CLIControlRequest create(T request) { + CLIControlRequest controlRequest = new CLIControlRequest<>(); + controlRequest.setRequest(request); + return controlRequest; + } + + /** + * Gets the ID of the request. + * + * @return The ID of the request + */ + public String getRequestId() { + return requestId; + } + + /** + * Sets the ID of the request. + * + * @param requestId The ID of the request + * @return This instance for method chaining + */ + public CLIControlRequest setRequestId(String requestId) { + this.requestId = requestId; + return this; + } + + /** + * Gets the actual request object. + * + * @return The actual request object + */ + public R getRequest() { + return request; + } + + /** + * Sets the actual request object. + * + * @param request The actual request object + * @return This instance for method chaining + */ + public CLIControlRequest setRequest(R request) { + this.request = request; + return this; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlResponse.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlResponse.java new file mode 100644 index 000000000..7e416786e --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/CLIControlResponse.java @@ -0,0 +1,138 @@ +package com.alibaba.qwen.code.cli.protocol.message.control; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONType; +import com.alibaba.qwen.code.cli.protocol.message.MessageBase; + +/** + * Represents a control response from the CLI. + * + * @param The type of the response object + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "type", typeName = "control_response") +public class CLIControlResponse extends MessageBase { + /** + * The response object. + */ + private Response response; + + /** + * Creates a new CLIControlResponse instance and sets the type to "control_response". + */ + public CLIControlResponse() { + super(); + this.type = "control_response"; + } + + /** + * Gets the response object. + * + * @return The response object + */ + public Response getResponse() { + return response; + } + + /** + * Sets the response object. + * + * @param response The response object + */ + public void setResponse(Response response) { + this.response = response; + } + + /** + * Creates a new response object. + * + * @return A new response object + */ + public Response createResponse() { + Response response = new Response<>(); + this.setResponse(response); + return response; + } + + /** + * Represents the response information. + * + * @param The type of the response object + */ + public static class Response { + /** + * The ID of the request. + */ + @JSONField(name = "request_id") + private String requestId; + /** + * The subtype of the response. + */ + private String subtype = "success"; + /** + * The actual response. + */ + R response; + + /** + * Gets the ID of the request. + * + * @return The ID of the request + */ + public String getRequestId() { + return requestId; + } + + /** + * Sets the ID of the request. + * + * @param requestId The ID of the request + * @return This instance for method chaining + */ + public Response setRequestId(String requestId) { + this.requestId = requestId; + return this; + } + + /** + * Gets the subtype of the response. + * + * @return The subtype of the response + */ + public String getSubtype() { + return subtype; + } + + /** + * Sets the subtype of the response. + * + * @param subtype The subtype of the response + * @return This instance for method chaining + */ + public Response setSubtype(String subtype) { + this.subtype = subtype; + return this; + } + + /** + * Gets the actual response. + * + * @return The actual response + */ + public R getResponse() { + return response; + } + + /** + * Sets the actual response. + * + * @param response The actual response + * @return This instance for method chaining + */ + public Response setResponse(R response) { + this.response = response; + return this; + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInitializeRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInitializeRequest.java new file mode 100644 index 000000000..a990e0316 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInitializeRequest.java @@ -0,0 +1,45 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONType; +import com.alibaba.qwen.code.cli.protocol.data.InitializeConfig; + +/** + * Represents a control initialize request to the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "initialize") +public class CLIControlInitializeRequest extends ControlRequestPayload { + public CLIControlInitializeRequest() { + super(); + this.subtype = "initialize"; + } + + /** + * The initialization configuration. + */ + @JSONField(unwrapped = true) + InitializeConfig initializeConfig = new InitializeConfig(); + + /** + * Gets the initialization configuration. + * + * @return The initialization configuration + */ + public InitializeConfig getInitializeConfig() { + return initializeConfig; + } + + /** + * Sets the initialization configuration. + * + * @param initializeConfig The initialization configuration + * @return This instance for method chaining + */ + public CLIControlInitializeRequest setInitializeConfig(InitializeConfig initializeConfig) { + this.initializeConfig = initializeConfig; + return this; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInitializeResponse.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInitializeResponse.java new file mode 100644 index 000000000..aabeec016 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInitializeResponse.java @@ -0,0 +1,41 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONType; +import com.alibaba.qwen.code.cli.protocol.data.Capabilities; + +/** + * Represents a control initialize response from the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "initialize") +public class CLIControlInitializeResponse extends ControlResponsePayload { + public CLIControlInitializeResponse() { + super(); + this.subtype = "initialize"; + } + + /** + * The capabilities' information. + */ + Capabilities capabilities; + + /** + * Gets the capabilities information. + * + * @return The capabilities information + */ + public Capabilities getCapabilities() { + return capabilities; + } + + /** + * Sets the capabilities information. + * + * @param capabilities The capabilities information + */ + public void setCapabilities(Capabilities capabilities) { + this.capabilities = capabilities; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInterruptRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInterruptRequest.java new file mode 100644 index 000000000..cf3f83567 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlInterruptRequest.java @@ -0,0 +1,17 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONType; + +/** + * Represents a control interrupt request to the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "interrupt") +public class CLIControlInterruptRequest extends ControlRequestPayload { + public CLIControlInterruptRequest() { + super(); + setSubtype("interrupt"); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlPermissionRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlPermissionRequest.java new file mode 100644 index 000000000..e15133dfb --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlPermissionRequest.java @@ -0,0 +1,235 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import java.util.List; +import java.util.Map; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONType; + +/** + * Represents a control permission request to the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "can_use_tool") +public class CLIControlPermissionRequest extends ControlRequestPayload { + public CLIControlPermissionRequest() { + super(); + this.subtype = "can_use_tool"; + } + + /** + * The name of the tool requesting permission. + */ + @JSONField(name = "tool_name") + private String toolName; + + /** + * The ID of the tool use. + */ + @JSONField(name = "tool_use_id") + private String toolUseId; + + /** + * The input for the tool. + */ + private Map input; + + /** + * List of permission suggestions. + */ + @JSONField(name = "permission_suggestions") + private List permissionSuggestions; + + /** + * The blocked path. + */ + @JSONField(name = "blocked_path") + private String blockedPath; + + /** + * Gets the name of the tool requesting permission. + * + * @return The name of the tool requesting permission + */ + public String getToolName() { + return toolName; + } + + /** + * Sets the name of the tool requesting permission. + * + * @param toolName The name of the tool requesting permission + */ + public void setToolName(String toolName) { + this.toolName = toolName; + } + + /** + * Gets the ID of the tool use. + * + * @return The ID of the tool use + */ + public String getToolUseId() { + return toolUseId; + } + + /** + * Sets the ID of the tool use. + * + * @param toolUseId The ID of the tool use + */ + public void setToolUseId(String toolUseId) { + this.toolUseId = toolUseId; + } + + /** + * Gets the input for the tool. + * + * @return The input for the tool + */ + public Map getInput() { + return input; + } + + /** + * Sets the input for the tool. + * + * @param input The input for the tool + */ + public void setInput(Map input) { + this.input = input; + } + + /** + * Gets the list of permission suggestions. + * + * @return The list of permission suggestions + */ + public List getPermissionSuggestions() { + return permissionSuggestions; + } + + /** + * Sets the list of permission suggestions. + * + * @param permissionSuggestions The list of permission suggestions + */ + public void setPermissionSuggestions( + List permissionSuggestions) { + this.permissionSuggestions = permissionSuggestions; + } + + /** + * Gets the blocked path. + * + * @return The blocked path + */ + public String getBlockedPath() { + return blockedPath; + } + + /** + * Sets the blocked path. + * + * @param blockedPath The blocked path + */ + public void setBlockedPath(String blockedPath) { + this.blockedPath = blockedPath; + } + + /** + * Represents a permission suggestion. + */ + public static class PermissionSuggestion { + /** + * The type of suggestion (allow, deny, modify). + */ + private String type; // 'allow' | 'deny' | 'modify' + /** + * The label for the suggestion. + */ + private String label; + /** + * The description of the suggestion. + */ + private String description; + /** + * The modified input. + */ + private Object modifiedInput; + + /** + * Gets the type of suggestion. + * + * @return The type of suggestion + */ + public String getType() { + return type; + } + + /** + * Sets the type of suggestion. + * + * @param type The type of suggestion + */ + public void setType(String type) { + this.type = type; + } + + /** + * Gets the label for the suggestion. + * + * @return The label for the suggestion + */ + public String getLabel() { + return label; + } + + /** + * Sets the label for the suggestion. + * + * @param label The label for the suggestion + */ + public void setLabel(String label) { + this.label = label; + } + + /** + * Gets the description of the suggestion. + * + * @return The description of the suggestion + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the suggestion. + * + * @param description The description of the suggestion + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the modified input. + * + * @return The modified input + */ + public Object getModifiedInput() { + return modifiedInput; + } + + /** + * Sets the modified input. + * + * @param modifiedInput The modified input + */ + public void setModifiedInput(Object modifiedInput) { + this.modifiedInput = modifiedInput; + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlPermissionResponse.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlPermissionResponse.java new file mode 100644 index 000000000..771f0d581 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlPermissionResponse.java @@ -0,0 +1,45 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONType; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; + +/** + * Represents a control permission response from the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "can_use_tool") +public class CLIControlPermissionResponse extends ControlResponsePayload { + public CLIControlPermissionResponse() { + super(); + this.subtype = "can_use_tool"; + } + + /** + * The behavior for the permission request. + */ + @JSONField(unwrapped = true) + Behavior behavior; + + /** + * Gets the behavior for the permission request. + * + * @return The behavior for the permission request + */ + public Behavior getBehavior() { + return behavior; + } + + /** + * Sets the behavior for the permission request. + * + * @param behavior The behavior for the permission request + * @return This instance for method chaining + */ + public CLIControlPermissionResponse setBehavior(Behavior behavior) { + this.behavior = behavior; + return this; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetModelRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetModelRequest.java new file mode 100644 index 000000000..e12b704a3 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetModelRequest.java @@ -0,0 +1,40 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONType; + +/** + * Represents a control request to set the model in the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "set_model") +public class CLIControlSetModelRequest extends ControlRequestPayload { + public CLIControlSetModelRequest() { + super(); + this.subtype = "set_model"; + } + + /** + * The model to set. + */ + String model; + + /** + * Gets the model to set. + * + * @return The model to set + */ + public String getModel() { + return model; + } + + /** + * Sets the model to set. + * + * @param model The model to set + */ + public void setModel(String model) { + this.model = model; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetModelResponse.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetModelResponse.java new file mode 100644 index 000000000..b59552e0c --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetModelResponse.java @@ -0,0 +1,54 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +/** + * Represents a control response for setting the model in the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class CLIControlSetModelResponse { + /** + * The subtype of the response ("set_model"). + */ + String subtype = "set_model"; + /** + * The model that was set. + */ + String model; + + /** + * Gets the subtype of the response. + * + * @return The subtype of the response + */ + public String getSubtype() { + return subtype; + } + + /** + * Sets the subtype of the response. + * + * @param subtype The subtype of the response + */ + public void setSubtype(String subtype) { + this.subtype = subtype; + } + + /** + * Gets the model that was set. + * + * @return The model that was set + */ + public String getModel() { + return model; + } + + /** + * Sets the model that was set. + * + * @param model The model that was set + */ + public void setModel(String model) { + this.model = model; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetPermissionModeRequest.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetPermissionModeRequest.java new file mode 100644 index 000000000..3e5ef9dd1 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/CLIControlSetPermissionModeRequest.java @@ -0,0 +1,40 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONType; + +/** + * Represents a control request to set the permission mode in the CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "set_permission_mode") +public class CLIControlSetPermissionModeRequest extends ControlRequestPayload { + public CLIControlSetPermissionModeRequest() { + super(); + setSubtype("set_permission_mode"); + } + + /** + * The permission mode to set. + */ + String mode; + + /** + * Gets the permission mode to set. + * + * @return The permission mode to set + */ + public String getMode() { + return mode; + } + + /** + * Sets the permission mode to set. + * + * @param mode The permission mode to set + */ + public void setMode(String mode) { + this.mode = mode; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/ControlRequestPayload.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/ControlRequestPayload.java new file mode 100644 index 000000000..1390850e7 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/ControlRequestPayload.java @@ -0,0 +1,26 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONType; + +/** + * Represents a payload request in the CLI control message. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "ControlRequestPayload", + seeAlso = {CLIControlInitializeRequest.class, CLIControlInterruptRequest.class, CLIControlPermissionRequest.class, CLIControlSetModelRequest.class, CLIControlSetPermissionModeRequest.class}) +public class ControlRequestPayload { + /** + * The subtype of the request. + */ + protected String subtype; + + public String getSubtype() { + return subtype; + } + + public void setSubtype(String subtype) { + this.subtype = subtype; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/ControlResponsePayload.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/ControlResponsePayload.java new file mode 100644 index 000000000..fe8cdd8ae --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/message/control/payload/ControlResponsePayload.java @@ -0,0 +1,26 @@ +package com.alibaba.qwen.code.cli.protocol.message.control.payload; + +import com.alibaba.fastjson2.annotation.JSONType; + +/** + * Represents a payload request in the CLI control message. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +@JSONType(typeKey = "subtype", typeName = "ControlResponsePayload", + seeAlso = {CLIControlInitializeResponse.class, CLIControlPermissionResponse.class}) +public class ControlResponsePayload { + /** + * The subtype of the request. + */ + protected String subtype; + + public String getSubtype() { + return subtype; + } + + public void setSubtype(String subtype) { + this.subtype = subtype; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts new file mode 100644 index 000000000..e5eeb1212 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts @@ -0,0 +1,594 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +export interface Annotation { + type: string; + value: string; +} + +export interface Usage { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + total_tokens?: number; +} + +export interface ExtendedUsage extends Usage { + server_tool_use?: { + web_search_requests: number; + }; + service_tier?: string; + cache_creation?: { + ephemeral_1h_input_tokens: number; + ephemeral_5m_input_tokens: number; + }; +} + +export interface ModelUsage { + inputTokens: number; + outputTokens: number; + cacheReadInputTokens: number; + cacheCreationInputTokens: number; + webSearchRequests: number; + contextWindow: number; +} + +export interface CLIPermissionDenial { + tool_name: string; + tool_use_id: string; + tool_input: unknown; +} + +export interface TextBlock { + type: 'text'; + text: string; + annotations?: Annotation[]; +} + +export interface ThinkingBlock { + type: 'thinking'; + thinking: string; + signature?: string; + annotations?: Annotation[]; +} + +export interface ToolUseBlock { + type: 'tool_use'; + id: string; + name: string; + input: unknown; + annotations?: Annotation[]; +} + +export interface ToolResultBlock { + type: 'tool_result'; + tool_use_id: string; + content?: string | ContentBlock[]; + is_error?: boolean; + annotations?: Annotation[]; +} + +export type ContentBlock = + | TextBlock + | ThinkingBlock + | ToolUseBlock + | ToolResultBlock; + +export interface APIUserMessage { + role: 'user'; + content: string | ContentBlock[]; +} + +export interface APIAssistantMessage { + id: string; + type: 'message'; + role: 'assistant'; + model: string; + content: ContentBlock[]; + stop_reason?: string | null; + usage: Usage; +} + +export interface SDKUserMessage { + type: 'user'; + uuid?: string; + session_id: string; + message: APIUserMessage; + parent_tool_use_id: string | null; + options?: Record; +} + +export interface SDKAssistantMessage { + type: 'assistant'; + uuid: string; + session_id: string; + message: APIAssistantMessage; + parent_tool_use_id: string | null; +} + +export interface SDKSystemMessage { + type: 'system'; + subtype: string; + uuid: string; + session_id: string; + data?: unknown; + cwd?: string; + tools?: string[]; + mcp_servers?: Array<{ + name: string; + status: string; + }>; + model?: string; + permission_mode?: string; + slash_commands?: string[]; + qwen_code_version?: string; + output_style?: string; + agents?: string[]; + skills?: string[]; + capabilities?: Record; + compact_metadata?: { + trigger: 'manual' | 'auto'; + pre_tokens: number; + }; +} + +export interface SDKResultMessageSuccess { + type: 'result'; + subtype: 'success'; + uuid: string; + session_id: string; + is_error: false; + duration_ms: number; + duration_api_ms: number; + num_turns: number; + result: string; + usage: ExtendedUsage; + modelUsage?: Record; + permission_denials: CLIPermissionDenial[]; + [key: string]: unknown; +} + +export interface SDKResultMessageError { + type: 'result'; + subtype: 'error_max_turns' | 'error_during_execution'; + uuid: string; + session_id: string; + is_error: true; + duration_ms: number; + duration_api_ms: number; + num_turns: number; + usage: ExtendedUsage; + modelUsage?: Record; + permission_denials: CLIPermissionDenial[]; + error?: { + type?: string; + message: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export type SDKResultMessage = SDKResultMessageSuccess | SDKResultMessageError; + +export interface MessageStartStreamEvent { + type: 'message_start'; + message: { + id: string; + role: 'assistant'; + model: string; + }; +} + +export interface ContentBlockStartEvent { + type: 'content_block_start'; + index: number; + content_block: ContentBlock; +} + +export type ContentBlockDelta = + | { + type: 'text_delta'; + text: string; + } + | { + type: 'thinking_delta'; + thinking: string; + } + | { + type: 'input_json_delta'; + partial_json: string; + }; + +export interface ContentBlockDeltaEvent { + type: 'content_block_delta'; + index: number; + delta: ContentBlockDelta; +} + +export interface ContentBlockStopEvent { + type: 'content_block_stop'; + index: number; +} + +export interface MessageStopStreamEvent { + type: 'message_stop'; +} + +export type StreamEvent = + | MessageStartStreamEvent + | ContentBlockStartEvent + | ContentBlockDeltaEvent + | ContentBlockStopEvent + | MessageStopStreamEvent; + +export interface SDKPartialAssistantMessage { + type: 'stream_event'; + uuid: string; + session_id: string; + event: StreamEvent; + parent_tool_use_id: string | null; +} + +export type PermissionMode = 'default' | 'plan' | 'auto-edit' | 'yolo'; + +/** + * TODO: Align with `ToolCallConfirmationDetails` + */ +export interface PermissionSuggestion { + type: 'allow' | 'deny' | 'modify'; + label: string; + description?: string; + modifiedInput?: unknown; +} + +export interface HookRegistration { + event: string; + callback_id: string; +} + +export interface HookCallbackResult { + shouldSkip?: boolean; + shouldInterrupt?: boolean; + suppressOutput?: boolean; + message?: string; +} + +export interface CLIControlInterruptRequest { + subtype: 'interrupt'; +} + +export interface CLIControlPermissionRequest { + subtype: 'can_use_tool'; + tool_name: string; + tool_use_id: string; + input: unknown; + permission_suggestions: PermissionSuggestion[] | null; + blocked_path: string | null; +} + +export enum AuthProviderType { + DYNAMIC_DISCOVERY = 'dynamic_discovery', + GOOGLE_CREDENTIALS = 'google_credentials', + SERVICE_ACCOUNT_IMPERSONATION = 'service_account_impersonation', +} + +export interface MCPServerConfig { + command?: string; + args?: string[]; + env?: Record; + cwd?: string; + url?: string; + httpUrl?: string; + headers?: Record; + tcp?: string; + timeout?: number; + trust?: boolean; + description?: string; + includeTools?: string[]; + excludeTools?: string[]; + extensionName?: string; + oauth?: Record; + authProviderType?: AuthProviderType; + targetAudience?: string; + targetServiceAccount?: string; +} + +/** + * SDK MCP Server configuration + * + * SDK MCP servers run in the SDK process and are connected via in-memory transport. + * Tool calls are routed through the control plane between SDK and CLI. + */ +export interface SDKMcpServerConfig { + /** + * Type identifier for SDK MCP servers + */ + type: 'sdk'; + /** + * Server name for identification and routing + */ + name: string; + /** + * The MCP Server instance created by createSdkMcpServer() + */ + instance: McpServer; +} + +/** + * Wire format for SDK MCP servers sent to the CLI + */ +export type WireSDKMcpServerConfig = Omit; + +export interface CLIControlInitializeRequest { + subtype: 'initialize'; + hooks?: HookRegistration[] | null; + /** + * SDK MCP servers config + * These are MCP servers running in the SDK process, connected via control plane. + * External MCP servers are configured separately in settings, not via initialization. + */ + sdkMcpServers?: Record; + /** + * External MCP servers that should be managed by the CLI. + */ + mcpServers?: Record; + agents?: SubagentConfig[]; +} + +export interface CLIControlSetPermissionModeRequest { + subtype: 'set_permission_mode'; + mode: PermissionMode; +} + +export interface CLIHookCallbackRequest { + subtype: 'hook_callback'; + callback_id: string; + input: unknown; + tool_use_id: string | null; +} + +export interface CLIControlMcpMessageRequest { + subtype: 'mcp_message'; + server_name: string; + message: { + jsonrpc?: string; + method: string; + params?: Record; + id?: string | number | null; + }; +} + +export interface CLIControlSetModelRequest { + subtype: 'set_model'; + model: string; +} + +export interface CLIControlMcpStatusRequest { + subtype: 'mcp_server_status'; +} + +export interface CLIControlSupportedCommandsRequest { + subtype: 'supported_commands'; +} + +export type ControlRequestPayload = + | CLIControlInterruptRequest + | CLIControlPermissionRequest + | CLIControlInitializeRequest + | CLIControlSetPermissionModeRequest + | CLIHookCallbackRequest + | CLIControlMcpMessageRequest + | CLIControlSetModelRequest + | CLIControlMcpStatusRequest + | CLIControlSupportedCommandsRequest; + +export interface CLIControlRequest { + type: 'control_request'; + request_id: string; + request: ControlRequestPayload; +} + +export interface PermissionApproval { + allowed: boolean; + reason?: string; + modifiedInput?: unknown; +} + +export interface ControlResponse { + subtype: 'success'; + request_id: string; + response: unknown; +} + +export interface ControlErrorResponse { + subtype: 'error'; + request_id: string; + error: string | { message: string; [key: string]: unknown }; +} + +export interface CLIControlResponse { + type: 'control_response'; + response: ControlResponse | ControlErrorResponse; +} + +export interface ControlCancelRequest { + type: 'control_cancel_request'; + request_id?: string; +} + +export type ControlMessage = + | CLIControlRequest + | CLIControlResponse + | ControlCancelRequest; + +/** + * Union of all SDK message types + */ +export type SDKMessage = + | SDKUserMessage + | SDKAssistantMessage + | SDKSystemMessage + | SDKResultMessage + | SDKPartialAssistantMessage; + +export function isSDKUserMessage(msg: any): msg is SDKUserMessage { + return ( + msg && typeof msg === 'object' && msg.type === 'user' && 'message' in msg + ); +} + +export function isSDKAssistantMessage(msg: any): msg is SDKAssistantMessage { + return ( + msg && + typeof msg === 'object' && + msg.type === 'assistant' && + 'uuid' in msg && + 'message' in msg && + 'session_id' in msg && + 'parent_tool_use_id' in msg + ); +} + +export function isSDKSystemMessage(msg: any): msg is SDKSystemMessage { + return ( + msg && + typeof msg === 'object' && + msg.type === 'system' && + 'subtype' in msg && + 'uuid' in msg && + 'session_id' in msg + ); +} + +export function isSDKResultMessage(msg: any): msg is SDKResultMessage { + return ( + msg && + typeof msg === 'object' && + msg.type === 'result' && + 'subtype' in msg && + 'duration_ms' in msg && + 'is_error' in msg && + 'uuid' in msg && + 'session_id' in msg + ); +} + +export function isSDKPartialAssistantMessage( + msg: any, +): msg is SDKPartialAssistantMessage { + return ( + msg && + typeof msg === 'object' && + msg.type === 'stream_event' && + 'uuid' in msg && + 'session_id' in msg && + 'event' in msg && + 'parent_tool_use_id' in msg + ); +} + +export function isControlRequest(msg: any): msg is CLIControlRequest { + return ( + msg && + typeof msg === 'object' && + msg.type === 'control_request' && + 'request_id' in msg && + 'request' in msg + ); +} + +export function isControlResponse(msg: any): msg is CLIControlResponse { + return ( + msg && + typeof msg === 'object' && + msg.type === 'control_response' && + 'response' in msg + ); +} + +export function isControlCancel(msg: any): msg is ControlCancelRequest { + return ( + msg && + typeof msg === 'object' && + msg.type === 'control_cancel_request' && + 'request_id' in msg + ); +} + +export function isTextBlock(block: any): block is TextBlock { + return block && typeof block === 'object' && block.type === 'text'; +} + +export function isThinkingBlock(block: any): block is ThinkingBlock { + return block && typeof block === 'object' && block.type === 'thinking'; +} + +export function isToolUseBlock(block: any): block is ToolUseBlock { + return block && typeof block === 'object' && block.type === 'tool_use'; +} + +export function isToolResultBlock(block: any): block is ToolResultBlock { + return block && typeof block === 'object' && block.type === 'tool_result'; +} + +export type SubagentLevel = 'session'; + +export interface ModelConfig { + model?: string; + temp?: number; + top_p?: number; +} + +export interface RunConfig { + max_time_minutes?: number; + max_turns?: number; +} + +export interface SubagentConfig { + name: string; + description: string; + tools?: string[]; + systemPrompt: string; + level: SubagentLevel; + filePath?: string; + modelConfig?: Partial; + runConfig?: Partial; + color?: string; + readonly isBuiltin?: boolean; +} + +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Control Request Types + * + * Centralized enum for all control request subtypes supported by the CLI. + * This enum should be kept in sync with the controllers in: + * - packages/cli/src/services/control/controllers/systemController.ts + * - packages/cli/src/services/control/controllers/permissionController.ts + * - packages/cli/src/services/control/controllers/mcpController.ts + * - packages/cli/src/services/control/controllers/hookController.ts + */ +export enum ControlRequestType { + // SystemController requests + INITIALIZE = 'initialize', + INTERRUPT = 'interrupt', + SET_MODEL = 'set_model', + SUPPORTED_COMMANDS = 'supported_commands', + + // PermissionController requests + CAN_USE_TOOL = 'can_use_tool', + SET_PERMISSION_MODE = 'set_permission_mode', + + // MCPController requests + MCP_MESSAGE = 'mcp_message', + MCP_SERVER_STATUS = 'mcp_server_status', + + // HookController requests + HOOK_CALLBACK = 'hook_callback', +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/Session.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/Session.java new file mode 100644 index 000000000..e72ab6a04 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/Session.java @@ -0,0 +1,302 @@ +package com.alibaba.qwen.code.cli.session; + +import java.util.Optional; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.JSONReader.Feature; +import com.alibaba.fastjson2.TypeReference; +import com.alibaba.qwen.code.cli.protocol.data.Capabilities; +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; +import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKPartialAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInitializeRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInitializeResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInterruptRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlSetModelRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlSetPermissionModeRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload; +import com.alibaba.qwen.code.cli.session.event.consumers.SessionEventConsumers; +import com.alibaba.qwen.code.cli.session.exception.SessionControlException; +import com.alibaba.qwen.code.cli.session.exception.SessionSendPromptException; +import com.alibaba.qwen.code.cli.transport.Transport; +import com.alibaba.qwen.code.cli.transport.TransportOptions; +import com.alibaba.qwen.code.cli.utils.MyConcurrentUtils; +import com.alibaba.qwen.code.cli.utils.Timeout; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages a session with the Qwen Code CLI, handling communication, sending prompts, and processing responses. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class Session { + private static final Logger log = LoggerFactory.getLogger(Session.class); + private final Transport transport; + private CLIControlInitializeResponse lastCliControlInitializeResponse; + private SDKSystemMessage lastSdkSystemMessage; + private final Timeout defaultEventTimeout = Timeout.TIMEOUT_60_SECONDS; + + /** + * Checks if the session is configured for streaming. + * + * @return true if streaming is enabled, false otherwise + */ + public boolean isStreaming() { + return Optional.ofNullable(transport) + .map(Transport::getTransportOptions) + .map(TransportOptions::getIncludePartialMessages) + .orElse(false); + } + + /** + * Constructs a new session with the specified transport. + * + * @param transport The transport layer to use for communication + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the transport is not available + */ + public Session(Transport transport) throws SessionControlException { + if (transport == null || !transport.isAvailable()) { + throw new SessionControlException("Transport is not available"); + } + this.transport = transport; + start(); + } + + /** + * Starts the session by initializing communication with the CLI. + * + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if initialization fails + */ + public void start() throws SessionControlException { + try { + if (!transport.isAvailable()) { + transport.start(); + } + String response = transport.inputWaitForOneLine(CLIControlRequest.create(new CLIControlInitializeRequest()).toString()); + CLIControlResponse cliControlResponse = JSON.parseObject(response, + new TypeReference>() {}); + this.lastCliControlInitializeResponse = cliControlResponse.getResponse().getResponse(); + } catch (Exception e) { + throw new SessionControlException("Failed to initialize the session", e); + } + } + + /** + * Closes the session and releases resources. + * + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if closing fails + */ + public void close() throws SessionControlException { + try { + transport.close(); + } catch (Exception e) { + throw new SessionControlException("Failed to close the session", e); + } + } + + /** + * Interrupts the current operation in the CLI. + * + * @return An optional boolean indicating success of the interrupt operation + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails + */ + public Optional interrupt() throws SessionControlException { + checkAvailable(); + return processControlRequest(new CLIControlRequest().setRequest(new CLIControlInterruptRequest()).toString()); + } + + /** + * Sets the model to be used in the session. + * + * @param modelName The name of the model to use + * @return An optional boolean indicating success of the operation + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails + */ + public Optional setModel(String modelName) throws SessionControlException { + checkAvailable(); + CLIControlSetModelRequest cliControlSetModelRequest = new CLIControlSetModelRequest(); + cliControlSetModelRequest.setModel(modelName); + return processControlRequest(new CLIControlRequest().setRequest(cliControlSetModelRequest).toString()); + } + + /** + * Sets the permission mode for the session. + * + * @param permissionMode The permission mode to use + * @return An optional boolean indicating success of the operation + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails + */ + public Optional setPermissionMode(PermissionMode permissionMode) throws SessionControlException { + checkAvailable(); + CLIControlSetPermissionModeRequest cliControlSetPermissionModeRequest = new CLIControlSetPermissionModeRequest(); + cliControlSetPermissionModeRequest.setMode(permissionMode.getValue()); + return processControlRequest( + new CLIControlRequest().setRequest(cliControlSetPermissionModeRequest).toString()); + } + + private Optional processControlRequest(String request) throws SessionControlException { + try { + if (transport.isReading()) { + transport.inputNoWaitResponse(request); + return Optional.empty(); + } else { + String response = transport.inputWaitForOneLine(request); + CLIControlResponse cliControlResponse = JSON.parseObject(response, new TypeReference>() {}); + return Optional.of("success".equals(cliControlResponse.getResponse().getSubtype())); + } + } catch (Exception e) { + throw new SessionControlException("Failed to set model", e); + } + } + + /** + * Continues the current session. + * + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails + */ + public void continueSession() throws SessionControlException { + resumeSession(getSessionId()); + } + + /** + * Resumes a session with the specified ID. + * + * @param sessionId The ID of the session to resume + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if the operation fails + */ + public void resumeSession(String sessionId) throws SessionControlException { + if (StringUtils.isNotBlank(sessionId)) { + transport.getTransportOptions().setResumeSessionId(sessionId); + } + this.start(); + } + + /** + * Sends a prompt to the CLI and processes the response. + * + * @param prompt The prompt to send to the CLI + * @param sessionEventConsumers Consumers for handling different types of events + * @throws com.alibaba.qwen.code.cli.session.exception.SessionSendPromptException if sending the prompt fails + * @throws com.alibaba.qwen.code.cli.session.exception.SessionControlException if a control operation fails + */ + public void sendPrompt(String prompt, SessionEventConsumers sessionEventConsumers) throws SessionSendPromptException, SessionControlException { + checkAvailable(); + try { + transport.inputWaitForMultiLine(new SDKUserMessage().setContent(prompt).toString(), (line) -> { + JSONObject jsonObject = JSON.parseObject(line); + String messageType = jsonObject.getString("type"); + if ("system".equals(messageType)) { + lastSdkSystemMessage = jsonObject.to(SDKSystemMessage.class); + MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onSystemMessage(this, lastSdkSystemMessage), + Optional.ofNullable(sessionEventConsumers.onSystemMessageTimeout(this, lastSdkSystemMessage)) + .orElse(defaultEventTimeout)); + return false; + } else if ("assistant".equals(messageType)) { + SDKAssistantMessage assistantMessage = jsonObject.to(SDKAssistantMessage.class); + MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onAssistantMessage(this, assistantMessage), + Optional.ofNullable(sessionEventConsumers.onAssistantMessageTimeout(this, assistantMessage)).orElse(defaultEventTimeout)); + return false; + } else if ("stream_event".equals(messageType)) { + SDKPartialAssistantMessage sdkPartialAssistantMessage = jsonObject.to(SDKPartialAssistantMessage.class); + MyConcurrentUtils.runAndWait( + () -> sessionEventConsumers.onPartialAssistantMessage(this, sdkPartialAssistantMessage), + Optional.ofNullable(sessionEventConsumers.onPartialAssistantMessageTimeout(this, sdkPartialAssistantMessage)) + .orElse(defaultEventTimeout)); + return false; + } else if ("user".equals(messageType)) { + SDKUserMessage sdkUserMessage = jsonObject.to(SDKUserMessage.class, Feature.FieldBased); + MyConcurrentUtils.runAndWait( + () -> sessionEventConsumers.onUserMessage(this, sdkUserMessage), + Optional.ofNullable(sessionEventConsumers.onUserMessageTimeout(this, sdkUserMessage)).orElse(defaultEventTimeout)); + return false; + } else if ("result".equals(messageType)) { + SDKResultMessage sdkResultMessage = jsonObject.to(SDKResultMessage.class); + MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onResultMessage(this, sdkResultMessage), + Optional.ofNullable(sessionEventConsumers.onResultMessageTimeout(this, sdkResultMessage)).orElse(defaultEventTimeout)); + return true; + } else if ("control_response".equals(messageType)) { + CLIControlResponse controlResponse = jsonObject.to( + new TypeReference>() {}); + MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onControlResponse(this, controlResponse), + Optional.ofNullable(sessionEventConsumers.onControlResponseTimeout(this, controlResponse)).orElse(defaultEventTimeout)); + if (!"error".equals(jsonObject.getString("subtype"))) { + return false; + } else { + log.info("control_response error: {}", jsonObject.toJSONString()); + return "error".equals(jsonObject.getString("subtype")); + } + } else if ("control_request".equals(messageType)) { + CLIControlResponse controlResponse; + try { + CLIControlRequest controlRequest = jsonObject.to( + new TypeReference>() {}); + controlResponse = MyConcurrentUtils.runAndWait( + () -> sessionEventConsumers.onControlRequest(this, controlRequest), + Optional.ofNullable(sessionEventConsumers.onControlRequestTimeout(this, controlRequest)).orElse(defaultEventTimeout)); + } catch (Exception e) { + log.error("Failed to process control request", e); + controlResponse = new CLIControlResponse<>(); + } + try { + transport.inputNoWaitResponse(Optional.ofNullable(controlResponse).map(CLIControlResponse::toString) + .orElse(new CLIControlResponse().toString())); + } catch (Exception e) { + throw new RuntimeException("Failed to send control response", e); + } + return false; + } else { + log.warn("unknown message type: {}", messageType); + MyConcurrentUtils.runAndWait(() -> sessionEventConsumers.onOtherMessage(this, line), + Optional.ofNullable(sessionEventConsumers.onOtherMessageTimeout(this, line)).orElse(defaultEventTimeout)); + return false; + } + }); + } catch (Exception e) { + throw new SessionSendPromptException("Failed to send prompt", e); + } + } + + /** + * Gets the current session ID. + * + * @return The session ID, or null if not available + */ + public String getSessionId() { + return Optional.ofNullable(lastSdkSystemMessage).map(SDKSystemMessage::getSessionId).orElse(null); + } + + /** + * Checks if the session is available for operations. + * + * @return true if the session is available, false otherwise + */ + public boolean isAvailable() { + return transport.isAvailable(); + } + + /** + * Gets the capabilities of the CLI. + * + * @return A Capabilities object representing the CLI's capabilities + */ + public Capabilities getCapabilities() { + return Optional.ofNullable(lastCliControlInitializeResponse).map(CLIControlInitializeResponse::getCapabilities).orElse(new Capabilities()); + } + + private void checkAvailable() throws SessionControlException { + if (!isAvailable()) { + throw new SessionControlException("Session is not available"); + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/AssistantContentConsumers.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/AssistantContentConsumers.java new file mode 100644 index 000000000..233bf7353 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/AssistantContentConsumers.java @@ -0,0 +1,159 @@ +package com.alibaba.qwen.code.cli.session.event.consumers; + +import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior.Operation; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlPermissionRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload; +import com.alibaba.qwen.code.cli.session.Session; +import com.alibaba.qwen.code.cli.utils.Timeout; + +/** + * Interface for handling different types of assistant content during a session. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public interface AssistantContentConsumers { + /** + * Handles text content from the assistant. + * + * @param session The session + * @param textAssistantContent The text content from the assistant + */ + void onText(Session session, TextAssistantContent textAssistantContent); + + /** + * Handles thinking content from the assistant. + * + * @param session The session + * @param thingkingAssistantContent The thinking content from the assistant + */ + void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent); + + /** + * Handles tool use content from the assistant. + * + * @param session The session + * @param toolUseAssistantContent The tool use content from the assistant + */ + void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent); + + /** + * Handles tool result content from the assistant. + * + * @param session The session + * @param toolResultAssistantContent The tool result content from the assistant + */ + void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent); + + /** + * Handles other types of assistant content. + * + * @param session The session + * @param other The other content from the assistant + */ + void onOtherContent(Session session, AssistantContent other); + + /** + * Handles permission requests. + * + * @param session The session + * @param permissionRequest The permission request + * @return The behavior for the permission request + */ + Behavior onPermissionRequest(Session session, CLIControlPermissionRequest permissionRequest); + + /** + * Handles permission requests. + * + * @param session The session + * @param requestPayload The control request payload + * @return The response payload for the control request + */ + ControlResponsePayload onOtherControlRequest(Session session, ControlRequestPayload requestPayload); + + /** + * Handles usage information from the assistant. + * + * @param session The session + * @param AssistantUsage The usage information from the assistant + */ + void onUsage(Session session, AssistantUsage AssistantUsage); + + /** + * Sets the default permission operation. + * + * @param defaultPermissionOperation The default permission operation + * @return This instance for method chaining + */ + AssistantContentSimpleConsumers setDefaultPermissionOperation(Operation defaultPermissionOperation); + + /** + * Gets timeout for permission request handling. + * + * @param session The session + * @return The timeout for permission request handling + */ + Timeout onPermissionRequestTimeout(Session session, CLIControlPermissionRequest permissionRequest); + + /** + * Gets timeout for other control request handling. + * + * @param session The session + * @param requestPayload The control request payload + * @return The timeout for other control request handling + */ + Timeout onOtherControlRequestTimeout(Session session, ControlRequestPayload requestPayload); + + /** + * Gets timeout for text handling. + * + * @param session The session + * @param textAssistantContent The text content from the assistant + * @return The timeout for text handling + */ + Timeout onTextTimeout(Session session, TextAssistantContent textAssistantContent); + + /** + * Gets timeout for thinking handling. + * + * @param session The session + * @param thingkingAssistantContent The thinking content from the assistant + * @return The timeout for thinking handling + */ + Timeout onThinkingTimeout(Session session, ThingkingAssistantContent thingkingAssistantContent); + + /** + * Gets timeout for tool use handling. + * + * @param session The session + * @param toolUseAssistantContent The tool use content from the assistant + * @return The timeout for tool use handling + */ + Timeout onToolUseTimeout(Session session, ToolUseAssistantContent toolUseAssistantContent); + + /** + * Gets timeout for tool result handling. + * + * @param session The session + * @param toolResultAssistantContent The tool result content from the assistant + * @return The timeout for tool result handling + */ + Timeout onToolResultTimeout(Session session, ToolResultAssistantContent toolResultAssistantContent); + + /** + * Gets timeout for other content handling. + * + * @param session The session + * @param other The other content from the assistant + * @return The timeout for other content handling + */ + Timeout onOtherContentTimeout(Session session, AssistantContent other); +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/AssistantContentSimpleConsumers.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/AssistantContentSimpleConsumers.java new file mode 100644 index 000000000..b5a158fed --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/AssistantContentSimpleConsumers.java @@ -0,0 +1,193 @@ +package com.alibaba.qwen.code.cli.session.event.consumers; + +import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Allow; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior.Operation; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Deny; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlPermissionRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload; +import com.alibaba.qwen.code.cli.session.Session; +import com.alibaba.qwen.code.cli.utils.Timeout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple implementation of AssistantContentConsumers that provides empty implementations for all methods. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class AssistantContentSimpleConsumers implements AssistantContentConsumers { + /** + * {@inheritDoc} + */ + @Override + public void onText(Session session, TextAssistantContent textAssistantContent) { + log.debug("Received textAssistantContent {}", textAssistantContent.getText()); + } + + /** + * {@inheritDoc} + */ + @Override + public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) { + log.debug("Received thingkingAssistantContent {}", thingkingAssistantContent.getThinking()); + } + + /** + * {@inheritDoc} + */ + @Override + public void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent) { + log.debug("Received toolUseAssistantContent {}", toolUseAssistantContent.getInput()); + } + + /** + * {@inheritDoc} + */ + @Override + public void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent) { + if (log.isDebugEnabled()) { + log.debug("Received toolResultAssistantContent {}", toolResultAssistantContent); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onOtherContent(Session session, AssistantContent other) { + if (log.isDebugEnabled()) { + log.debug("Received other content {}", other); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Behavior onPermissionRequest(Session session, CLIControlPermissionRequest permissionRequest) { + if (Operation.deny.equals(this.defaultPermissionOperation)) { + log.info("use defaultPermissionOperation Permission denied."); + return new Deny().setMessage("Permission denied."); + } else { + log.info("use defaultPermissionOperation Permission allowed."); + return new Allow().setUpdatedInput(permissionRequest.getInput()); + } + } + + @Override + public ControlResponsePayload onOtherControlRequest(Session session, ControlRequestPayload requestPayload) { + throw new RuntimeException("need override onOtherControlRequest"); + } + + /** + * {@inheritDoc} + */ + @Override + public void onUsage(Session session, AssistantUsage AssistantUsage) { + log.info("received usage {} of message {}", AssistantUsage.getUsage(), AssistantUsage.getMessageId()); + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onPermissionRequestTimeout(Session session, CLIControlPermissionRequest permissionRequest) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onOtherControlRequestTimeout(Session session, ControlRequestPayload requestPayload) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onTextTimeout(Session session, TextAssistantContent textAssistantContent) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onThinkingTimeout(Session session, ThingkingAssistantContent thingkingAssistantContent) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onToolUseTimeout(Session session, ToolUseAssistantContent toolUseAssistantContent) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onToolResultTimeout(Session session, ToolResultAssistantContent toolResultAssistantContent) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onOtherContentTimeout(Session session, AssistantContent other) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public AssistantContentSimpleConsumers setDefaultPermissionOperation(Operation defaultPermissionOperation) { + this.defaultPermissionOperation = defaultPermissionOperation; + return this; + } + + /** + * Constructor. + * + * @param defaultPermissionOperation The default permission operation. + * @param defaultEventTimeout The default event timeout. + */ + public AssistantContentSimpleConsumers(Operation defaultPermissionOperation, Timeout defaultEventTimeout) { + this.defaultPermissionOperation = defaultPermissionOperation; + this.defaultEventTimeout = defaultEventTimeout; + } + + /** + * Constructor. + */ + public AssistantContentSimpleConsumers() { + } + + /** + * The default permission operation. + */ + private Operation defaultPermissionOperation = Operation.deny; + + /** + * The default event timeout. + */ + protected Timeout defaultEventTimeout = Timeout.TIMEOUT_60_SECONDS; + + private static final Logger log = LoggerFactory.getLogger(AssistantContentSimpleConsumers.class); +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/SessionEventConsumers.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/SessionEventConsumers.java new file mode 100644 index 000000000..ba37ca641 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/SessionEventConsumers.java @@ -0,0 +1,158 @@ +package com.alibaba.qwen.code.cli.session.event.consumers; + +import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKPartialAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload; +import com.alibaba.qwen.code.cli.session.Session; +import com.alibaba.qwen.code.cli.utils.Timeout; + +/** + * Interface for handling different types of events during a session. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public interface SessionEventConsumers { + /** + * Handles system messages. + * + * @param session The session + * @param systemMessage The system message + */ + void onSystemMessage(Session session, SDKSystemMessage systemMessage); + + /** + * Handles result messages. + * + * @param session The session + * @param resultMessage The result message + */ + void onResultMessage(Session session, SDKResultMessage resultMessage); + + /** + * Handles assistant messages. + * + * @param session The session + * @param assistantMessage The assistant message + */ + void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage); + + /** + * Handles partial assistant messages. + * + * @param session The session + * @param partialAssistantMessage The partial assistant message + */ + void onPartialAssistantMessage(Session session, SDKPartialAssistantMessage partialAssistantMessage); + + /** + * Handles user messages. + * + * @param session The session + * @param userMessage The user message + */ + void onUserMessage(Session session, SDKUserMessage userMessage); + + /** + * Handles other types of messages. + * + * @param session The session + * @param message The message + */ + void onOtherMessage(Session session, String message); + + /** + * Handles control responses. + * + * @param session The session + * @param cliControlResponse The control response + */ + void onControlResponse(Session session, CLIControlResponse cliControlResponse); + + /** + * Handles control requests. + * + * @param session The session + * @param cliControlRequest The control request + * @return The control response + */ + CLIControlResponse onControlRequest(Session session, CLIControlRequest cliControlRequest); + + /** + * Gets timeout for system message handling. + * + * @param session The session + * @param systemMessage The system message + * @return The timeout for system message handling + */ + Timeout onSystemMessageTimeout(Session session, SDKSystemMessage systemMessage); + + /** + * Gets timeout for result message handling. + * + * @param session The session + * @param resultMessage The result message + * @return The timeout for result message handling + */ + Timeout onResultMessageTimeout(Session session, SDKResultMessage resultMessage); + + /** + * Gets timeout for assistant message handling. + * + * @param session The session + * @param assistantMessage The assistant message + * @return The timeout for assistant message handling + */ + Timeout onAssistantMessageTimeout(Session session, SDKAssistantMessage assistantMessage); + + /** + * Gets timeout for partial assistant message handling. + * + * @param session The session + * @param partialAssistantMessage The partial assistant message + * @return The timeout for partial assistant message handling + */ + Timeout onPartialAssistantMessageTimeout(Session session, SDKPartialAssistantMessage partialAssistantMessage); + + /** + * Gets timeout for user message handling. + * + * @param session The session + * @param userMessage The user message + * @return The timeout for user message handling + */ + Timeout onUserMessageTimeout(Session session, SDKUserMessage userMessage); + + /** + * Gets timeout for other message handling. + * + * @param session The session + * @param message The message + * @return The timeout for other message handling + */ + Timeout onOtherMessageTimeout(Session session, String message); + + /** + * Gets timeout for control response handling. + * + * @param session The session + * @param cliControlResponse The control response + * @return The timeout for control response handling + */ + Timeout onControlResponseTimeout(Session session, CLIControlResponse cliControlResponse); + + /** + * Gets timeout for control request handling. + * + * @param session The session + * @param cliControlRequest The control request + * @return The timeout for control request handling + */ + Timeout onControlRequestTimeout(Session session, CLIControlRequest cliControlRequest); +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/SessionEventSimpleConsumers.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/SessionEventSimpleConsumers.java new file mode 100644 index 000000000..e3d9e47cc --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/event/consumers/SessionEventSimpleConsumers.java @@ -0,0 +1,339 @@ +package com.alibaba.qwen.code.cli.session.event.consumers; + +import java.util.List; +import java.util.Optional; + +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Allow; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior; +import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKPartialAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.block.ContentBlock; +import com.alibaba.qwen.code.cli.protocol.message.assistant.event.ContentBlockDeltaEvent; +import com.alibaba.qwen.code.cli.protocol.message.assistant.event.StreamEvent; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlPermissionRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlPermissionResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload; +import com.alibaba.qwen.code.cli.session.Session; +import com.alibaba.qwen.code.cli.utils.MyConcurrentUtils; +import com.alibaba.qwen.code.cli.utils.Timeout; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple implementation of SessionEventConsumers that provides basic implementations for all methods. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class SessionEventSimpleConsumers implements SessionEventConsumers { + /** + * {@inheritDoc} + */ + @Override + public void onSystemMessage(Session session, SDKSystemMessage systemMessage) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onResultMessage(Session session, SDKResultMessage resultMessage) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) { + List> contentBlocks = assistantMessage.getMessage().getContent(); + if (assistantContentConsumers == null || contentBlocks == null || contentBlocks.isEmpty()) { + return; + } + assistantContentConsumers.onUsage(session, + new AssistantUsage(assistantMessage.getMessage().getId(), assistantMessage.getMessage().getUsage())); + + if (!session.isStreaming()) { + contentBlocks.forEach(contentBlock -> consumeAssistantContent(session, contentBlock)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onPartialAssistantMessage(Session session, SDKPartialAssistantMessage partialAssistantMessage) { + StreamEvent event = partialAssistantMessage.getEvent(); + if (!(event instanceof ContentBlockDeltaEvent)) { + log.debug("received partialAssistantMessage and is not instance of ContentBlockDeltaEvent, will ignore process. the message is {}", + partialAssistantMessage); + return; + } + ContentBlockDeltaEvent contentBlockDeltaEvent = (ContentBlockDeltaEvent) event; + contentBlockDeltaEvent.getDelta().setMessageId(partialAssistantMessage.getMessageId()); + consumeAssistantContent(session, contentBlockDeltaEvent.getDelta()); + } + + /** + *

consumeAssistantContent.

+ * + * @param session a {@link com.alibaba.qwen.code.cli.session.Session} object. + * @param assistantContent a {@link com.alibaba.qwen.code.cli.protocol.data.AssistantContent} object. + */ + protected void consumeAssistantContent(Session session, AssistantContent assistantContent) { + if (assistantContent instanceof TextAssistantContent) { + MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onText(session, (TextAssistantContent) assistantContent), + Optional.ofNullable(assistantContentConsumers.onTextTimeout(session, (TextAssistantContent) assistantContent)) + .orElse(defaultEventTimeout)); + } else if (assistantContent instanceof ThingkingAssistantContent) { + MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onThinking(session, (ThingkingAssistantContent) assistantContent), + Optional.ofNullable(assistantContentConsumers.onThinkingTimeout(session, (ThingkingAssistantContent) assistantContent)) + .orElse(defaultEventTimeout)); + } else if (assistantContent instanceof ToolUseAssistantContent) { + MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onToolUse(session, (ToolUseAssistantContent) assistantContent), + Optional.ofNullable(assistantContentConsumers.onToolUseTimeout(session, (ToolUseAssistantContent) assistantContent)) + .orElse(defaultEventTimeout)); + } else if (assistantContent instanceof ToolResultAssistantContent) { + MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onToolResult(session, (ToolResultAssistantContent) assistantContent), + Optional.ofNullable(assistantContentConsumers.onToolResultTimeout(session, (ToolResultAssistantContent) assistantContent)) + .orElse(defaultEventTimeout)); + } else { + MyConcurrentUtils.runAndWait(() -> assistantContentConsumers.onOtherContent(session, assistantContent), + Optional.ofNullable(assistantContentConsumers.onOtherContentTimeout(session, assistantContent)).orElse(defaultEventTimeout)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onUserMessage(Session session, SDKUserMessage userMessage) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onOtherMessage(Session session, String message) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onControlResponse(Session session, CLIControlResponse cliControlResponse) { + } + + /** + * {@inheritDoc} + */ + @Override + public CLIControlResponse onControlRequest(Session session, CLIControlRequest cliControlRequest) { + if (assistantContentConsumers == null) { + throw new RuntimeException("please set assistantContentConsumers or override onControlRequest of "); + } + ControlRequestPayload payload = cliControlRequest.getRequest(); + if (payload instanceof CLIControlPermissionRequest) { + CLIControlPermissionRequest permissionRequest = (CLIControlPermissionRequest) payload; + return supplyPermissionControlResponse(session, permissionRequest, cliControlRequest.getRequestId()); + } else { + ControlRequestPayload request = cliControlRequest.getRequest(); + return supplyOtherControlResponse(session, request, cliControlRequest.getRequestId()); + } + } + + private CLIControlResponse supplyPermissionControlResponse(Session session, + CLIControlPermissionRequest permissionRequest, String requestId) { + Behavior behavior; + try { + behavior = Optional.ofNullable( + MyConcurrentUtils.runAndWait(() -> this.assistantContentConsumers.onPermissionRequest(session, permissionRequest), + Optional.ofNullable(assistantContentConsumers.onPermissionRequestTimeout(session, permissionRequest)) + .orElse(defaultEventTimeout))) + .map(b -> { + if (b instanceof Allow) { + Allow allow = (Allow) b; + if (allow.getUpdatedInput() == null) { + allow.setUpdatedInput(permissionRequest.getInput()); + } + } + return b; + }) + .orElse(Behavior.defaultBehavior()); + } catch (Exception e) { + log.error("Failed to process permission response", e); + behavior = Behavior.defaultBehavior(); + } + + CLIControlResponse permissionResponse = new CLIControlResponse<>(); + permissionResponse.createResponse().setResponse(new CLIControlPermissionResponse().setBehavior(behavior)).setRequestId(requestId); + return permissionResponse; + } + + private CLIControlResponse supplyOtherControlResponse(Session session, ControlRequestPayload requestPayload, + String requestId) { + ControlResponsePayload controlResponsePayload; + try { + controlResponsePayload = Optional.ofNullable( + MyConcurrentUtils.runAndWait(() -> this.assistantContentConsumers.onOtherControlRequest(session, requestPayload), + ObjectUtils.getIfNull(assistantContentConsumers.onOtherControlRequestTimeout(session, requestPayload), + defaultEventTimeout))) + .orElse(new ControlResponsePayload()); + } catch (Exception e) { + log.error("Failed to process permission response", e); + controlResponsePayload = new ControlResponsePayload(); + } + + CLIControlResponse cliControlResponse = new CLIControlResponse<>(); + cliControlResponse.createResponse().setResponse(controlResponsePayload).setRequestId(requestId); + return cliControlResponse; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onSystemMessageTimeout(Session session, SDKSystemMessage systemMessage) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onResultMessageTimeout(Session session, SDKResultMessage resultMessage) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onAssistantMessageTimeout(Session session, SDKAssistantMessage assistantMessage) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onPartialAssistantMessageTimeout(Session session, SDKPartialAssistantMessage partialAssistantMessage) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onUserMessageTimeout(Session session, SDKUserMessage userMessage) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onOtherMessageTimeout(Session session, String message) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onControlResponseTimeout(Session session, CLIControlResponse cliControlResponse) { + return defaultEventTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public Timeout onControlRequestTimeout(Session session, CLIControlRequest cliControlRequest) { + return defaultEventTimeout; + } + + /** + * Gets the default event timeout. + * + * @return The default event timeout + */ + protected Timeout getDefaultEventTimeout() { + return defaultEventTimeout; + } + + /** + * Sets the default event timeout. + * + * @param defaultEventTimeout The default event timeout + * @return This instance for method chaining + */ + public SessionEventSimpleConsumers setDefaultEventTimeout(Timeout defaultEventTimeout) { + this.defaultEventTimeout = defaultEventTimeout; + return this; + } + + /** + * Creates a new SessionEventSimpleConsumers instance with default values. + */ + public SessionEventSimpleConsumers() { + } + + /** + * Creates a new SessionEventSimpleConsumers instance with the specified parameters. + * + * @param defaultEventTimeout The default event timeout + * @param assistantContentConsumers The assistant content consumers + */ + public SessionEventSimpleConsumers(Timeout defaultEventTimeout, AssistantContentConsumers assistantContentConsumers) { + Validate.notNull(defaultEventTimeout, "defaultEventTimeout can't be null"); + Validate.notNull(assistantContentConsumers, "assistantContentConsumers can't be null"); + this.defaultEventTimeout = defaultEventTimeout; + this.assistantContentConsumers = assistantContentConsumers; + } + + /** + * The default event timeout. + */ + protected Timeout defaultEventTimeout = Timeout.TIMEOUT_180_SECONDS; + /** + * The assistant content consumers. + */ + protected AssistantContentConsumers assistantContentConsumers = new AssistantContentSimpleConsumers(); + private static final Logger log = LoggerFactory.getLogger(SessionEventSimpleConsumers.class); + + /** + * Sets the assistant content consumers. + * + * @param assistantContentConsumers The assistant content consumers + * @return This instance for method chaining + */ + public SessionEventSimpleConsumers setAssistantContentConsumer(AssistantContentConsumers assistantContentConsumers) { + Validate.notNull(assistantContentConsumers, "assistantContentConsumers can't be null"); + this.assistantContentConsumers = assistantContentConsumers; + return this; + } + + /** + * Gets the assistant content consumers. + * + * @return The assistant content consumers + */ + public AssistantContentConsumers getAssistantContentConsumers() { + return assistantContentConsumers; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionControlException.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionControlException.java new file mode 100644 index 000000000..ad629e895 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionControlException.java @@ -0,0 +1,55 @@ +package com.alibaba.qwen.code.cli.session.exception; + +/** + * Exception thrown when a session control operation fails. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class SessionControlException extends Exception { + /** + * Creates a new exception. + */ + public SessionControlException() { + } + + /** + * Creates a new exception with a message. + * + * @param message The exception message + */ + public SessionControlException(String message) { + super(message); + } + + /** + * Creates a new exception with a message and cause. + * + * @param message The exception message + * @param cause The exception cause + */ + public SessionControlException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new exception with a cause. + * + * @param cause The exception cause + */ + public SessionControlException(Throwable cause) { + super(cause); + } + + /** + * Creates a new exception with all parameters. + * + * @param message The exception message + * @param cause The exception cause + * @param enableSuppression Whether suppression is enabled + * @param writableStackTrace Whether the stack trace is writable + */ + public SessionControlException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionSendPromptException.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionSendPromptException.java new file mode 100644 index 000000000..6f2c87f0c --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/session/exception/SessionSendPromptException.java @@ -0,0 +1,55 @@ +package com.alibaba.qwen.code.cli.session.exception; + +/** + * Exception thrown when sending a prompt in a session fails. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class SessionSendPromptException extends Exception { + /** + * Creates a new exception. + */ + public SessionSendPromptException() { + } + + /** + * Creates a new exception with a message. + * + * @param message The exception message + */ + public SessionSendPromptException(String message) { + super(message); + } + + /** + * Creates a new exception with a message and cause. + * + * @param message The exception message + * @param cause The exception cause + */ + public SessionSendPromptException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new exception with a cause. + * + * @param cause The exception cause + */ + public SessionSendPromptException(Throwable cause) { + super(cause); + } + + /** + * Creates a new exception with all parameters. + * + * @param message The exception message + * @param cause The exception cause + * @param enableSuppression Whether suppression is enabled + * @param writableStackTrace Whether the stack trace is writable + */ + public SessionSendPromptException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/Transport.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/Transport.java new file mode 100644 index 000000000..0171821e1 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/Transport.java @@ -0,0 +1,78 @@ +package com.alibaba.qwen.code.cli.transport; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +/** + * Defines the contract for communication with the Qwen Code CLI. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public interface Transport { + /** + * Gets the transport options used by this transport. + * + * @return The transport options + */ + TransportOptions getTransportOptions(); + + /** + * Checks if the transport is currently reading. + * + * @return true if reading, false otherwise + */ + boolean isReading(); + + /** + * Starts the transport. + * + * @throws java.io.IOException if starting fails + */ + void start() throws IOException; + + /** + * Closes the transport and releases resources. + * + * @throws java.io.IOException if closing fails + */ + void close() throws IOException; + + /** + * Checks if the transport is available for communication. + * + * @return true if available, false otherwise + */ + boolean isAvailable(); + + /** + * Sends a message and waits for a single-line response. + * + * @param message The message to send + * @return The response message + * @throws java.io.IOException if an I/O error occurs + * @throws java.util.concurrent.ExecutionException if an execution error occurs + * @throws java.lang.InterruptedException if the operation is interrupted + * @throws java.util.concurrent.TimeoutException if the operation times out + */ + String inputWaitForOneLine(String message) throws IOException, ExecutionException, InterruptedException, TimeoutException; + + /** + * Sends a message and waits for a multi-line response. + * + * @param message The message to send + * @param callBackFunction A function to process each line of the response + * @throws java.io.IOException if an I/O error occurs + */ + void inputWaitForMultiLine(String message, Function callBackFunction) throws IOException; + + /** + * Sends a message without waiting for a response. + * + * @param message The message to send + * @throws java.io.IOException if an I/O error occurs + */ + void inputNoWaitResponse(String message) throws IOException; +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/TransportOptions.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/TransportOptions.java new file mode 100644 index 000000000..5f72e1c0b --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/TransportOptions.java @@ -0,0 +1,410 @@ +package com.alibaba.qwen.code.cli.transport; + +import java.util.List; +import java.util.Map; + +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; +import com.alibaba.qwen.code.cli.utils.Timeout; + +/** + * Configuration options for the transport layer. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class TransportOptions implements Cloneable { + /** + * Path to the Qwen executable. + */ + private String pathToQwenExecutable; + /** + * Current working directory for the CLI process. + */ + private String cwd; + /** + * Model to use for the session. + */ + private String model; + /** + * Permission mode for the session. + */ + private PermissionMode permissionMode; + /** + * Environment variables to pass to the CLI process. + */ + private Map env; + /** + * Maximum number of turns in a session. + */ + private Integer maxSessionTurns; + /** + * List of core tools to enable. + */ + private List coreTools; + /** + * List of tools to exclude. + */ + private List excludeTools; + /** + * List of tools that are allowed. + */ + private List allowedTools; + /** + * Authentication type to use. + */ + private String authType; + /** + * Whether to include partial messages in responses. + */ + private Boolean includePartialMessages; + /** + * Whether to enable skills. + */ + private Boolean skillsEnable; + /** + * Timeout for individual turns. + */ + private Timeout turnTimeout; + /** + * Timeout for messages. + */ + private Timeout messageTimeout; + /** + * Session ID to resume. + */ + private String resumeSessionId; + /** + * Additional options to pass to the CLI. + */ + private List otherOptions; + + /** + * Gets the path to the Qwen executable. + * + * @return The path to the Qwen executable + */ + public String getPathToQwenExecutable() { + return pathToQwenExecutable; + } + + /** + * Sets the path to the Qwen executable. + * + * @param pathToQwenExecutable The path to the Qwen executable + * @return This instance for method chaining + */ + public TransportOptions setPathToQwenExecutable(String pathToQwenExecutable) { + this.pathToQwenExecutable = pathToQwenExecutable; + return this; + } + + /** + * 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 + * @return This instance for method chaining + */ + public TransportOptions setCwd(String cwd) { + this.cwd = cwd; + return this; + } + + /** + * Gets the model to use. + * + * @return The model name + */ + public String getModel() { + return model; + } + + /** + * Sets the model to use. + * + * @param model The model name + * @return This instance for method chaining + */ + public TransportOptions setModel(String model) { + this.model = model; + return this; + } + + /** + * Gets the permission mode. + * + * @return The permission mode + */ + public PermissionMode getPermissionMode() { + return permissionMode; + } + + /** + * Sets the permission mode. + * + * @param permissionMode The permission mode + * @return This instance for method chaining + */ + public TransportOptions setPermissionMode(PermissionMode permissionMode) { + this.permissionMode = permissionMode; + return this; + } + + /** + * Gets the environment variables. + * + * @return A map of environment variables + */ + public Map getEnv() { + return env; + } + + /** + * Sets the environment variables. + * + * @param env A map of environment variables + * @return This instance for method chaining + */ + public TransportOptions setEnv(Map env) { + this.env = env; + return this; + } + + /** + * Gets the maximum number of session turns. + * + * @return The maximum number of session turns + */ + public Integer getMaxSessionTurns() { + return maxSessionTurns; + } + + /** + * Sets the maximum number of session turns. + * + * @param maxSessionTurns The maximum number of session turns + * @return This instance for method chaining + */ + public TransportOptions setMaxSessionTurns(Integer maxSessionTurns) { + this.maxSessionTurns = maxSessionTurns; + return this; + } + + /** + * Gets the list of core tools. + * + * @return The list of core tools + */ + public List getCoreTools() { + return coreTools; + } + + /** + * Sets the list of core tools. + * + * @param coreTools The list of core tools + * @return This instance for method chaining + */ + public TransportOptions setCoreTools(List coreTools) { + this.coreTools = coreTools; + return this; + } + + /** + * Gets the list of excluded tools. + * + * @return The list of excluded tools + */ + public List getExcludeTools() { + return excludeTools; + } + + /** + * Sets the list of excluded tools. + * + * @param excludeTools The list of excluded tools + * @return This instance for method chaining + */ + public TransportOptions setExcludeTools(List excludeTools) { + this.excludeTools = excludeTools; + return this; + } + + /** + * Gets the list of allowed tools. + * + * @return The list of allowed tools + */ + public List getAllowedTools() { + return allowedTools; + } + + /** + * Sets the list of allowed tools. + * + * @param allowedTools The list of allowed tools + * @return This instance for method chaining + */ + public TransportOptions setAllowedTools(List allowedTools) { + this.allowedTools = allowedTools; + return this; + } + + /** + * Gets the authentication type. + * + * @return The authentication type + */ + public String getAuthType() { + return authType; + } + + /** + * Sets the authentication type. + * + * @param authType The authentication type + * @return This instance for method chaining + */ + public TransportOptions setAuthType(String authType) { + this.authType = authType; + return this; + } + + /** + * Gets whether to include partial messages. + * + * @return Whether to include partial messages + */ + public Boolean getIncludePartialMessages() { + return includePartialMessages; + } + + /** + * Sets whether to include partial messages. + * + * @param includePartialMessages Whether to include partial messages + * @return This instance for method chaining + */ + public TransportOptions setIncludePartialMessages(Boolean includePartialMessages) { + this.includePartialMessages = includePartialMessages; + return this; + } + + /** + * Gets whether skills are enabled. + * + * @return Whether skills are enabled + */ + public Boolean getSkillsEnable() { + return skillsEnable; + } + + /** + * Sets whether skills are enabled. + * + * @param skillsEnable Whether skills are enabled + * @return This instance for method chaining + */ + public TransportOptions setSkillsEnable(Boolean skillsEnable) { + this.skillsEnable = skillsEnable; + return this; + } + + /** + * Gets the turn timeout. + * + * @return The turn timeout + */ + public Timeout getTurnTimeout() { + return turnTimeout; + } + + /** + * Sets the turn timeout. + * + * @param turnTimeout The turn timeout + * @return This instance for method chaining + */ + public TransportOptions setTurnTimeout(Timeout turnTimeout) { + this.turnTimeout = turnTimeout; + return this; + } + + /** + * Gets the message timeout. + * + * @return The message timeout + */ + public Timeout getMessageTimeout() { + return messageTimeout; + } + + /** + * Sets the message timeout. + * + * @param messageTimeout The message timeout + * @return This instance for method chaining + */ + public TransportOptions setMessageTimeout(Timeout messageTimeout) { + this.messageTimeout = messageTimeout; + return this; + } + + /** + * Gets the session ID to resume. + * + * @return The session ID to resume + */ + public String getResumeSessionId() { + return resumeSessionId; + } + + /** + * Sets the session ID to resume. + * + * @param resumeSessionId The session ID to resume + * @return This instance for method chaining + */ + public TransportOptions setResumeSessionId(String resumeSessionId) { + this.resumeSessionId = resumeSessionId; + return this; + } + + /** + * Gets additional options. + * + * @return Additional options + */ + public List getOtherOptions() { + return otherOptions; + } + + /** + * Sets additional options. + * + * @param otherOptions Additional options + * @return This instance for method chaining + */ + public TransportOptions setOtherOptions(List otherOptions) { + this.otherOptions = otherOptions; + return this; + } + + /** {@inheritDoc} */ + @Override + public TransportOptions clone() { + try { + return (TransportOptions) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransport.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransport.java new file mode 100644 index 000000000..ee53c21f7 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransport.java @@ -0,0 +1,224 @@ +package com.alibaba.qwen.code.cli.transport.process; + +import com.alibaba.qwen.code.cli.transport.Transport; +import com.alibaba.qwen.code.cli.transport.TransportOptions; +import com.alibaba.qwen.code.cli.utils.MyConcurrentUtils; +import com.alibaba.qwen.code.cli.utils.Timeout; + +import org.apache.commons.lang3.exception.ContextedRuntimeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.ProcessBuilder.Redirect; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Implementation of the Transport interface that communicates with the Qwen CLI via a process. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class ProcessTransport implements Transport { + private static final Logger log = LoggerFactory.getLogger(ProcessTransport.class); + private final TransportOptions transportOptions; + protected Timeout turnTimeout; + protected Timeout messageTimeout; + + protected Process process; + protected BufferedWriter processInput; + protected BufferedReader processOutput; + protected BufferedReader processError; + protected final Consumer errorHandler; + + private final AtomicBoolean reading = new AtomicBoolean(false); + + /** + * Constructs a new ProcessTransport with default options. + * + * @throws java.io.IOException if starting the process fails + */ + public ProcessTransport() throws IOException { + this(new TransportOptions()); + } + + /** + * Constructs a new ProcessTransport with the specified options. + * + * @param transportOptions The transport options to use + * @throws java.io.IOException if starting the process fails + */ + public ProcessTransport(TransportOptions transportOptions) throws IOException { + this(transportOptions, (line) -> log.error("process error: {}", line)); + } + + /** + * Constructs a new ProcessTransport with the specified options and error handler. + * + * @param transportOptions The transport options to use + * @param errorHandler The error handler to use + * @throws java.io.IOException if starting the process fails + */ + public ProcessTransport(TransportOptions transportOptions, Consumer errorHandler) throws IOException { + this.transportOptions = transportOptions; + this.errorHandler = errorHandler; + start(); + } + + /** {@inheritDoc} */ + @Override + public TransportOptions getTransportOptions() { + return transportOptions; + } + + /** {@inheritDoc} */ + @Override + public boolean isReading() { + return reading.get(); + } + + /** {@inheritDoc} */ + @Override + public void start() throws IOException { + TransportOptionsAdapter transportOptionsAdapter = new TransportOptionsAdapter(transportOptions); + this.turnTimeout = transportOptionsAdapter.getHandledTransportOptions().getTurnTimeout(); + this.messageTimeout = transportOptionsAdapter.getHandledTransportOptions().getMessageTimeout(); + + String[] commandArgs = transportOptionsAdapter.buildCommandArgs(); + log.debug("trans to command args: {}", transportOptionsAdapter); + + ProcessBuilder processBuilder = new ProcessBuilder(commandArgs) + .redirectOutput(Redirect.PIPE) + .redirectInput(Redirect.PIPE) + .redirectError(Redirect.PIPE) + .redirectErrorStream(false) + .directory(new File(transportOptionsAdapter.getCwd())); + + process = processBuilder.start(); + processInput = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); + processOutput = new BufferedReader(new InputStreamReader(process.getInputStream())); + processError = new BufferedReader(new InputStreamReader(process.getErrorStream())); + startErrorReading(); + } + + /** {@inheritDoc} */ + @Override + public void close() throws IOException { + if (processInput != null) { + processInput.close(); + } + if (processOutput != null) { + processOutput.close(); + } + if (processError != null) { + processError.close(); + } + if (process != null) { + process.destroy(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isAvailable() { + return process != null && process.isAlive(); + } + + /** {@inheritDoc} */ + @Override + public String inputWaitForOneLine(String message) throws IOException, ExecutionException, InterruptedException, TimeoutException { + return inputWaitForOneLine(message, turnTimeout); + } + + private String inputWaitForOneLine(String message, Timeout timeOut) + throws IOException, TimeoutException, InterruptedException, ExecutionException { + inputNoWaitResponse(message); + try { + reading.set(true); + String line = MyConcurrentUtils.runAndWait(() -> { + try { + return processOutput.readLine(); + } catch (IOException e) { + throw new ContextedRuntimeException("read line error", e) + .addContextValue("message", message); + } + }, timeOut); + log.info("inputWaitForOneLine result: {}", line); + return line; + } finally { + reading.set(false); + } + } + + /** {@inheritDoc} */ + @Override + public void inputWaitForMultiLine(String message, Function callBackFunction) throws IOException { + inputWaitForMultiLine(message, callBackFunction, turnTimeout); + } + + private void inputWaitForMultiLine(String message, Function callBackFunction, Timeout timeOut) throws IOException { + log.debug("input message for multiLine: {}", message); + inputNoWaitResponse(message); + MyConcurrentUtils.runAndWait(() -> iterateOutput(callBackFunction), timeOut); + } + + /** {@inheritDoc} */ + @Override + public void inputNoWaitResponse(String message) throws IOException { + log.debug("input message to process: {}", message); + processInput.write(message); + processInput.newLine(); + processInput.flush(); + } + + private void startErrorReading() { + MyConcurrentUtils.asyncRun(() -> { + try { + for (;;) { + final String line = processError.readLine(); + if (line == null) { + break; + } + if (errorHandler != null) { + try { + MyConcurrentUtils.runAndWait(() -> errorHandler.accept(line), messageTimeout); + } catch (Exception e) { + log.warn("error handler error", e); + } + } + } + } catch (IOException e) { + log.warn("Failed read error {}, caused by {}", e.getMessage(), e.getCause(), e); + } + }, (e, t) -> log.warn("read error {}", t.getMessage(), t)); + } + + private void iterateOutput(Function callBackFunction) { + try { + reading.set(true); + MyConcurrentUtils.runAndWait(() -> { + try { + for (String line = processOutput.readLine(); line != null; line = processOutput.readLine()) { + log.debug("read a message from process {}", line); + if (callBackFunction.apply(line)) { + break; + } + } + } catch (IOException e) { + throw new RuntimeException("read process output error", e); + } + }, messageTimeout); + } finally { + reading.set(false); + } + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/TransportOptionsAdapter.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/TransportOptionsAdapter.java new file mode 100644 index 000000000..fe8f21691 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/transport/process/TransportOptionsAdapter.java @@ -0,0 +1,157 @@ +package com.alibaba.qwen.code.cli.transport.process; + +import com.alibaba.qwen.code.cli.transport.TransportOptions; +import com.alibaba.qwen.code.cli.utils.Timeout; + +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * Adapter that converts TransportOptions to command-line arguments for the CLI process. + */ +class TransportOptionsAdapter { + /** + * The adapted transport options. + */ + TransportOptions transportOptions; + /** + * Default timeout for turns. + */ + private static final Timeout DEFAULT_TURN_TIMEOUT = new Timeout(1000 * 60 * 30L, TimeUnit.MILLISECONDS); + /** + * Default timeout for messages. + */ + private static final Timeout DEFAULT_MESSAGE_TIMEOUT = new Timeout(1000 * 60 * 3L, TimeUnit.MILLISECONDS); + + /** + * Constructs a new adapter with the specified options. + * + * @param userTransportOptions The user's transport options + */ + TransportOptionsAdapter(TransportOptions userTransportOptions) { + transportOptions = addDefaultTransportOptions(userTransportOptions); + } + + /** + * Gets the processed transport options. + * + * @return The processed transport options + */ + TransportOptions getHandledTransportOptions() { + return transportOptions; + } + + /** + * Gets the current working directory. + * + * @return The current working directory + */ + String getCwd() { + return transportOptions.getCwd(); + } + + /** + * Builds command-line arguments from the transport options. + * + * @return An array of command-line arguments + */ + String[] buildCommandArgs() { + List args = new ArrayList<>( + Arrays.asList(transportOptions.getPathToQwenExecutable(), "--input-format", "stream-json", "--output-format", + "stream-json", "--channel=SDK")); + + if (StringUtils.isNotBlank(transportOptions.getModel())) { + args.add("--model"); + args.add(transportOptions.getModel()); + } + + if (transportOptions.getPermissionMode() != null) { + args.add("--approval-mode"); + args.add(transportOptions.getPermissionMode().getValue()); + } + + if (transportOptions.getMaxSessionTurns() != null) { + args.add("--max-session-turns"); + args.add(transportOptions.getMaxSessionTurns().toString()); + } + + if (transportOptions.getCoreTools() != null && !transportOptions.getCoreTools().isEmpty()) { + args.add("--core-tools"); + args.add(String.join(",", transportOptions.getCoreTools())); + } + + if (transportOptions.getExcludeTools() != null && !transportOptions.getExcludeTools().isEmpty()) { + args.add("--exclude-tools"); + args.add(String.join(",", transportOptions.getExcludeTools())); + } + + if (transportOptions.getAllowedTools() != null && !transportOptions.getAllowedTools().isEmpty()) { + args.add("--allowed-tools"); + args.add(String.join(",", transportOptions.getAllowedTools())); + } + + if (StringUtils.isNotBlank(transportOptions.getAuthType())) { + args.add("--auth-type"); + args.add(transportOptions.getAuthType()); + } + + if (transportOptions.getIncludePartialMessages() != null && transportOptions.getIncludePartialMessages()) { + args.add("--include-partial-messages"); + } + + if (transportOptions.getSkillsEnable() != null && transportOptions.getSkillsEnable()) { + args.add("--experimental-skills"); + } + + if (StringUtils.isNotBlank(transportOptions.getResumeSessionId())) { + args.add("--resume"); + args.add(transportOptions.getResumeSessionId()); + } + + if (transportOptions.getOtherOptions() != null) { + args.addAll(transportOptions.getOtherOptions()); + } + return args.toArray(new String[] {}); + } + + /** + * Adds default values to the user's transport options. + * + * @param userTransportOptions The user's transport options + * @return The options with defaults added + */ + private TransportOptions addDefaultTransportOptions(TransportOptions userTransportOptions) { + TransportOptions transportOptions = Optional.ofNullable(userTransportOptions) + .map(TransportOptions::clone) + .orElse(new TransportOptions()); + + if (StringUtils.isBlank(transportOptions.getPathToQwenExecutable())) { + transportOptions.setPathToQwenExecutable("qwen"); + } + + if (StringUtils.isBlank(transportOptions.getCwd())) { + transportOptions.setCwd(new File("").getAbsolutePath()); + } + + Map env = new HashMap<>(System.getenv()); + Optional.ofNullable(transportOptions.getEnv()).ifPresent(env::putAll); + transportOptions.setEnv(env); + + if (transportOptions.getTurnTimeout() == null) { + transportOptions.setTurnTimeout(DEFAULT_TURN_TIMEOUT); + } + + if (transportOptions.getMessageTimeout() == null) { + transportOptions.setMessageTimeout(DEFAULT_MESSAGE_TIMEOUT); + } + return transportOptions; + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/MyConcurrentUtils.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/MyConcurrentUtils.java new file mode 100644 index 000000000..34c7585d5 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/MyConcurrentUtils.java @@ -0,0 +1,94 @@ +package com.alibaba.qwen.code.cli.utils; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for concurrent operations. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class MyConcurrentUtils { + private static final Logger log = LoggerFactory.getLogger(MyConcurrentUtils.class); + + /** + * Runs a task and waits for it to complete with a timeout. + * + * @param runnable The task to run + * @param timeOut The timeout for the operation + */ + public static void runAndWait(Runnable runnable, Timeout timeOut) { + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + runnable.run(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, ThreadPoolConfig.getExecutor()); + try { + future.get(timeOut.getValue(), timeOut.getUnit()); + } catch (InterruptedException e) { + log.warn("task interrupted", e); + future.cancel(true); + } catch (TimeoutException e) { + log.warn("Operation timed out", e); + future.cancel(true); + } catch (Exception e) { + future.cancel(true); + log.warn("Operation error", e); + } + } + + /** + * Runs a task that returns a value and waits for it to complete with a timeout. + * + * @param supplier The task to run + * @param timeOut The timeout for the operation + * @param The type of the result + * @return The result of the task + * @throws java.util.concurrent.ExecutionException if an execution error occurs + * @throws java.lang.InterruptedException if the operation is interrupted + * @throws java.util.concurrent.TimeoutException if the operation times out + */ + public static T runAndWait(Supplier supplier, Timeout timeOut) + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, ThreadPoolConfig.getExecutor()); + + try { + return future.get(timeOut.getValue(), timeOut.getUnit()); + } catch (TimeoutException | InterruptedException | ExecutionException e) { + future.cancel(true); + throw e; + } + } + + /** + * Runs a task asynchronously with an error callback. + * + * @param runnable The task to run + * @param errorCallback The error callback + */ + public static void asyncRun(Runnable runnable, BiConsumer errorCallback) { + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + runnable.run(); + } catch (Exception e) { + log.warn("async task error", e); + } + }, ThreadPoolConfig.getExecutor()); + future.whenComplete(errorCallback); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/ThreadPoolConfig.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/ThreadPoolConfig.java new file mode 100644 index 000000000..78e58a92e --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/ThreadPoolConfig.java @@ -0,0 +1,64 @@ +package com.alibaba.qwen.code.cli.utils; + +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +/** + * Configuration for the thread pool used by the SDK. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class ThreadPoolConfig { + private static final ThreadPoolExecutor defaultExecutor = new ThreadPoolExecutor( + 30, 100, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(300), + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "qwen_code_cli-pool-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 + ); + + private static Supplier executorSupplier; + + /** + * Sets the supplier for the executor. + * + * @param executorSupplier The supplier for the executor + */ + public static void setExecutorSupplier(Supplier executorSupplier) { + ThreadPoolConfig.executorSupplier = executorSupplier; + } + + /** + * Gets the default executor. + * + * @return The default executor + */ + public static ThreadPoolExecutor getDefaultExecutor() { + return defaultExecutor; + } + + static ExecutorService getExecutor() { + return Optional.ofNullable(executorSupplier).map(s -> { + try { + return s.get(); + } catch (Exception e) { + return defaultExecutor; + } + }).orElse(defaultExecutor); + } +} diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/Timeout.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/Timeout.java new file mode 100644 index 000000000..089752723 --- /dev/null +++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/utils/Timeout.java @@ -0,0 +1,68 @@ +package com.alibaba.qwen.code.cli.utils; + +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.Validate; + +/** + * Represents a timeout value with a time unit. + * + * @author skyfire + * @version $Id: 0.0.1 + */ +public class Timeout { + /** + * The timeout value. + */ + private final Long value; + /** + * The time unit. + */ + private final TimeUnit unit; + + /** + * Creates a new Timeout instance. + * + * @param value The timeout value + * @param unit The time unit + */ + public Timeout(Long value, TimeUnit unit) { + Validate.notNull(value, "value can not be null"); + Validate.notNull(unit, "unit can not be null"); + this.value = value; + this.unit = unit; + } + + /** + * Gets the timeout value. + * + * @return The timeout value + */ + public Long getValue() { + return value; + } + + /** + * Gets the time unit. + * + * @return The time unit + */ + public TimeUnit getUnit() { + return unit; + } + + /** + * A timeout of 60 seconds. + */ + public static final Timeout TIMEOUT_60_SECONDS = new Timeout(60L, TimeUnit.SECONDS); + + /** + * A timeout of 180 seconds. + */ + public static final Timeout TIMEOUT_180_SECONDS = new Timeout(180L, TimeUnit.SECONDS); + + /** + * A timeout of 30 minutes. + */ + public static final Timeout TIMEOUT_30_MINUTES = new Timeout(60L, TimeUnit.MINUTES); +} diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/QwenCodeCliTest.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/QwenCodeCliTest.java new file mode 100644 index 000000000..ca10173f7 --- /dev/null +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/QwenCodeCliTest.java @@ -0,0 +1,29 @@ +package com.alibaba.qwen.code.cli; + +import java.util.List; + +import com.alibaba.qwen.code.cli.transport.TransportOptions; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.*; + +class QwenCodeCliTest { + + private static final Logger log = LoggerFactory.getLogger(QwenCodeCliTest.class); + @Test + void simpleQuery() { + List result = QwenCodeCli.simpleQuery("hello world"); + log.info("simpleQuery result: {}", result); + assertNotNull(result); + } + + @Test + void simpleQueryWithModel() { + List result = QwenCodeCli.simpleQuery("hello world", new TransportOptions().setModel("qwen-plus")); + log.info("simpleQueryWithModel result: {}", result); + assertNotNull(result); + } +} diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/QuickStartExample.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/QuickStartExample.java new file mode 100644 index 000000000..2a7a20dc6 --- /dev/null +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/QuickStartExample.java @@ -0,0 +1,109 @@ +package com.alibaba.qwen.code.cli.example; + +import com.alibaba.qwen.code.cli.QwenCodeCli; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage; +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; +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.AssistantContentSimpleConsumers; +import com.alibaba.qwen.code.cli.transport.TransportOptions; +import com.alibaba.qwen.code.cli.utils.Timeout; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class QuickStartExample { + private static final Logger logger = LoggerFactory.getLogger(QuickStartExample.class); + + public static void main(String[] args) { + logger.info("runSimpleExample started.{}", StringUtils.repeat("=", 150)); + runSimpleExample(); + + logger.info("runTransportOptionsExample started. {}", StringUtils.repeat("=", 150)); + runTransportOptionsExample(); + + logger.info("runStreamingExample started. {}", StringUtils.repeat("=", 150)); + runStreamingExample(); + + System.exit(0); + } + + /** + * Simple example showing basic query usage + */ + public static void runSimpleExample() { + List result = QwenCodeCli.simpleQuery("hello world"); + result.forEach(logger::info); + } + + /** + * TransportOptions example showing comprehensive transport options configuration + */ + public static void runTransportOptionsExample() { + TransportOptions options = new TransportOptions() + .setModel("qwen3-coder-flash") + .setPermissionMode(PermissionMode.AUTO_EDIT) + .setCwd("./") + .setEnv(new HashMap() {{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 result = QwenCodeCli.simpleQuery("who are you, what are your capabilities?", options); + result.forEach(logger::info); + } + + /** + * Streaming example showing simple query usage + */ + 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."); + } +} diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/SessionExample.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/SessionExample.java new file mode 100644 index 000000000..fdbc58bdf --- /dev/null +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/SessionExample.java @@ -0,0 +1,256 @@ +package com.alibaba.qwen.code.cli.example; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.qwen.code.cli.QwenCodeCli; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent; +import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage; +import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior.Operation; +import com.alibaba.qwen.code.cli.protocol.message.SDKResultMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKPartialAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlResponsePayload; +import com.alibaba.qwen.code.cli.session.Session; +import com.alibaba.qwen.code.cli.session.event.consumers.AssistantContentSimpleConsumers; +import com.alibaba.qwen.code.cli.session.event.consumers.SessionEventSimpleConsumers; +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.block.TextBlock; +import com.alibaba.qwen.code.cli.session.exception.SessionControlException; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +public class SessionExample { + private static final Logger logger = LoggerFactory.getLogger(SessionExample.class); + + public static void main(String[] args) { + Session session = QwenCodeCli.newSession(); + try { + logger.info("runPermissionModeExample started {}", StringUtils.repeat("=", 150)); + runPermissionModeExample(session); + + logger.info("runSetModelExample started {}", StringUtils.repeat("=", 150)); + runSetModelExample(session); + + logger.info("runSetPermissionModeExample started {}", StringUtils.repeat("=", 150)); + runSetPermissionModeExample(session); + + logger.info("runInterruptExample started {}", StringUtils.repeat("=", 150)); + runInterruptExample(session); + + logger.info("runSetModelExample started {}", StringUtils.repeat("=", 150)); + runSetModelExample(session); + + logger.info("runPromptUseLowLevelEventExample started {}", StringUtils.repeat("=", 150)); + runPromptUseLowLevelEventExample(session); + + logger.info("runPromptUseHighLevelEventExample started {}", StringUtils.repeat("=", 150)); + runPromptUseHighLevelEventExample(session); + + System.exit(0); + } finally { + try { + session.close(); + } catch (SessionControlException e) { + logger.error("Error closing session", e); + } + } + } + + /** + * Example showing how to set different permission modes + */ + public static void runPermissionModeExample(Session session) { + try { + logger.info(session.setPermissionMode(PermissionMode.PLAN).map(s -> s ? "Permission mode set to PLAN" : "Permission mode set error") + .orElse("Permission mode set unknown")); + } catch (SessionControlException e) { + logger.error("Error setting permission mode", e); + } + } + + /** + * Example showing how to interrupt a running prompt + */ + public static void runInterruptExample(Session session) { + try { + session.sendPrompt("Analyze this large codebase...", new SessionEventSimpleConsumers() { + @Override + public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) { + String message = assistantMessage.getMessage().getContent().stream() + .findFirst() + .filter(content -> content instanceof TextBlock) + .map(content -> ((TextBlock) content).getText()) + .orElse(""); + logger.info("Received: {}", message); + + // Interrupt the session after receiving the first message + try { + Optional interruptResult = session.interrupt(); + logger.info("{}", interruptResult.map(s -> s ? "Interrupt successful" : "Interrupt error") + .orElse("Interrupt unknown")); + } catch (SessionControlException e) { + logger.error("Interrupt error: {}", e.getMessage(), e); + } + } + }); + } catch (Exception e) { + logger.error("An error occurred while sending the prompt", e); + } + } + + /** + * Example showing how to dynamically change the AI model during a session + */ + public static void runSetModelExample(Session session) { + try { + // Switch to a specific model + Optional modelChangeResult = session.setModel("qwen3-coder-flash"); + logger.info("{}", modelChangeResult.map(s -> s ? "setModel success" : "setModel error") + .orElse("setModel unknown")); + + // Use the model for a prompt + session.sendPrompt("hello world", new SessionEventSimpleConsumers()); + + // Switch to another model + Optional modelChangeResult2 = session.setModel("qwen3-coder-plus"); + logger.info("{}", modelChangeResult2.map(s -> s ? "setModel success" : "setModel error") + .orElse("setModel unknown")); + + // Use the new model for another prompt + session.sendPrompt("list files in the current directory", new SessionEventSimpleConsumers()); + } catch (Exception e) { + logger.error("An error occurred while changing model or sending prompt", e); + } + } + + /** + * Example showing how to dynamically change permission mode during a session + */ + public static void runSetPermissionModeExample(Session session) { + try { + // Switch to a permissive mode + Optional permissionChangeResult = session.setPermissionMode(PermissionMode.YOLO); + logger.info("{}", permissionChangeResult.map(s -> s ? "setPermissionMode success" : "setPermissionMode error") + .orElse("setPermissionMode unknown")); + + // Use the session with the new permission mode + session.sendPrompt("in the dir src/test/temp/, create file empty file test.touch", new SessionEventSimpleConsumers()); + + // Switch to another permission mode + Optional permissionChangeResult2 = session.setPermissionMode(PermissionMode.PLAN); + logger.info("{}", permissionChangeResult2.map(s -> s ? "setPermissionMode success" : "setPermissionMode error") + .orElse("setPermissionMode unknown")); + + // Use the session with the new permission mode + session.sendPrompt("rename test.touch to test_rename.touch", new SessionEventSimpleConsumers()); + } catch (Exception e) { + logger.error("An error occurred while changing permission mode or sending prompt", e); + } + } + + public static void runPromptUseLowLevelEventExample(Session session) { + try { + session.setPermissionMode(PermissionMode.YOLO); + session.sendPrompt("devlop Fibonacci function by python", new SessionEventSimpleConsumers() { + @Override + public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) { + logger.info("Received assistantMessage {}", JSON.toJSONString(assistantMessage)); + } + + @Override + public void onPartialAssistantMessage(Session session, SDKPartialAssistantMessage partialAssistantMessage) { + logger.info("Received partialAssistantMessage {}", JSON.toJSONString(partialAssistantMessage)); + } + + @Override + public void onUserMessage(Session session, SDKUserMessage userMessage) { + logger.info("Received userMessage {}", JSON.toJSONString(userMessage)); + } + + @Override + public void onOtherMessage(Session session, String message) { + logger.info("Received otherMessage {}", message); + } + + @Override + public void onControlResponse(Session session, CLIControlResponse cliControlResponse) { + logger.info("Received controlResponse {}", JSON.toJSONString(cliControlResponse)); + } + + @Override + public CLIControlResponse onControlRequest(Session session, CLIControlRequest cliControlRequest) { + logger.info("Received controlRequest {}", JSON.toJSONString(cliControlRequest)); + return new CLIControlResponse<>(); + } + + @Override + public void onResultMessage(Session session, SDKResultMessage resultMessage) { + logger.info("Received resultMessage {}", JSON.toJSONString(resultMessage)); + } + + @Override + public void onSystemMessage(Session session, SDKSystemMessage systemMessage) { + logger.info("Received systemMessage {}", JSON.toJSONString(systemMessage)); + } + }); + } catch (Exception e) { + logger.error("An error occurred while sending prompt", e); + } + } + + public static void runPromptUseHighLevelEventExample(Session session) { + try { + session.sendPrompt("devlop Fibonacci function by python", new SessionEventSimpleConsumers().setAssistantContentConsumer(new AssistantContentSimpleConsumers(){ + @Override + public void onText(Session session, TextAssistantContent textAssistantContent) { + logger.info("Received textAssistantContent {}", textAssistantContent.getText()); + } + + @Override + public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) { + logger.info("Received thingkingAssistantContent {}", thingkingAssistantContent.getThinking()); + } + + @Override + public void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent) { + logger.info("Received toolUseAssistantContent {}", toolUseAssistantContent.getInput()); + } + + @Override + public void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent) { + logger.info("Received toolResultAssistantContent {}", toolResultAssistantContent.getContent()); + } + + @Override + public void onOtherContent(Session session, AssistantContent other) { + logger.info("Received other {}", other); + } + + @Override + public void onUsage(Session session, AssistantUsage assistantUsage) { + logger.info("Received usage {}", assistantUsage); + } + + @Override + public ControlResponsePayload onOtherControlRequest(Session session, ControlRequestPayload requestPayload) { + logger.info("Received otherControlRequest {}", requestPayload); + return new ControlResponsePayload(); + } + }.setDefaultPermissionOperation(Operation.allow))); + } catch (Exception e) { + logger.error("An error occurred while sending prompt", e); + } + } +} diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/ThreadPoolConfigurationExample.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/ThreadPoolConfigurationExample.java new file mode 100644 index 000000000..0a77a03ad --- /dev/null +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/example/ThreadPoolConfigurationExample.java @@ -0,0 +1,50 @@ +package com.alibaba.qwen.code.cli.example; + +import com.alibaba.qwen.code.cli.QwenCodeCli; +import com.alibaba.qwen.code.cli.session.Session; +import com.alibaba.qwen.code.cli.utils.ThreadPoolConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolConfigurationExample { + private static final Logger logger = LoggerFactory.getLogger(ThreadPoolConfigurationExample.class); + + public static void main(String[] args) { + runModifyDefaultExample(); + runCustomSupplierExample(); + } + + /** + * Example showing how to set a custom thread pool supplier + */ + public static void runCustomSupplierExample() { + // Set a custom thread pool supplier + ThreadPoolConfig.setExecutorSupplier(() -> (ThreadPoolExecutor) Executors.newFixedThreadPool(20)); + logger.info("Custom thread pool supplier set"); + } + + /** + * Example showing how to modify properties of the default thread pool + */ + public static void runModifyDefaultExample() { + // Get the default executor and modify its properties + ThreadPoolExecutor executor = ThreadPoolConfig.getDefaultExecutor(); + + // Modify the core pool size + executor.setCorePoolSize(15); + + // Modify the maximum pool size + executor.setMaximumPoolSize(40); + + // Modify the keep-alive time + executor.setKeepAliveTime(120, TimeUnit.SECONDS); + + logger.info("Default thread pool properties modified"); + + // The SDK will now use the modified executor for all operations + Session session = QwenCodeCli.newSession(); + } +} diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/session/SessionTest.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/session/SessionTest.java new file mode 100644 index 000000000..0353d0065 --- /dev/null +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/session/SessionTest.java @@ -0,0 +1,185 @@ +package com.alibaba.qwen.code.cli.session; + +import java.util.concurrent.TimeUnit; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.qwen.code.cli.QwenCodeCli; +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.PermissionMode; +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.protocol.message.SDKResultMessage; +import com.alibaba.qwen.code.cli.protocol.message.SDKSystemMessage; +import com.alibaba.qwen.code.cli.protocol.message.assistant.SDKAssistantMessage; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.session.event.consumers.AssistantContentSimpleConsumers; +import com.alibaba.qwen.code.cli.session.event.consumers.SessionEventConsumers; +import com.alibaba.qwen.code.cli.session.event.consumers.SessionEventSimpleConsumers; +import com.alibaba.qwen.code.cli.session.exception.SessionControlException; +import com.alibaba.qwen.code.cli.session.exception.SessionSendPromptException; +import com.alibaba.qwen.code.cli.transport.TransportOptions; +import com.alibaba.qwen.code.cli.utils.Timeout; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class SessionTest { + + private static final Logger log = LoggerFactory.getLogger(SessionTest.class); + + @Test + void partialSendPromptSuccessfully() throws SessionControlException, SessionSendPromptException { + Session session = QwenCodeCli.newSession(new TransportOptions().setIncludePartialMessages(true)); + session.sendPrompt("in the dir src/test/temp/, create file empty file test.touch", new SessionEventSimpleConsumers() { + }.setAssistantContentConsumer(new AssistantContentSimpleConsumers() { + @Override + public void onText(Session session, TextAssistantContent textAssistantContent) { + log.info("receive textAssistantContent {}", textAssistantContent); + } + + @Override + public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) { + log.info("receive thingkingAssistantContent {}", thingkingAssistantContent); + } + + @Override + public void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent) { + log.info("receive toolUseAssistantContent {}", toolUseAssistantContent); + } + + @Override + public void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent) { + log.info("receive toolResultAssistantContent {}", toolResultAssistantContent); + } + + public void onOtherContent(Session session, AssistantContent other) { + log.info("receive otherContent {}", other); + } + + @Override + public void onUsage(Session session, AssistantUsage assistantUsage) { + log.info("receive assistantUsage {}", assistantUsage); + } + }.setDefaultPermissionOperation(Operation.allow))); + } + + @Test + void setPermissionModeSuccessfully() throws SessionControlException, SessionSendPromptException { + Session session = QwenCodeCli.newSession(new TransportOptions()); + + log.info(session.setPermissionMode(PermissionMode.YOLO).map(s -> s ? "setPermissionMode 1 success" : "setPermissionMode 1 error") + .orElse("setPermissionMode 1 unknown")); + session.sendPrompt("in the dir src/test/temp/, create file empty file test.touch", new SessionEventSimpleConsumers()); + + log.info(session.setPermissionMode(PermissionMode.PLAN).map(s -> s ? "setPermissionMode 2 success" : "setPermissionMode 2 error") + .orElse("setPermissionMode 2 unknown")); + session.sendPrompt("rename test.touch to test_rename.touch", new SessionEventSimpleConsumers()); + + log.info(session.setPermissionMode(PermissionMode.AUTO_EDIT).map(s -> s ? "setPermissionMode 3 success" : "setPermissionMode 3 error") + .orElse("setPermissionMode 3 unknown")); + session.sendPrompt("rename test.touch to test_rename.touch", new SessionEventSimpleConsumers()); + + session.sendPrompt("rename test.touch to test_rename.touch again user will allow", + new SessionEventSimpleConsumers().setAssistantContentConsumer(new AssistantContentSimpleConsumers().setDefaultPermissionOperation(Operation.allow))); + + session.close(); + } + + @Test + void sendPromptAndSetModelSuccessfully() throws SessionControlException, SessionSendPromptException { + Session session = QwenCodeCli.newSession(new TransportOptions()); + + log.info(session.setModel("qwen3-coder-flash").map(s -> s ? "setModel 1 success" : "setModel 1 error").orElse("setModel 1 unknown")); + writeSplitLine("setModel 1 end"); + + session.sendPrompt("hello world", new SessionEventSimpleConsumers()); + writeSplitLine("prompt 1 end"); + + log.info(session.setModel("qwen3-coder-plus").map(s -> s ? "setModel 2 success" : "setModel 2 error").orElse("setModel 2 unknown")); + writeSplitLine("setModel 1 end"); + + session.sendPrompt("Check how many files are in the current directory", new SessionEventSimpleConsumers()); + writeSplitLine("prompt 2 end"); + + log.info(session.setModel("qwen3-max").map(s -> s ? "setModel 3 success" : "setModel 3 error").orElse("setModel 3 unknown")); + writeSplitLine("setModel 1 end"); + + session.sendPrompt("Check how many xml files are in the current directory", new SessionEventSimpleConsumers()); + writeSplitLine("prompt 3 end"); + + session.close(); + } + + @Test + void sendPromptAndInterruptContinueSuccessfully() throws SessionControlException, SessionSendPromptException { + Session session = QwenCodeCli.newSession(); + + SessionEventConsumers sessionEventConsumers = new SessionEventSimpleConsumers() { + + @Override + public void onSystemMessage(Session session, SDKSystemMessage systemMessage) { + log.info("systemMessage: {}", systemMessage); + } + + @Override + public void onResultMessage(Session session, SDKResultMessage resultMessage) { + log.info("resultMessage: {}", resultMessage); + } + + @Override + public void onAssistantMessage(Session session, SDKAssistantMessage assistantMessage) { + log.info("assistantMessage: {}", assistantMessage); + try { + session.interrupt(); + } catch (SessionControlException e) { + log.error("interrupt error", e); + } + } + + @Override + public void onControlResponse(Session session, CLIControlResponse cliControlResponse) { + log.info("cliControlResponse: {}", cliControlResponse); + } + + @Override + public void onOtherMessage(Session session, String message) { + log.info("otherMessage: {}", message); + } + }.setDefaultEventTimeout(new Timeout(90L, TimeUnit.SECONDS)); + + session.sendPrompt("Check how many files are in the current directory", sessionEventConsumers); + writeSplitLine("prompt 1 end"); + + session.continueSession(); + session.sendPrompt("hello world", sessionEventConsumers); + writeSplitLine("prompt 2 end"); + + session.continueSession(); + session.sendPrompt("How many Java files are in the current directory", sessionEventConsumers); + writeSplitLine("prompt 3 end"); + + session.close(); + } + + public void writeSplitLine(String line) { + log.info("{} {}", line, StringUtils.repeat("=", 300)); + } + + @Test + void testJSON() { + String json + = "{\"type\":\"assistant\",\"uuid\":\"ed8374fe-a4eb-4fc0-9780-9bd2fd831cda\"," + + "\"session_id\":\"166badc0-e6d3-4978-ae47-4ccd51c468ef\",\"message\":{\"content\":[{\"text\":\"Hello! How can I help you with the" + + " Qwen Code SDK for Java today?\",\"type\":\"text\"}],\"id\":\"ed8374fe-a4eb-4fc0-9780-9bd2fd831cda\"," + + "\"model\":\"qwen3-coder-plus\",\"role\":\"assistant\",\"type\":\"message\",\"usage\":{\"cache_read_input_tokens\":12766," + + "\"input_tokens\":12770,\"output_tokens\":17,\"total_tokens\":12787}}}"; + SDKAssistantMessage assistantMessage = JSON.parseObject(json, SDKAssistantMessage.class); + log.info("the assistantMessage: {}", assistantMessage); + } +} diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java new file mode 100644 index 000000000..97e6fe0d1 --- /dev/null +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java @@ -0,0 +1,19 @@ +package com.alibaba.qwen.code.cli.transport; + +import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PermissionModeTest { + + @Test + public void shouldBeReturnQwenPermissionModeValue() { + assertEquals("default", PermissionMode.DEFAULT.getValue()); + assertEquals("plan", PermissionMode.PLAN.getValue()); + assertEquals("auto-edit", PermissionMode.AUTO_EDIT.getValue()); + assertEquals("yolo", PermissionMode.YOLO.getValue()); + } + +} diff --git a/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransportTest.java b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransportTest.java new file mode 100644 index 000000000..a23800ada --- /dev/null +++ b/packages/sdk-java/src/test/java/com/alibaba/qwen/code/cli/transport/process/ProcessTransportTest.java @@ -0,0 +1,87 @@ +package com.alibaba.qwen.code.cli.transport.process; + +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInitializeRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.payload.CLIControlInitializeResponse; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlRequest; +import com.alibaba.qwen.code.cli.protocol.message.control.CLIControlResponse; +import com.alibaba.qwen.code.cli.protocol.message.SDKUserMessage; +import com.alibaba.qwen.code.cli.transport.Transport; +import com.alibaba.qwen.code.cli.transport.TransportOptions; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ProcessTransportTest { + + private static final Logger logger = LoggerFactory.getLogger(ProcessTransportTest.class); + + @Test + void shouldStartAndCloseSuccessfully() throws IOException { + TransportOptions transportOptions = new TransportOptions(); + Transport transport = new ProcessTransport(transportOptions); + transport.close(); + } + + @Test + void shouldInputWaitForOneLineSuccessfully() throws IOException, ExecutionException, InterruptedException, TimeoutException { + TransportOptions transportOptions = new TransportOptions(); + Transport transport = new ProcessTransport(transportOptions); + + String message = "{\"type\": \"control_request\", \"request_id\": \"1\", \"request\": {\"subtype\": \"initialize\"} }"; + System.out.println(transport.inputWaitForOneLine(message)); + } + + @Test + void shouldInitializeSuccessfully() throws IOException, ExecutionException, InterruptedException, TimeoutException { + Transport transport = new ProcessTransport(); + + String message = CLIControlRequest.create(new CLIControlInitializeRequest()).toString(); + String responseMsg = transport.inputWaitForOneLine(message); + logger.info("responseMsg: {}", responseMsg); + CLIControlResponse response = JSON.parseObject(responseMsg, + new TypeReference>() {}); + logger.info("response: {}", response); + } + + @Test + void shouldSdkMessageSuccessfully() throws IOException, ExecutionException, InterruptedException, TimeoutException { + Transport transport = new ProcessTransport(); + String message = CLIControlRequest.create(new CLIControlInitializeRequest()).toString(); + transport.inputWaitForOneLine(message); + + String sessionId = "session-" + UUID.randomUUID().toString(); + String userMessage = new SDKUserMessage().setSessionId(sessionId).setContent("hello world").toString(); + transport.inputWaitForMultiLine(userMessage, line -> { + return "result".equals(JSON.parseObject(line).getString("type")); + }); + + String userMessage2 = new SDKUserMessage().setSessionId(sessionId).setContent("Please respond in Chinese").toString(); + transport.inputWaitForMultiLine(userMessage2, line -> { + return "result".equals(JSON.parseObject(line).getString("type")); + }); + + + String userMessage3 = new SDKUserMessage().setSessionId(sessionId).setContent("How many files are there in the current workspace").toString(); + transport.inputWaitForMultiLine(userMessage3, line -> { + return "result".equals(JSON.parseObject(line).getString("type")); + }); + + String userMessage4 = new SDKUserMessage().setSessionId("session-sec" + UUID.randomUUID()).setContent("How many XML files are there").toString(); + transport.inputWaitForMultiLine(userMessage4, line -> { + return "result".equals(JSON.parseObject(line).getString("type")); + }); + + transport.inputWaitForOneLine(CLIControlRequest.create(new CLIControlInitializeRequest()).toString()); + transport.inputWaitForMultiLine(new SDKUserMessage().setContent("您好").toString(), + line -> "result".equals(JSON.parseObject(line).getString("type"))); + } + +} diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index a1310056f..435df48f3 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code-test-utils", - "version": "0.6.0", + "version": "0.6.1", "private": true, "main": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 5fa753162..50982df00 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -2,7 +2,7 @@ "name": "qwen-code-vscode-ide-companion", "displayName": "Qwen Code Companion", "description": "Enable Qwen Code with direct access to your VS Code workspace.", - "version": "0.6.0", + "version": "0.6.1", "publisher": "qwenlm", "icon": "assets/icon.png", "repository": { diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 6ea38b826..e4bdc0cf6 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -95,7 +95,7 @@ export class AcpConnection { const spawnCommand: string = process.execPath; const spawnArgs: string[] = [ cliEntryPath, - '--experimental-acp', + '--acp', '--channel=VSCode', ...extraArgs, ];