* feat(core): add continueInterruptedTurn to resume unfinished turns from history Detect how a session's last turn ended from persisted chat history alone (no in-memory request refs, so it works across process restarts) and continue it without injecting any synthetic user message: - interrupted_prompt (orphaned trailing user entry): re-submit the same content via SendMessageType.Retry — strip + re-push, same logical turn. - interrupted_turn (dangling functionCall): close each pair with a synthesized error functionResponse submitted as a ToolResult, the same continuation signal a real tool completion would have sent. - none: idempotent no-op (null stream). Core layer for the SDK/ACP "continue previous turn" capability discussed in #4679; protocol exposure (_qwen/session/continue) comes separately. * feat(cli,sdk): expose continue-interrupted-turn through ACP and SDK control protocol ACP surface: - New vendor ext method `qwen/session/continue` (acpAgent) backed by Session.continueTurn(): classifies the last turn from history and re-drives the model loop — Retry semantics for an orphaned trailing user entry, synthesized error tool results for dangling tool calls. No new user message enters the transcript; the prompt id reuses the interrupted turn's number. 409 when a turn is already in flight. - The model-call ↔ tool-call loop is extracted verbatim from #executePrompt into #runModelTurnLoop (+ #finishTurn epilogue) and shared by both paths. - initialize advertises the capability via agentCapabilities._meta (qwen.sessionContinue) per ACP extensibility guidance. SDK surface (stream-json control protocol): - New control request subtype `continue_last_turn` routed to SystemController and delegated to the nonInteractive Session, which schedules a continuation turn ahead of queued user messages. - runNonInteractive gains `continueInterrupted` mode: detection + Retry/ToolResult first-turn override; reminder injection is skipped so tool_result parts stay at the head of the user message. - Query.continueLastTurn() sends the request; reply reports { accepted, interruption } and resumed output flows as regular stream messages. Part of the #4679 continue-previous-turn work; core detection landed in the previous commit. * fix(core): replay the orphaned user entry verbatim, reminders included The Retry send path does not re-inject per-turn system reminders, so filtering them out of the captured entry made the resumed request differ from the original (the date reminder vanished). A continuation is the same turn, not a fresh one — capture the trailing user entry byte-for- byte so the resumed wire request is identical to the interrupted one. Verified end-to-end against a request-capturing mock server. * fix(cli): re-trigger work loop for pending continue turns; cover continueInterrupted - ensureProcessingStarted's finally-block re-trigger condition omitted pendingContinueTurns, so a continue request arriving as the work loop wound down could be stranded until the next user message. Add it to the guard alongside the other queues. - Add runNonInteractive({continueInterrupted}) unit tests: Retry path, ToolResult (interrupted_turn) path, and the clean-turn no-op. * fix(core,cli): restore orphaned entries when a continued send is skipped When resuming an interrupted turn, the orphaned trailing user entries are stripped from history before the content is re-sent. If that send is then skipped (e.g. session token limit exceeded), the stripped content was lost from history entirely. Have strip return the removed entries and restore them on any non-cancelled skip across the core retry, non-interactive, and ACP continue paths. On the ACP path the turn loop already re-adds functionResponse parts of an unsent message via #preserveUnsentMessageHistory, so a tool_result orphan would be written twice (once by preserve, once by restore). Let the caller own restoring the initial message and skip the loop's functionResponse-only preservation for that one message when it isn't a cancellation. * fix(cli,sdk): address continuation review feedback * fix(cli): address continuation review feedback * fix(cli): preserve continuation constraints and drain state * Merge latest main into feature branch * fix(core,cli): harden continuation recovery * fix(cli): close continuation review gaps * chore(cli): restore acp bridge path mapping * fix(cli): reject continuation during background turns * refactor(cli): simplify continuation handling * fix(cli): harden interrupted turn continuation * refactor(cli): share continuation helper logic * fix: harden interrupted-turn continuation * chore: remove unrelated continuation diff churn * refactor(cli): drop ACP session-continue surface from PR #5030 Issue #4679 only needs the SDK/stream-json continuation path. The SDK's Query.continueLastTurn() rides the stdio control protocol handled by the nonInteractive session, not ACP, and the qwen/session/continue vendor method had no SDK caller. Revert the ACP continuation surface (Session.continueTurn, #executeContinue, the qwen/session/continue routing and capability advertisement, and their tests) to the merge-base so the PR ships only the path the issue requires. * refactor(core): rename turnInterruption to kebab-case turn-interruption check-file/filename-naming-convention requires kebab-case for new files (AGENTS.md), so turnInterruption.ts failed CI lint. Rename the module and its test and update the index re-export. * fix(core,cli): delegate interrupted-prompt continuation to the Retry path The continuation path duplicated the orphan strip/restore that the Retry send in client.ts already performs, and its restore guard flipped on pre-push stream events, so an abort before the history push could permanently drop the orphaned user entry. Delegate to the Retry strip/restore, whose guard flips only after the content is committed. Removing the local strip drops the now-dead maxEntries parameter from stripOrphanedUserEntriesFromHistory, and detection now reads the full history (getHistory) so its re-submitted parts match the unbounded strip. Also fire onResultEmitted only after a successful emit, so a throwing emitResult cannot lose both the result and the error. * test(cli): cover insertAfterFunctionResponses ordering Add direct unit coverage for the splice helper used by the continuation reminder insertion: mixed parts, all-functionResponse input, empty additions, and empty parts. * feat(cli,acp,sdk): expose continue-last-turn to daemon + ACP and fix continuation review findings Surface the interrupted-turn continuation through two more entry points, both routing into a new Session.continueLastTurn() that reuses the core detectTurnInterruption classification and the existing send/tool loop: - daemon: new control method qwen/control/session/continue (status -> bridgeTypes -> bridge -> POST /session/:id/continue -> acpAgent handler), returning { accepted, interruption }. - ACP: a `qwen.daemon.continueLastTurn` prompt _meta flag, mirroring the existing daemon retry meta. interrupted_prompt re-submits the stripped orphaned run; interrupted_turn closes dangling tool calls with synthesized error functionResponses. System reminders are now inserted after any leading functionResponse parts so a tool-result continuation keeps tool_result blocks first (normal prompts unchanged). Also resolve the open continuation review findings: - core/client: history-length snapshot guard so a Retry send that fails before the first event never duplicates history (replaces the retrySendStarted flag that missed pre-event failures). - cli/nonInteractive/session: emit a terminal error result when an accepted continuation is abandoned by shutdown; emit a continue_turn_failed diagnostic when a continuation fails after a result was already flushed. - sdk/query: idempotent finishTransportRead with optional error, also invoked on the message-router error path, so a transport crash rejects pending control (continueLastTurn) and MCP requests instead of hanging. Adds regression tests across core/cli/sdk for each fix and the new surfaces. * test(sdk): cover finishTransportRead MCP-response rejection on teardown Addresses the review ask to validate that finishTransportRead rejects pending MCP responses when the transport read loop ends. White-box: injects a pending entry rather than standing up a full SDK MCP server handshake, since the rejection drain is map-internal regardless of how the entry was registered. * fix(acp): harden continueLastTurn — restore stripped orphan on send failure, fix ordering and error surfacing Review follow-ups on the ACP/daemon continuation path: - interrupted_prompt: preserve the full stripped orphan message back into history when a continuation send fails non-cancelled. Previously only functionResponse parts were kept, so a stripped text orphan was lost — the user's unanswered turn vanished from history. - worktree restore notice: insert after any leading functionResponse parts so a tool-result continuation keeps tool_result blocks first (Anthropic ordering), matching the system-reminder insertion. - continueLastTurn fire-and-forget: emit a client diagnostic on failure instead of log-only, so a failed continuation is visible rather than silently stalled. - log at warn when the in-prompt re-detection finds no interrupted turn. Adds a Session regression test asserting the orphan is preserved on a non-cancelled continuation send failure. * perf(acp,core): classify continuation from a bounded tail; O(1) retry length read Review follow-ups (all non-blocking): - continueLastTurn classifies from a shallow bounded tail (getHistoryTailShallow) instead of structuredClone-ing the entire history. The authoritative re-detection inside the fired prompt() still reads full history for the strip, matching the nonInteractive two-tier pattern. - the Retry history-length guard reads getHistoryLength() (O(1)) instead of getHistory().length, which structuredClones the whole history just to read .length (the truncateHistory path already warns about this). Adds a Session test for the pendingPrompt re-entrancy guard (continueLastTurn rejects while a prompt is in flight). * fix(acp-bridge): strip daemon continue meta key from external prompts An external POST /session/:id/prompt could set _meta['qwen.daemon.continueLastTurn'] and trigger a continuation through the prompt FIFO, bypassing continueLastTurn()'s admission guards. sendPrompt now strips it alongside the retry meta key; the legitimate path is the agent's internal Session.prompt() and does not go through sendPrompt. * fix(cli): resolve continuation review comments - Session.ts: reuse the shared insertAfterFunctionResponses helper for both the reminder and worktree-notice phases (removes the duplicated findIndex pattern) and guard against duplicate reminders on an interrupted_prompt continuation, mirroring nonInteractiveCli.ts. Adds an ordering test. - systemController.ts: point the missing-callback error at the wiring (onContinueLastTurn on ControlContext) instead of 'not available'. - nonInteractive/session.ts: tag the continue logs with sessionId. * fix(core): gate retry restore on a push counter, not history length The Retry strip/restore guard compared history length to a post-strip baseline to decide whether the re-submitted user content landed. Auto- compression inside GeminiChat.sendMessageStream runs before the push and shrinks history independently of it, so the length could fall below the baseline even after a successful push, restoring the stripped entries and duplicating the user's prompt (reachable on success once compaction removes >=2 entries, common near the token limit). GeminiChat now exposes a monotonic userContentPushCount (incremented when the user content is pushed, decremented only if that push is rolled back on a setup error). client.ts snapshots it after the strip and restores only when it did not advance — invariant under compression. * fix(sdk): log pending requests when the transport finalizes finishTransportRead rejected pending control/MCP requests with no anchor log, so a CLI crash mid-continuation left only scattered rejections. Emit one correlatable line with the pending counts and error when work was in flight. * fix(serve): track daemon continuation through the prompt-admission path POST /session/:id/continue ran the continuation as an untracked internal agent prompt (continueLastTurn fired Session.prompt() fire-and-forget via the sessionContinue control method). The bridge never set pendingPromptCount / promptActive / activePromptOriginatorClientId for it, so the daemon reported the session idle during a continuation, a racing POST /prompt was admitted with no FIFO wait and aborted the in-flight continuation, and the continuation's events went out unattributed. Now continueLastTurn is the accept/reject pre-check only; when it accepts, bridge.continueSession drives the actual turn through sendPrompt with a trusted continue flag (re-arming the continue meta key that sendPrompt strips from untrusted callers), so the continuation gets full admission tracking, FIFO serialization, originator attribution, and turn-complete broadcasting. The /continue route forwards X-Qwen-Client-Id and continueSession validates it up-front, matching POST /session/:id/prompt. The {accepted, interruption} response contract is preserved. Addresses the continuation-lifecycle review finding. * test(serve): cover continuation admission tracking end-to-end - bridge: assert a continuation reports the session BUSY (hasActivePrompt + pendingPromptCount) and that a racing prompt queues behind it via the FIFO instead of aborting it — the two behaviors the lifecycle fix targets. - serve: assert POST /session/:id/continue forwards X-Qwen-Client-Id to continueSession so the continuation turn is attributed like a prompt. * test(serve): add real-daemon smoke for POST /session/:id/continue Exercises the full route → bridge → agent continuation path against a spawned daemon: a fresh session has no interrupted turn, so /continue returns {accepted:false, interruption:'none'} (model-free, like the prompt-admission checks). Also fixes the now-stale DAEMON_CONTINUE_META_KEY comment to reflect that continueSession (not continueLastTurn) re-arms the meta via sendPrompt. * refactor(acp-bridge): drop dead AbortController in continueSession; harden reject test - continueSession passes undefined instead of a never-aborted AbortController to sendPrompt (continuations cancel via the cancelSession route, not this handle). - The 'rejected continueSession does not dispatch' test waits long enough for a regression fire-and-forget to reach the agent before asserting none ran, instead of a single microtask flush. * fix(cli): restore orphaned turn when a continuation send throws The interrupted_prompt continuation strips the orphaned user run from history before re-sending it. #preserveUnsentMessageHistory only restores on a graceful null-stream result, not when the send throws (e.g. a hard-rescue compression error before the user-content push) — so the orphan was permanently lost (transcript gap; --resume loads history missing the user's turn). Capture the stripped entries + a GeminiChat push-count snapshot, and restore them in the send-loop catch when the count did not advance (mirrors the core Retry restore in client.ts) — so a later tool-loop failure, where the orphan already landed, does not double-restore. Also emit conversation_finished on the no-op continuation early-return, which sits before the send-loop try/finally. * fix(serve): surface continuation admission failures + mirror prompt replay contract continueSession previously swallowed post-accept admission failures (queue-full, pre-aborted, session reaped) yet returned accepted:true, leaving SDK callers waiting for output that never arrives. Now it dispatches sendPrompt synchronously so an admission throw propagates (the caller gets an error, not a false accept); only post-admission turn failures are logged (now via teeServeDebugLine, so they reach the SSE diagnostic stream). It also mirrors POST /session/:id/prompt's replay/correlation contract: capture lastEventId + accept a route-generated promptId and return both on accept, so a client attaching the SSE stream can replay missed events and correlate turn_complete/turn_error. Adds tests for the combined retry+continue meta strip, nonexistent-session and async-failure error paths, and the new contract fields. * chore(core): log retry-restore decision for diagnostics restoreStrippedRetryEntries mutates history in a finally with no trace; a single debugLogger.info on the restore branch makes a future push-counter regression (silent duplicate/loss) diagnosable. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|---|---|---|
| .github | ||
| .husky | ||
| .qwen | ||
| .vscode | ||
| docs | ||
| docs-site | ||
| eslint-rules | ||
| integration-tests | ||
| packages | ||
| patches | ||
| scripts | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .yamllint.yml | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| Dockerfile | ||
| esbuild.config.js | ||
| eslint.config.js | ||
| eslint.legacy-filenames.mjs | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
The open-source AI coding agent that lives in your terminal.
中文 | Deutsch | français | 日本語 | Русский | Português (Brasil)
Why Qwen Code?
- Agentic out of the box — Auto-Memory, Auto-Skills, SubAgents, Agent Teams, and MCP. Dynamic workflows, zero setup.
- Open-source, inside and out — The framework and the Qwen models are open-source. They evolve together. No vendor lock-in.
- Multi-protocol — Supports OpenAI, Anthropic, Gemini, and Qwen APIs. Any third-party provider or local model (Ollama / vLLM). Switch at runtime.
- Beyond the terminal — IDE plugins, Desktop app, daemon mode, SDKs, and IM bots (Telegram / DingTalk / WeChat / Feishu).
Tip
Qwen Code is actively iterating on itself — using its own agent and models to file issues, submit PRs, review code, and run tests. Powered by the community, driven by AI.
Installation
Linux / macOS:
curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash
Windows:
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex
Restart your terminal after installation to ensure environment variables take effect.
NPM / Homebrew
NPM (requires Node.js 22+):
npm install -g @qwen-code/qwen-code@latest
Homebrew (macOS / Linux):
brew install qwen-code
Quick Start
qwen # Launch interactive terminal UI
# Inside the session:
/auth # Configure your provider and API key
See the Authentication Guide and Settings Reference for detailed setup.
How to Use Qwen Code
| Mode | Command | Use Case |
|---|---|---|
| Interactive | qwen |
Terminal UI with rich rendering, @file references, slash commands |
| Headless | qwen -p "..." |
Scripts, CI/CD, batch processing — no UI |
| IDE | — | VS Code, Zed, JetBrains |
| Desktop | — | Qwen Code Desktop — GUI for macOS, Windows, Linux |
| Daemon | qwen serve |
Shared agent session over HTTP+SSE (ACP). Multiple clients, one agent. (experimental) Docs |
| SDK | — | TypeScript, Python, Java |
| IM Bot | qwen channel |
Connect to Telegram, DingTalk, WeChat, or Feishu |
SDK example (Python)
import asyncio
from qwen_code_sdk import is_sdk_result_message, query
async def main() -> None:
result = query(
"Summarize the repository layout.",
{
"cwd": "/path/to/project",
"path_to_qwen_executable": "qwen",
},
)
async for message in result:
if is_sdk_result_message(message):
print(message["result"])
asyncio.run(main())
Capabilities
If you know Claude Code, you already know Qwen Code — and then some. We've put significant effort into bringing Qwen Code to feature parity with Claude Code, improving both breadth and reliability across the board.
| Feature | Qwen Code | Claude Code |
|---|---|---|
| SubAgents, Agent Teams, Dynamic Workflows | ✓ | ✓ |
| Auto-Memory, Auto-Skills, Hooks | ✓ | ✓ |
| Built-in Skills (/review, /batch, /loop, /bugfix…) | ✓ | ✓ |
| MCP, Plan Mode, LSP Integration | ✓ | ✓ |
| Auto Mode, Sandbox, Git Worktrees | ✓ | ✓ |
| Computer Use (desktop automation) | ✓ | ✓ |
| IDE Plugins (VS Code / JetBrains / Zed) | ✓ | ✓ |
| SDK | ✓ | ✓ |
| Headless Mode, Session Management | ✓ | ✓ |
| Open-source — model and framework | ✓ | — |
| Multi-protocol (OpenAI / Anthropic / Gemini / Qwen + any provider) | ✓ | — |
| Agent Arena (multi-model head-to-head on same task) | ✓ | — |
Daemon Mode — qwen serve (multi-client shared agent) |
✓ | — |
| IM Channels (Telegram / DingTalk / WeChat / Feishu) | ✓ | — |
Ecosystem
-
Qwen Code Desktop — Official desktop app for macOS, Windows, and Linux
-
AionUi — A modern GUI for command-line AI tools including Qwen Code
-
Gemini CLI Desktop — A cross-platform desktop/web/mobile UI for Qwen Code
-
🦞 Qwen Code Claw — Let other agents (Claude, Codex, etc.) delegate coding tasks to Qwen Code via ACP. Paste this prompt into your agent:
Use Qwen Code as a coding agent. First, fetch and read this skill reference:
https://raw.githubusercontent.com/QwenLM/qwen-code/refs/heads/main/.qwen/skills/qwen-code-claw/SKILL.md
Then follow the instructions inside to install, authenticate, and use Qwen Code via acpx for all coding tasks.
Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines.
Acknowledgments
This project was originally based on Google Gemini CLI v0.8.2. We gratefully acknowledge the Gemini CLI team's excellent work. Starting from Qwen Code v0.1, we stopped syncing with upstream and began independent development as a multi-protocol, multi-platform agent framework with deep integrations for Qwen models and beyond.
