# @qwen-code/sdk A minimum experimental TypeScript SDK for programmatic access to Qwen Code. Feel free to submit a feature request/issue/PR. ## Installation ```bash npm install @qwen-code/sdk ``` ## Requirements - Node.js >= 22.0.0 > From v0.1.1, the CLI is bundled with the SDK. So no standalone CLI installation is needed. ## Quick Start ```typescript import { query } from '@qwen-code/sdk'; // Single-turn query const result = query({ prompt: 'What files are in the current directory?', options: { cwd: '/path/to/project', }, }); // Iterate over messages for await (const message of result) { if (message.type === 'assistant') { console.log('Assistant:', message.message.content); } else if (message.type === 'result') { console.log('Result:', message.result); } } ``` ## API Reference ### `query(config)` Creates a new query session with the Qwen Code. #### Parameters - `prompt`: `string | AsyncIterable` - The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations. - `options`: `QueryOptions` - Configuration options for the query session. #### QueryOptions | Option | Type | Default | Description | | ------------------------ | -------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cwd` | `string` | `process.cwd()` | The working directory for the query session. Determines the context in which file operations and commands are executed. | | `model` | `string` | - | The AI model to use (e.g., `'qwen-max'`, `'qwen-plus'`, `'qwen-turbo'`). Takes precedence over `OPENAI_MODEL` and `QWEN_MODEL` environment variables. | | `pathToQwenExecutable` | `string` | Bundled CLI | Path to the Qwen Code executable. Supports multiple formats: `'qwen'` (native binary from PATH), `'/path/to/qwen'` (explicit path), `'/path/to/cli.js'` (Node.js bundle), `'node:/path/to/cli.js'` (force Node.js runtime), `'bun:/path/to/cli.js'` (force Bun runtime). If not provided, the SDK uses the bundled CLI included with the package. | | `permissionMode` | `'default' \| 'plan' \| 'auto-edit' \| 'auto' \| 'yolo'` | `'default'` | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details. | | `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). | | `env` | `Record` | - | Environment variables to pass to the Qwen Code process. Merged with the current process environment. | | `systemPrompt` | `string \| QuerySystemPromptPreset` | - | System prompt configuration for the main session. Use a string to fully override the built-in Qwen Code system prompt, or a preset object to keep the built-in prompt and append extra instructions. | | `mcpServers` | `Record` | - | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`. | | `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. | | `debug` | `boolean` | `false` | Enable debug mode for verbose logging from the CLI process. | | `maxSessionTurns` | `number` | `-1` (unlimited) | Maximum number of conversation turns before the session automatically terminates. Must be an integer. A turn consists of a user message and an assistant response. | | `coreTools` | `string[]` | - | Uses the legacy `coreTools` / CLI `--core-tools` allowlist semantics. If specified, only matching core tools are registered for the session. This is the only allowlist-style option that restricts built-in tool registration; a whole-tool `permissions.deny` / `excludeTools` rule (and `tools.disabled` in settings.json) also removes a tool from the registry. `permissions.allow` in settings.json is pure auto-approval and never removes, demotes, or hides a tool (#10075). To keep a tool's schema out of the initial model request, use `tools.eager` in settings.json (requires restart, #9827) — `tool_search`, `tool_call`, `structured_output`, plan-mode lifecycle tools, `task_stop`, `mcp__*` and `computer_use__*` tools are exempt from that allowlist and keep their normal loading; tools demoted this way stay registered and reachable through `tool_search` + `tool_call` while both bridge tools are registered — when either is unregistered (`tools.toolSearch.enabled: false` denies both; a `tool_search` or `tool_call` deny rule, or a `tools.disabled` entry removes one) the demoted tools that remain hidden are not offered to the model and cannot be reached through the bridge for that session, and a warning is written to the CLI process's stderr (SDK forwards it only with piped stderr and effective `debug` logging; an explicit `logLevel` always wins). These bridge and warning rules apply to direct tool mode. CodeModeOnly finds deferred schemas with top-level `tool_search` and calls them through `exec`; `tools.eager` also shrinks the initial `exec` description. Without search in scope, `exec` lists each allowed tool's signature. In direct mode they stay registered, so a direct call by their own name is still evaluated and approved normally — except tools also listed in `tools.visible`, which are declared upfront, and sessions whose live history contains a direct call to a still-hidden demoted tool, which any tool-set refresh (resume, MCP discovery, the first plan-mode entry in a session, a subagent definition change) re-declares.; to remove a tool entirely, use a whole-tool `excludeTools` / `permissions.deny` rule — a rule with a specifier (such as `'Bash(rm *)'`) only denies matching invocations at runtime. MCP tools are exempt from deny-based removal: hide them with the per-server `excludeTools` / `tools.disabled` filters instead (deny still blocks their calls at runtime). Example: `['read_file', 'edit', 'run_shell_command']`. | | `excludeTools` | `string[]` | - | Equivalent to `permissions.deny` in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports tool name aliases and pattern matching: tool name (`'write_file'`), shell command prefix (`'Bash(rm *)'`), or path patterns (`'Read(.env)'`, `'Edit(/src/**)'`). | | `allowedTools` | `string[]` | - | Equivalent to `permissions.allow` in settings.json for auto-approval. Matching tools bypass `canUseTool` callback and execute automatically. Only applies when tool requires confirmation. Like `permissions.allow`, this is pure auto-approval and never affects which tools are registered or which schemas are sent (#10075). Supports same pattern matching as `excludeTools`. Example: `['Bash(git status)', 'Bash(npm test)']`. | | `authType` | `'openai' \| 'anthropic' \| 'qwen-oauth' \| 'gemini' \| 'vertex-ai'` | - | Authentication type for the AI service. When provided, the SDK forwards it to the CLI as `--auth-type`. | | `agents` | `SubagentConfig[]` | - | Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains. | | `includePartialMessages` | `boolean` | `false` | When `true`, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response. | | `resume` | `string` | - | Resume a previous session by providing its session ID. Equivalent to CLI's `--resume` flag. | | `sessionId` | `string` | - | Specify a session ID for the new session. Ensures SDK and CLI use the same ID without resuming history. Equivalent to CLI's `--session-id` flag. | > [!tip] > If you need to configure `coreTools`, `excludeTools`, or `allowedTools`, it is **strongly recommended** to read the [permissions configuration documentation](../../docs/users/configuration/settings.md#permissions) first, especially the **Tool name aliases** and **Rule syntax examples** sections. Rule patterns such as `Bash(git *)`, `Read(.env)`, and `Edit(/src/**)` apply to `excludeTools` and `allowedTools`; `coreTools` accepts aliases but strips invocation specifiers. #### Empty core-tool allowlists An omitted or empty `coreTools` list is treated as unspecified; `coreTools: []` does **not** create a tool-free session. A non-empty list restricts core tools, not every tool category: MCP and other non-core tools have separate registration and permission rules. Use `excludeTools` to exclude named tools explicitly. `allowedTools` controls approval, not which tools are registered. ### Timeouts The SDK enforces the following default timeouts: | Timeout | Default | Description | | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `canUseTool` | 1 minute | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied. | | `mcpRequest` | 1 minute | Maximum time for SDK MCP tool calls to complete. | | `controlRequest` | 1 minute | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, `getContextUsage()`, and `interrupt()` to complete. | | `streamClose` | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. | You can customize these timeouts via the `timeout` option: ```typescript import { query } from '@qwen-code/sdk'; const q = query({ prompt: 'Your prompt', options: { timeout: { canUseTool: 60000, // 60 seconds for permission callback mcpRequest: 600000, // 10 minutes for MCP tool calls controlRequest: 60000, // 60 seconds for control requests streamClose: 15000, // 15 seconds for stream close wait }, }, }); ``` ### Experimental Daemon Session Client `DaemonSessionClient` is an experimental wrapper for clients that talk to a running `qwen serve` daemon over HTTP + SSE. It binds one daemon session so TUI, channel, IDE, or web backend adapters do not need to pass `sessionId` into every call. ```typescript import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk'; const daemon = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170', token: process.env['QWEN_SERVER_TOKEN'], }); const caps = await daemon.capabilities(); const session = await DaemonSessionClient.createOrAttach(daemon, { workspaceCwd: caps.workspaceCwd, }); const eventController = new AbortController(); const eventTask = (async () => { for await (const event of session.events({ signal: eventController.signal, })) { console.log(event.type, event.data); } })(); const result = await session.prompt({ prompt: [{ type: 'text', text: 'Summarize this workspace.' }], }); eventController.abort(); await eventTask; console.log(result.stopReason); ``` `session.events()` tracks the last seen SSE event id and reuses it on the next subscription by default. Pass `{ resume: false }` to start a fresh subscription without sending `Last-Event-ID`. When `createOrAttach()` is called with `modelServiceId`, the returned session client seeds its first event subscription with `Last-Event-ID: 0`. This replays the daemon ring from the oldest available event so adapters can observe attach-time `model_switch_failed` or `model_switched` events that are not reported on the create/attach HTTP response. Raw `DaemonClient` callers should pass `{ lastEventId: 0 }` on their first `subscribeEvents()` call when they use `modelServiceId`. The raw event envelope remains available as `DaemonEvent` with `data: unknown`. Adapters that want a v1 typed view can layer the schema helpers on top without changing the wire stream: ```typescript import { asKnownDaemonEvent, createDaemonSessionViewState, reduceDaemonSessionEvent, } from '@qwen-code/sdk'; let view = createDaemonSessionViewState(); for await (const event of session.events()) { view = reduceDaemonSessionEvent(view, event); const known = asKnownDaemonEvent(event); if (known?.type === 'permission_request') { console.log(known.data.requestId); } } ``` ### Offline daemon transcript projection The opt-in browser-safe transcript entry converts already-parsed append-only ChatRecord values into the same block model used by daemon clients. It does not read files, parse JSONL text, start a daemon, or access network or browser storage. ```typescript import { projectChatRecordsToDaemonTranscript } from '@qwen-code/sdk/daemon/transcript'; const projection = projectChatRecordsToDaemonTranscript(records); if (!projection.complete) { console.warn(projection.diagnostics); } renderTranscript(projection.blocks); ``` Projection is synchronous and scans all records and message parts. `maxBlocks` limits retained output blocks, not computation. As a conservative browser guideline, project up to roughly 1,000 ordinary records on the main thread; move larger or unusually text-heavy inputs to a Web Worker and call the same entry there. ### Talking to running sessions `@qwen-code/sdk/peer` lets a program that is not a Qwen Code session join the sessions running as the same user on the same machine — a voice front-end, a relay, a build watcher. The program shows up in `qwen sessions ps`, and in the `list_agents` of every session that takes part in cross-session messaging — its `agents.crossSessionMessaging` setting is on, and it was not started with `--bare` or `--safe-mode` — which is also what lets those sessions message it by name with `send_message`. It can message them back. It runs on Node only and needs nothing beyond Node itself. ```typescript import { PeerEndpoint } from '@qwen-code/sdk/peer'; const endpoint = await PeerEndpoint.start({ name: 'voice-bridge', onMessage: (message) => console.log(`${message.fromName}: ${message.content}`), }); const [session] = await endpoint.list(); if (session) { const sent = await endpoint.send({ to: session.address, content: 'What are you working on?', }); if (sent.kind === 'sent') { const receipt = await endpoint.awaitReceipt(sent.msgId, { final: true }); console.log(receipt?.status); // delivered, denied, refused, ... } } await endpoint.close(); ``` A message like that is held for the session's user to review. To direct a session without that review, mint a controller token with `qwen sessions controllers add --label voice-bridge`, give it to the endpoint, and mark the sends that should present it: ```typescript const endpoint = await PeerEndpoint.start({ name: 'voice-bridge', controllerToken: process.env['QWEN_CONTROLLER_TOKEN'], }); await endpoint.send({ to: 'my-app-3f', content: 'run the tests', controller: true, }); ``` Things to know: - A session has an inbox only while its `agents.crossSessionMessaging` setting is on, which it is by default, and it was not started with `--bare` or `--safe-mode` — those turn messaging off whatever the setting says. A session that turned it off does not appear in `list()`, and its own `list_agents` and `send_message` cannot see or reach the program either. `qwen sessions ps` lists the program regardless. - A message is delivered without review in exactly two cases: the send presents a controller token (`controller: true`), or its `fromMode` names the receiving session's own review class. `fromMode` is a claim nothing authenticates, so a program that is not a coding session should leave it out. Nothing in the record — not `kind`, not `name` — buys delivery. The receiving session's `agents.crossSessionInbound` setting outranks both: `hold` or `refuse` there wins over a controller token. - Mark only the sends meant to direct a session. Addresses are resolved from records any program running as you can write, so a controller send presents the token to whichever process's record answers to that address. A controller send to another peer endpoint is dropped unread, because an endpoint's inbox accepts only its own token. - The endpoint's inbox applies none of the protections a Qwen Code session applies to its own: no rate limit, no holds, and no duplicate window beyond the last 200 messages it answered. Every message is answered `delivered` and handed to `onMessage` as it arrives, so apply your own limits there if you need them. Without `onMessage`, every message is answered `refused`. - Call `close()` before exiting, including from your own signal handlers. A process killed without closing leaves its record behind until a Qwen Code session lists the directory and sees the process is gone. - UNIX domain sockets only: Windows is not supported yet. The record schema, wire format and receipt states are documented in [Cross-Session Protocol](https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/cross-session-protocol.md). ### Message Types The SDK provides type guards to identify different message types: ```typescript import { isSDKUserMessage, isSDKAssistantMessage, isSDKSystemMessage, isSDKResultMessage, isSDKPartialAssistantMessage, } from '@qwen-code/sdk'; for await (const message of result) { if (isSDKAssistantMessage(message)) { // Handle assistant message } else if (isSDKResultMessage(message)) { // Handle result message } } ``` ### Query Instance Methods The `Query` instance returned by `query()` provides several methods: ```typescript const q = query({ prompt: 'Hello', options: {} }); // Get session ID const sessionId = q.getSessionId(); // Check if closed const closed = q.isClosed(); // Interrupt the current operation await q.interrupt(); // Change permission mode mid-session await q.setPermissionMode('yolo'); // Change model mid-session await q.setModel('qwen-max'); // Get context window usage breakdown (token counts per category) const usage = await q.getContextUsage(); // Pass true to hint that per-item details should be displayed const detail = await q.getContextUsage(true); // Close the session await q.close(); ``` ## 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`, `notebook_edit`) while other tools require confirmation. - **`auto`**: Uses the built-in classifier to auto-approve safe tool calls and block risky ones, with manual-approval fallback after repeated policy blocks or classifier outages. - **`yolo`**: All tools execute automatically without confirmation. ### Permission Priority Chain Decision priority (highest first): `deny` > `ask` > `allow` > _(default/interactive mode)_ The first matching rule wins. 1. `excludeTools` / `permissions.deny` - Blocks tools completely (returns permission error) 2. `permissions.ask` - Always requires user confirmation 3. `permissionMode: 'plan'` - Blocks all non-read-only tools 4. `permissionMode: 'yolo'` - Auto-approves all tools 5. `allowedTools` / `permissions.allow` - Auto-approves matching tools 6. `permissionMode: 'auto'` - Classifier-mediated approval for remaining tools 7. `canUseTool` callback - Custom approval logic (if provided, not called for allowed tools) 8. Default behavior - Auto-deny in SDK mode (write tools require explicit approval) ## Examples ### Multi-turn Conversation ```typescript import { query, type SDKUserMessage } from '@qwen-code/sdk'; async function* generateMessages(): AsyncIterable { yield { type: 'user', session_id: 'my-session', message: { role: 'user', content: 'Create a hello.txt file' }, parent_tool_use_id: null, }; // Wait for some condition or user input yield { type: 'user', session_id: 'my-session', message: { role: 'user', content: 'Now read the file back' }, parent_tool_use_id: null, }; } const result = query({ prompt: generateMessages(), options: { permissionMode: 'auto-edit', }, }); for await (const message of result) { console.log(message); } ``` ### Custom Permission Handler ```typescript import { query, type CanUseTool } from '@qwen-code/sdk'; const canUseTool: CanUseTool = async (toolName, input, { signal }) => { // Allow all read operations if (toolName.startsWith('read_')) { return { behavior: 'allow', updatedInput: input }; } // Prompt user for write operations (in a real app) const userApproved = await promptUser(`Allow ${toolName}?`); if (userApproved) { return { behavior: 'allow', updatedInput: input }; } return { behavior: 'deny', message: 'User denied the operation' }; }; const result = query({ prompt: 'Create a new file', options: { canUseTool, }, }); ``` ### Handling `ask_user_question` When the model needs a decision from the user it calls the built-in `ask_user_question` tool. The SDK surfaces this through the same `canUseTool` callback: the tool input contains a `questions` array, and you return the collected answers via `updatedInput.answers`. `answers` is an object keyed by the question's index (as a string), where each value is the label of the chosen option (or free-form text when the user picks "Other"). ```typescript import { query, type CanUseTool } from '@qwen-code/sdk'; const canUseTool: CanUseTool = async (toolName, input, { signal }) => { if (toolName === 'ask_user_question') { const questions = input.questions as Array<{ question: string; header: string; options: Array<{ label: string; description: string }>; }>; // Present the questions to the user however your app sees fit, then // build an index-keyed map of their answers. const answers: Record = {}; for (let i = 0; i < questions.length; i++) { answers[String(i)] = await promptUserToChoose(questions[i]); } // Return the answers through `updatedInput.answers` — the CLI forwards // them to the tool so the model receives the user's decisions. return { behavior: 'allow', updatedInput: { ...input, answers } }; } return { behavior: 'allow', updatedInput: input }; }; ``` > If you return `allow` without any `answers`, the tool reports that no > answer was provided; return `deny` to signal the user declined. ### With External MCP Servers ```typescript import { query } from '@qwen-code/sdk'; const result = query({ prompt: 'Use the custom tool from my MCP server', options: { mcpServers: { 'my-server': { command: 'node', args: ['path/to/mcp-server.js'], env: { PORT: '3000' }, }, }, }, }); ``` ### Override the System Prompt ```typescript import { query } from '@qwen-code/sdk'; const result = query({ prompt: 'Say hello in one sentence.', options: { systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.', }, }); ``` ### Append to the Built-in System Prompt ```typescript import { query } from '@qwen-code/sdk'; const result = query({ prompt: 'Review the current directory.', options: { systemPrompt: { type: 'preset', preset: 'qwen_code', append: 'Be terse and focus on concrete findings.', }, }, }); ``` ### With SDK-Embedded MCP Servers The SDK provides `tool` and `createSdkMcpServer` to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process. #### `tool(name, description, inputSchema, handler)` Creates a tool definition with Zod schema type inference. | Parameter | Type | Description | | ------------- | ---------------------------------- | ------------------------------------------------------------------------ | | `name` | `string` | Tool name (1-64 chars, starts with letter, alphanumeric and underscores) | | `description` | `string` | Human-readable description of what the tool does | | `inputSchema` | `ZodRawShape` | Zod schema object defining the tool's input parameters | | `handler` | `(args, extra) => Promise` | Async function that executes the tool and returns MCP content blocks | The handler must return a `CallToolResult` object with the following structure: ```typescript { content: Array< | { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string } | { type: 'resource'; uri: string; mimeType?: string; text?: string } >; isError?: boolean; } ``` #### `createSdkMcpServer(options)` Creates an SDK-embedded MCP server instance. | Option | Type | Default | Description | | --------- | ------------------------ | --------- | ------------------------------------ | | `name` | `string` | Required | Unique name for the MCP server | | `version` | `string` | `'1.0.0'` | Server version | | `tools` | `SdkMcpToolDefinition[]` | - | Array of tools created with `tool()` | Returns a `McpSdkServerConfigWithInstance` object that can be passed directly to the `mcpServers` option. #### Example ```typescript import { z } from 'zod'; import { query, tool, createSdkMcpServer } from '@qwen-code/sdk'; // Define a tool with Zod schema const calculatorTool = tool( 'calculate_sum', 'Add two numbers', { a: z.number(), b: z.number() }, async (args) => ({ content: [{ type: 'text', text: String(args.a + args.b) }], }), ); // Create the MCP server const server = createSdkMcpServer({ name: 'calculator', tools: [calculatorTool], }); // Use the server in a query const result = query({ prompt: 'What is 42 + 17?', options: { permissionMode: 'yolo', mcpServers: { calculator: server, }, }, }); for await (const message of result) { console.log(message); } ``` ### Abort a Query ```typescript import { query, isAbortError } from '@qwen-code/sdk'; const abortController = new AbortController(); const result = query({ prompt: 'Long running task...', options: { abortController, }, }); // Abort after 5 seconds setTimeout(() => abortController.abort(), 5000); try { for await (const message of result) { console.log(message); } } catch (error) { if (isAbortError(error)) { console.log('Query was aborted'); } else { throw error; } } ``` ## Error Handling The SDK provides an `AbortError` class for handling aborted queries: ```typescript import { AbortError, isAbortError } from '@qwen-code/sdk'; try { // ... query operations } catch (error) { if (isAbortError(error)) { // Handle abort } else { // Handle other errors } } ``` ## FAQ / Troubleshooting ### Version 0.1.0 Requirements If you're using SDK version **0.1.0**, please note the following requirements: #### Qwen Code Installation Required Version 0.1.0 requires [Qwen Code](https://github.com/QwenLM/qwen-code) **>= 0.4.0** to be installed separately and accessible in your PATH. ```bash # Install Qwen Code globally npm install -g @qwen-code/qwen-code@latest ``` **Note**: From version **0.1.1** onwards, the CLI is bundled with the SDK, so no separate Qwen Code installation is needed. ## License Apache-2.0 - see [LICENSE](../../LICENSE) for details.