From b40d2fbefa74eebf8bbed32efa5102f52b52d54f Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 11 Feb 2026 14:59:12 +0800 Subject: [PATCH 1/6] feat(acp): add protocol version negotiation and V1 consistency tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement ACP version management framework so the server properly negotiates protocol_version instead of echoing the client's value. Fix SDK version declaration (0.7.0 → 0.8.0) to match the actual installed package, and add 13 new tests covering both the negotiation logic and end-to-end protocol V1 consistency via the real SDK client. --- pyproject.toml | 2 +- src/kimi_cli/acp/server.py | 9 ++- src/kimi_cli/acp/version.py | 46 ++++++++++++ tests/acp/__init__.py | 0 tests/acp/conftest.py | 136 ++++++++++++++++++++++++++++++++++ tests/acp/test_protocol_v1.py | 115 ++++++++++++++++++++++++++++ tests/acp/test_version.py | 56 ++++++++++++++ uv.lock | 8 +- 8 files changed, 365 insertions(+), 7 deletions(-) create mode 100644 src/kimi_cli/acp/version.py create mode 100644 tests/acp/__init__.py create mode 100644 tests/acp/conftest.py create mode 100644 tests/acp/test_protocol_v1.py create mode 100644 tests/acp/test_version.py diff --git a/pyproject.toml b/pyproject.toml index 1abced6b7..e449e98ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Kimi Code CLI is your next CLI agent." readme = "README.md" requires-python = ">=3.12" dependencies = [ - "agent-client-protocol==0.7.0", + "agent-client-protocol==0.8.0", "aiofiles>=24.0,<26.0", "aiohttp==3.13.3", "typer==0.21.1", diff --git a/src/kimi_cli/acp/server.py b/src/kimi_cli/acp/server.py index 5e77a0eed..3524e72ce 100644 --- a/src/kimi_cli/acp/server.py +++ b/src/kimi_cli/acp/server.py @@ -14,6 +14,7 @@ from kimi_cli.acp.session import ACPSession from kimi_cli.acp.tools import replace_tools from kimi_cli.acp.types import ACPContentBlock, MCPServer +from kimi_cli.acp.version import ACPVersionSpec, negotiate_version from kimi_cli.app import KimiCLI from kimi_cli.config import LLMModel, load_config, save_config from kimi_cli.constant import NAME, VERSION @@ -29,6 +30,7 @@ def __init__(self) -> None: self.client_capabilities: acp.schema.ClientCapabilities | None = None self.conn: acp.Client | None = None self.sessions: dict[str, tuple[ACPSession, _ModelIDConv]] = {} + self.negotiated_version: ACPVersionSpec | None = None def on_connect(self, conn: acp.Client) -> None: logger.info("ACP client connected") @@ -41,10 +43,13 @@ async def initialize( client_info: acp.schema.Implementation | None = None, **kwargs: Any, ) -> acp.InitializeResponse: + self.negotiated_version = negotiate_version(protocol_version) logger.info( - "ACP server initialized with protocol version: {version}, " + "ACP server initialized with client protocol version: {version}, " + "negotiated version: {negotiated}, " "client capabilities: {capabilities}, client info: {info}", version=protocol_version, + negotiated=self.negotiated_version, capabilities=client_capabilities, info=client_info, ) @@ -59,7 +64,7 @@ async def initialize( args = sys.argv[1 : idx + 1] return acp.InitializeResponse( - protocol_version=protocol_version, + protocol_version=self.negotiated_version.protocol_version, agent_capabilities=acp.schema.AgentCapabilities( load_session=True, prompt_capabilities=acp.schema.PromptCapabilities( diff --git a/src/kimi_cli/acp/version.py b/src/kimi_cli/acp/version.py new file mode 100644 index 000000000..bdbe91381 --- /dev/null +++ b/src/kimi_cli/acp/version.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ACPVersionSpec: + """Describes one supported ACP protocol version.""" + + protocol_version: int # negotiation integer (currently 1) + spec_tag: str # ACP spec tag (e.g. "v0.10.8") + sdk_version: str # corresponding SDK version (e.g. "0.8.0") + + +CURRENT_VERSION = ACPVersionSpec( + protocol_version=1, + spec_tag="v0.10.8", + sdk_version="0.8.0", +) + +SUPPORTED_VERSIONS: dict[int, ACPVersionSpec] = { + 1: CURRENT_VERSION, +} + +MIN_PROTOCOL_VERSION = 1 + + +def negotiate_version(client_protocol_version: int) -> ACPVersionSpec: + """Negotiate the protocol version with the client. + + Returns the highest server-supported version that does not exceed the + client's requested version. If the client version is lower than + ``MIN_PROTOCOL_VERSION`` the server still returns its own current + version so the client can decide whether to disconnect. + """ + if client_protocol_version < MIN_PROTOCOL_VERSION: + return CURRENT_VERSION + + # Find the highest supported version <= client version + best: ACPVersionSpec | None = None + for ver, spec in SUPPORTED_VERSIONS.items(): + if ver <= client_protocol_version: + if best is None or ver > best.protocol_version: + best = spec + + return best if best is not None else CURRENT_VERSION diff --git a/tests/acp/__init__.py b/tests/acp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/acp/conftest.py b/tests/acp/conftest.py new file mode 100644 index 000000000..b3ec91934 --- /dev/null +++ b/tests/acp/conftest.py @@ -0,0 +1,136 @@ +"""ACP test configuration and fixtures.""" + +from __future__ import annotations + +import json +import os +from collections.abc import AsyncIterator +from pathlib import Path +from typing import Any + +import acp +import pytest +import pytest_asyncio + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def _kimi_bin() -> str: + """Return the path to the kimi entry-point script inside the venv.""" + return str(_repo_root() / ".venv" / "bin" / "kimi") + + +class ACPTestClient: + """Minimal ACP client for tests — collects session_update callbacks.""" + + def __init__(self) -> None: + self.updates: list[Any] = [] + self.conn: acp.Agent | None = None + + def on_connect(self, conn: acp.Agent) -> None: + self.conn = conn + + async def session_update(self, session_id: str, update: Any, **kwargs: Any) -> None: + self.updates.append(update) + + async def request_permission(self, **kwargs: Any) -> Any: + return acp.schema.RequestPermissionResponse( + result=acp.schema.PermissionDecision( + decision="allow", + options=[], + ) + ) + + async def read_text_file(self, **kwargs: Any) -> Any: + raise NotImplementedError + + async def write_text_file(self, **kwargs: Any) -> Any: + raise NotImplementedError + + async def create_terminal(self, **kwargs: Any) -> Any: + raise NotImplementedError + + async def terminal_output(self, **kwargs: Any) -> Any: + raise NotImplementedError + + async def wait_for_terminal_exit(self, **kwargs: Any) -> Any: + raise NotImplementedError + + async def kill_terminal(self, **kwargs: Any) -> Any: + raise NotImplementedError + + async def release_terminal(self, **kwargs: Any) -> Any: + raise NotImplementedError + + async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]: + raise NotImplementedError + + async def ext_notification(self, method: str, params: dict[str, Any]) -> None: + pass + + +@pytest.fixture +def acp_share_dir(tmp_path: Path) -> Path: + """Create a share dir with _scripted_echo config at config.toml.""" + share_dir = tmp_path / "share" + share_dir.mkdir() + + scripts = [ + "text: Hello from scripted echo!", + "text: Second response from scripted echo.", + ] + scripts_path = tmp_path / "scripts.json" + scripts_path.write_text(json.dumps(scripts), encoding="utf-8") + + trace_env = os.getenv("KIMI_SCRIPTED_ECHO_TRACE", "0") + config_data = { + "default_model": "scripted", + "models": { + "scripted": { + "provider": "scripted_provider", + "model": "scripted_echo", + "max_context_size": 100000, + } + }, + "providers": { + "scripted_provider": { + "type": "_scripted_echo", + "base_url": "", + "api_key": "", + "env": { + "KIMI_SCRIPTED_ECHO_SCRIPTS": str(scripts_path), + "KIMI_SCRIPTED_ECHO_TRACE": trace_env, + }, + } + }, + } + + import tomlkit + + config_path = share_dir / "config.toml" + config_path.write_text(tomlkit.dumps(config_data), encoding="utf-8") + return share_dir + + +@pytest_asyncio.fixture +async def acp_client( + acp_share_dir: Path, tmp_path: Path +) -> AsyncIterator[tuple[acp.ClientSideConnection, ACPTestClient]]: + """Spawn a kimi ACP subprocess and return the SDK connection + test client.""" + test_client = ACPTestClient() + env = { + **os.environ, + "KIMI_SHARE_DIR": str(acp_share_dir), + } + + async with acp.spawn_agent_process( + test_client, + _kimi_bin(), + "acp", + env=env, + cwd=str(_repo_root()), + use_unstable_protocol=True, + ) as (conn, process): + yield conn, test_client diff --git a/tests/acp/test_protocol_v1.py b/tests/acp/test_protocol_v1.py new file mode 100644 index 000000000..cdca9c147 --- /dev/null +++ b/tests/acp/test_protocol_v1.py @@ -0,0 +1,115 @@ +"""Protocol V1 consistency tests using the real ACP SDK client.""" + +from __future__ import annotations + +import acp +import pytest + +from kimi_cli.acp.version import CURRENT_VERSION + +from .conftest import ACPTestClient + +pytestmark = pytest.mark.asyncio + + +async def test_initialize_returns_negotiated_version( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], +): + """initialize(protocol_version=1) returns version 1 with expected fields.""" + conn, _ = acp_client + resp = await conn.initialize(protocol_version=1) + + assert resp.protocol_version == 1 + assert resp.agent_capabilities is not None + assert resp.agent_capabilities.prompt_capabilities is not None + assert resp.agent_info is not None + assert resp.agent_info.name == "Kimi Code CLI" + + +async def test_initialize_with_higher_version( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], +): + """initialize(protocol_version=99) returns the server's current max version.""" + conn, _ = acp_client + resp = await conn.initialize(protocol_version=99) + + assert resp.protocol_version == CURRENT_VERSION.protocol_version + + +async def test_new_session_response_shape( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """new_session returns session_id, modes, and models.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + resp = await conn.new_session(cwd=str(work_dir)) + + assert isinstance(resp.session_id, str) + assert len(resp.session_id) > 0 + assert resp.modes is not None + assert resp.models is not None + + +async def test_prompt_with_scripted_echo( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """Full flow: initialize → new_session → prompt returns a valid response.""" + conn, test_client = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + session_resp = await conn.new_session(cwd=str(work_dir)) + + resp = await conn.prompt( + prompt=[acp.text_block("Say hello")], + session_id=session_resp.session_id, + ) + + assert resp.stop_reason in ("end_turn", "max_tokens", "max_turn_requests") + # The scripted echo provider should have sent session updates + assert len(test_client.updates) > 0 + + +async def test_list_sessions( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """After creating a session and prompting, list_sessions returns it.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + session_resp = await conn.new_session(cwd=str(work_dir)) + + # Must prompt first; Session.list() skips empty sessions + await conn.prompt( + prompt=[acp.text_block("Hello")], + session_id=session_resp.session_id, + ) + + list_resp = await conn.list_sessions(cwd=str(work_dir)) + session_ids = [s.session_id for s in list_resp.sessions] + assert session_resp.session_id in session_ids + + +async def test_cancel_session( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """cancel on an idle session completes without error.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + session_resp = await conn.new_session(cwd=str(work_dir)) + + # cancel should not raise + await conn.cancel(session_id=session_resp.session_id) diff --git a/tests/acp/test_version.py b/tests/acp/test_version.py new file mode 100644 index 000000000..ff26b6d4b --- /dev/null +++ b/tests/acp/test_version.py @@ -0,0 +1,56 @@ +"""Unit tests for ACP version negotiation.""" + +from __future__ import annotations + +import dataclasses + +from kimi_cli.acp.version import ( + CURRENT_VERSION, + MIN_PROTOCOL_VERSION, + SUPPORTED_VERSIONS, + negotiate_version, +) + + +def test_negotiate_current_version(): + """Client sends protocol_version=1 → server returns v1.""" + result = negotiate_version(1) + assert result.protocol_version == 1 + assert result is CURRENT_VERSION + + +def test_negotiate_future_version(): + """Client sends protocol_version=99 → server returns the highest supported version.""" + result = negotiate_version(99) + max_supported = max(SUPPORTED_VERSIONS.keys()) + assert result.protocol_version == max_supported + assert result is SUPPORTED_VERSIONS[max_supported] + + +def test_negotiate_zero_version(): + """Client sends protocol_version=0 (below minimum) → server returns CURRENT_VERSION.""" + result = negotiate_version(0) + assert result is CURRENT_VERSION + + +def test_negotiate_negative_version(): + """Client sends negative protocol_version → server returns CURRENT_VERSION.""" + result = negotiate_version(-1) + assert result is CURRENT_VERSION + + +def test_version_spec_immutable(): + """CURRENT_VERSION is a frozen dataclass and cannot be mutated.""" + assert dataclasses.is_dataclass(CURRENT_VERSION) + assert CURRENT_VERSION.__dataclass_params__.frozen # type: ignore[attr-defined] + + +def test_supported_versions_contains_current(): + """SUPPORTED_VERSIONS includes at least the CURRENT_VERSION.""" + assert CURRENT_VERSION.protocol_version in SUPPORTED_VERSIONS + assert SUPPORTED_VERSIONS[CURRENT_VERSION.protocol_version] is CURRENT_VERSION + + +def test_min_protocol_version_consistency(): + """MIN_PROTOCOL_VERSION is the smallest key in SUPPORTED_VERSIONS.""" + assert MIN_PROTOCOL_VERSION == min(SUPPORTED_VERSIONS.keys()) diff --git a/uv.lock b/uv.lock index 036a1a6a5..13cddd88a 100644 --- a/uv.lock +++ b/uv.lock @@ -17,14 +17,14 @@ members = [ [[package]] name = "agent-client-protocol" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/4d/e33e4e997de8fdc6c7154e59490a20c455cd46543b62dab768ae99317046/agent_client_protocol-0.7.0.tar.gz", hash = "sha256:c66811bb804868c4e7728b18b67379bcb0335afba3b1c2ff0fcdfd0c48d93029", size = 64809, upload-time = "2025-12-04T16:17:34.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/a4/26698e0186933b4ab6e814626c99ee52b5522d039b5c94c983ecb3a66eed/agent_client_protocol-0.8.0.tar.gz", hash = "sha256:f9eade29167ff72a10fae7a0a0c1f27436909a790e159fb10265c2874e58d922", size = 68577, upload-time = "2026-02-07T17:08:46.513Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/02/257ea400cfee72a48dabe04ef0a984c496c9687830cf7977b327979e8cd7/agent_client_protocol-0.7.0-py3-none-any.whl", hash = "sha256:71fce4088fe7faa85b30278aecd1d8d6012f03505ae2ee6e312f9e2ba4ea1f4e", size = 52922, upload-time = "2025-12-04T16:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/4b/04/e55a3c549c09c0023cb92c696c7b98d97bb657088f940e34f4bc47d1a49a/agent_client_protocol-0.8.0-py3-none-any.whl", hash = "sha256:2d5712b88b3249dbd6148b24d32c6eb8992e5663f224db6291524ac80cca8037", size = 54362, upload-time = "2026-02-07T17:08:45.575Z" }, ] [[package]] @@ -1255,7 +1255,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "agent-client-protocol", specifier = "==0.7.0" }, + { name = "agent-client-protocol", specifier = "==0.8.0" }, { name = "aiofiles", specifier = ">=24.0,<26.0" }, { name = "aiohttp", specifier = "==3.13.3" }, { name = "batrachian-toad", marker = "python_full_version >= '3.14'", specifier = "==0.5.23" }, From 1ac88faf364f7bba19ef8df93dd28ae71454a9a5 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 11 Feb 2026 14:59:12 +0800 Subject: [PATCH 2/6] feat(acp): implement session/resume (unstable) Extract shared session setup logic into _setup_session and add resume_session method that returns ResumeSessionResponse with modes/models state. Advertise the resume capability in initialize. --- src/kimi_cli/acp/server.py | 52 +++++++++++++++++++++++++++++------ tests/acp/test_protocol_v1.py | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/kimi_cli/acp/server.py b/src/kimi_cli/acp/server.py index 3524e72ce..f16c57720 100644 --- a/src/kimi_cli/acp/server.py +++ b/src/kimi_cli/acp/server.py @@ -73,6 +73,7 @@ async def initialize( mcp_capabilities=acp.schema.McpCapabilities(http=True, sse=False), session_capabilities=acp.schema.SessionCapabilities( list=acp.schema.SessionListCapabilities(), + resume=acp.schema.SessionResumeCapabilities(), ), ), auth_methods=[ @@ -170,17 +171,13 @@ async def new_session( ), ) - async def load_session( - self, cwd: str, mcp_servers: list[MCPServer], session_id: str, **kwargs: Any - ) -> None: - logger.info("Loading session: {id} for working directory: {cwd}", id=session_id, cwd=cwd) + async def _setup_session( + self, cwd: str, session_id: str, mcp_servers: list[MCPServer] + ) -> tuple[ACPSession, _ModelIDConv]: + """Load or resume a session. Shared by load_session and resume_session.""" assert self.conn is not None, "ACP client not connected" assert self.client_capabilities is not None, "ACP connection not initialized" - if session_id in self.sessions: - logger.warning("Session already loaded: {id}", id=session_id) - return - work_dir = KaosPath.unsafe_from_local_path(Path(cwd)) session = await Session.find(work_dir, session_id) if session is None: @@ -209,8 +206,47 @@ async def load_session( cli_instance.soul.runtime, ) + return acp_session, model_id_conv + + async def load_session( + self, cwd: str, mcp_servers: list[MCPServer], session_id: str, **kwargs: Any + ) -> None: + logger.info("Loading session: {id} for working directory: {cwd}", id=session_id, cwd=cwd) + + if session_id in self.sessions: + logger.warning("Session already loaded: {id}", id=session_id) + return + + await self._setup_session(cwd, session_id, mcp_servers) # TODO: replay session history? + async def resume_session( + self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any + ) -> acp.schema.ResumeSessionResponse: + logger.info("Resuming session: {id} for working directory: {cwd}", id=session_id, cwd=cwd) + + if session_id not in self.sessions: + await self._setup_session(cwd, session_id, mcp_servers or []) + + acp_session, model_id_conv = self.sessions[session_id] + config = acp_session.cli.soul.runtime.config + return acp.schema.ResumeSessionResponse( + modes=acp.schema.SessionModeState( + available_modes=[ + acp.schema.SessionMode( + id="default", + name="Default", + description="The default mode.", + ), + ], + current_mode_id="default", + ), + models=acp.schema.SessionModelState( + available_models=_expand_llm_models(config.models), + current_model_id=model_id_conv.to_acp_model_id(), + ), + ) + async def list_sessions( self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any ) -> acp.schema.ListSessionsResponse: diff --git a/tests/acp/test_protocol_v1.py b/tests/acp/test_protocol_v1.py index cdca9c147..70e413caa 100644 --- a/tests/acp/test_protocol_v1.py +++ b/tests/acp/test_protocol_v1.py @@ -99,6 +99,55 @@ async def test_list_sessions( assert session_resp.session_id in session_ids +async def test_resume_session( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """initialize → new_session → prompt → resume_session returns modes and models.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + session_resp = await conn.new_session(cwd=str(work_dir)) + + # Must prompt first so the session is persisted + await conn.prompt( + prompt=[acp.text_block("Hello")], + session_id=session_resp.session_id, + ) + + resume_resp = await conn.resume_session( + cwd=str(work_dir), + session_id=session_resp.session_id, + ) + + assert resume_resp.modes is not None + assert resume_resp.modes.current_mode_id == "default" + assert len(resume_resp.modes.available_modes) > 0 + assert resume_resp.models is not None + assert isinstance(resume_resp.models.current_model_id, str) + assert len(resume_resp.models.available_models) > 0 + + +async def test_resume_session_not_found( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """resume_session with a non-existent session_id raises an error.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + + with pytest.raises(acp.RequestError): + await conn.resume_session( + cwd=str(work_dir), + session_id="non-existent-session-id", + ) + + async def test_cancel_session( acp_client: tuple[acp.ClientSideConnection, ACPTestClient], tmp_path, From 024fd92ef90517793f84fc066b982d68d1d39297 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 11 Feb 2026 14:59:45 +0800 Subject: [PATCH 3/6] docs: update changelog and docs for ACP version negotiation and session/resume --- CHANGELOG.md | 2 ++ docs/en/reference/kimi-web.md | 3 ++- docs/en/release-notes/changelog.md | 2 ++ docs/zh/reference/kimi-web.md | 3 ++- docs/zh/release-notes/changelog.md | 2 ++ src/kimi_cli/acp/version.py | 5 ++--- tests/acp/test_version.py | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 980b90fb7..4f4f03a91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Only write entries that are worth mentioning to users. - Web: Fix Ctrl+C not working in uvicorn web server by restoring default SIGINT handler and terminal state after shell mode exits - Web: Improve session stop handling with proper async cleanup and timeout +- ACP: Add protocol version negotiation framework for client-server compatibility +- ACP: Add session resume method to restore session state (experimental) ## 1.11.0 (2026-02-10) diff --git a/docs/en/reference/kimi-web.md b/docs/en/reference/kimi-web.md index 473e1671c..83edeef9a 100644 --- a/docs/en/reference/kimi-web.md +++ b/docs/en/reference/kimi-web.md @@ -182,12 +182,13 @@ Session search feature added in version 1.5. Directory auto-creation prompt adde Web UI provides a unified prompt toolbar above the input box, displaying various information in collapsible tabs: +- **Context usage**: Shows the current context usage percentage. Hover to view detailed token usage breakdown (including input/output tokens, cache read/write, etc.) - **Activity status**: Shows the current agent state (processing, waiting for approval, etc.) - **Message queue**: Queue follow-up messages while the AI is processing; queued messages are sent automatically when the current response completes - **File changes**: Detects Git repository status, showing the number of new, modified, and deleted files (including untracked files). Click to view a detailed list of changes ::: info Changed -Git diff status bar added in version 1.5. Activity status indicator added in version 1.9. Later versions unified it into the prompt toolbar, integrating activity status, message queue, and file changes. +Git diff status bar added in version 1.5. Activity status indicator added in version 1.9. Version 1.10 unified it into the prompt toolbar. Version 1.11 moved the context usage indicator to the prompt toolbar. ::: ### Open-in functionality diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 2aff926a8..491f167b3 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -6,6 +6,8 @@ This page documents the changes in each Kimi Code CLI release. - Web: Fix Ctrl+C not working in uvicorn web server by restoring default SIGINT handler and terminal state after shell mode exits - Web: Improve session stop handling with proper async cleanup and timeout +- ACP: Add protocol version negotiation framework for client-server compatibility +- ACP: Add session resume method to restore session state (experimental) ## 1.11.0 (2026-02-10) diff --git a/docs/zh/reference/kimi-web.md b/docs/zh/reference/kimi-web.md index 7ab0cbc79..f94beac62 100644 --- a/docs/zh/reference/kimi-web.md +++ b/docs/zh/reference/kimi-web.md @@ -182,12 +182,13 @@ Web UI 提供了便捷的会话管理界面: Web UI 在输入框上方提供统一的提示工具栏,以可折叠标签页的形式展示多种信息: +- **上下文用量**:显示当前上下文的使用百分比,悬停可查看详细的 Token 用量明细(包括输入/输出 Token、缓存读取/写入等) - **活动状态**:显示 Agent 当前状态(处理中、等待审批等) - **消息队列**:在 AI 处理过程中可以排队发送后续消息,待当前回复完成后自动发送 - **文件变更**:检测 Git 仓库状态,显示新增、修改和删除的文件数量(包含未跟踪文件),点击可查看详细的变更列表 ::: info 变更 -Git diff 状态栏新增于 1.5 版本。1.9 版本添加了活动状态指示器。后续版本将其统一为提示工具栏,整合活动状态、消息队列和文件变更。 +Git diff 状态栏新增于 1.5 版本。1.9 版本添加了活动状态指示器。1.10 版本将其统一为提示工具栏。1.11 版本将上下文用量指示器移至提示工具栏。 ::: ### Open-in 功能 diff --git a/docs/zh/release-notes/changelog.md b/docs/zh/release-notes/changelog.md index 711cf7d58..23199e2bb 100644 --- a/docs/zh/release-notes/changelog.md +++ b/docs/zh/release-notes/changelog.md @@ -6,6 +6,8 @@ - Web:修复在 uvicorn Web 服务器中 Ctrl+C 无法使用的问题,在 Shell 模式退出后恢复默认的 SIGINT 信号处理程序和终端状态 - Web:改进会话停止处理,使用正确的异步清理和超时机制 +- ACP:添加协议版本协商框架,用于客户端与服务端之间的兼容性校验 +- ACP:添加会话恢复方法,用于恢复会话状态(实验性) ## 1.11.0 (2026-02-10) diff --git a/src/kimi_cli/acp/version.py b/src/kimi_cli/acp/version.py index bdbe91381..6c51cd0d3 100644 --- a/src/kimi_cli/acp/version.py +++ b/src/kimi_cli/acp/version.py @@ -39,8 +39,7 @@ def negotiate_version(client_protocol_version: int) -> ACPVersionSpec: # Find the highest supported version <= client version best: ACPVersionSpec | None = None for ver, spec in SUPPORTED_VERSIONS.items(): - if ver <= client_protocol_version: - if best is None or ver > best.protocol_version: - best = spec + if ver <= client_protocol_version and (best is None or ver > best.protocol_version): + best = spec return best if best is not None else CURRENT_VERSION diff --git a/tests/acp/test_version.py b/tests/acp/test_version.py index ff26b6d4b..2b3861870 100644 --- a/tests/acp/test_version.py +++ b/tests/acp/test_version.py @@ -53,4 +53,4 @@ def test_supported_versions_contains_current(): def test_min_protocol_version_consistency(): """MIN_PROTOCOL_VERSION is the smallest key in SUPPORTED_VERSIONS.""" - assert MIN_PROTOCOL_VERSION == min(SUPPORTED_VERSIONS.keys()) + assert min(SUPPORTED_VERSIONS.keys()) == MIN_PROTOCOL_VERSION From dbe9d074370e2a81646cd7fecfb431cad26bb72e Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 11 Feb 2026 15:13:09 +0800 Subject: [PATCH 4/6] fix(acp): align Agent/Client protocol signatures with SDK v0.8.0 - Fix new_session/load_session parameter order and defaults to match Agent protocol - Add fork_session stub to ACPServer and ACPServerSingleSession - Add resume_session stub to ACPServerSingleSession - Update ACPTestClient to match Client protocol signatures - Fix RequestPermissionResponse to use new outcome API --- src/kimi_cli/acp/server.py | 20 ++++++++++----- src/kimi_cli/ui/acp/__init__.py | 14 +++++++++-- tests/acp/conftest.py | 44 ++++++++++++++++++++++++--------- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/kimi_cli/acp/server.py b/src/kimi_cli/acp/server.py index f16c57720..70231f9ed 100644 --- a/src/kimi_cli/acp/server.py +++ b/src/kimi_cli/acp/server.py @@ -112,7 +112,7 @@ async def initialize( ) async def new_session( - self, cwd: str, mcp_servers: list[MCPServer], **kwargs: Any + self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any ) -> acp.NewSessionResponse: logger.info("Creating new session for working directory: {cwd}", cwd=cwd) assert self.conn is not None, "ACP client not connected" @@ -120,7 +120,7 @@ async def new_session( session = await Session.create(KaosPath.unsafe_from_local_path(Path(cwd))) - mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers) + mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers or []) cli_instance = await KimiCLI.create( session, mcp_configs=[mcp_config], @@ -172,7 +172,10 @@ async def new_session( ) async def _setup_session( - self, cwd: str, session_id: str, mcp_servers: list[MCPServer] + self, + cwd: str, + session_id: str, + mcp_servers: list[MCPServer] | None = None, ) -> tuple[ACPSession, _ModelIDConv]: """Load or resume a session. Shared by load_session and resume_session.""" assert self.conn is not None, "ACP client not connected" @@ -186,7 +189,7 @@ async def _setup_session( ) raise acp.RequestError.invalid_params({"session_id": "Session not found"}) - mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers) + mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers or []) cli_instance = await KimiCLI.create( session, mcp_configs=[mcp_config], @@ -209,7 +212,7 @@ async def _setup_session( return acp_session, model_id_conv async def load_session( - self, cwd: str, mcp_servers: list[MCPServer], session_id: str, **kwargs: Any + self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any ) -> None: logger.info("Loading session: {id} for working directory: {cwd}", id=session_id, cwd=cwd) @@ -226,7 +229,7 @@ async def resume_session( logger.info("Resuming session: {id} for working directory: {cwd}", id=session_id, cwd=cwd) if session_id not in self.sessions: - await self._setup_session(cwd, session_id, mcp_servers or []) + await self._setup_session(cwd, session_id, mcp_servers) acp_session, model_id_conv = self.sessions[session_id] config = acp_session.cli.soul.runtime.config @@ -247,6 +250,11 @@ async def resume_session( ), ) + async def fork_session( + self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any + ) -> acp.schema.ForkSessionResponse: + raise NotImplementedError + async def list_sessions( self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any ) -> acp.schema.ListSessionsResponse: diff --git a/src/kimi_cli/ui/acp/__init__.py b/src/kimi_cli/ui/acp/__init__.py index 6a6c58249..b71668bdc 100644 --- a/src/kimi_cli/ui/acp/__init__.py +++ b/src/kimi_cli/ui/acp/__init__.py @@ -35,15 +35,25 @@ async def initialize( self._raise() async def new_session( - self, cwd: str, mcp_servers: list[MCPServer], **kwargs: Any + self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any ) -> acp.NewSessionResponse: self._raise() async def load_session( - self, cwd: str, mcp_servers: list[MCPServer], session_id: str, **kwargs: Any + self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any ) -> None: self._raise() + async def resume_session( + self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any + ) -> acp.schema.ResumeSessionResponse: + self._raise() + + async def fork_session( + self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any + ) -> acp.schema.ForkSessionResponse: + self._raise() + async def list_sessions( self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any ) -> acp.schema.ListSessionsResponse: diff --git a/tests/acp/conftest.py b/tests/acp/conftest.py index b3ec91934..b12e4ea65 100644 --- a/tests/acp/conftest.py +++ b/tests/acp/conftest.py @@ -35,33 +35,55 @@ def on_connect(self, conn: acp.Agent) -> None: async def session_update(self, session_id: str, update: Any, **kwargs: Any) -> None: self.updates.append(update) - async def request_permission(self, **kwargs: Any) -> Any: + async def request_permission( + self, + options: list[acp.schema.PermissionOption], + session_id: str, + tool_call: acp.schema.ToolCallUpdate, + **kwargs: Any, + ) -> acp.schema.RequestPermissionResponse: return acp.schema.RequestPermissionResponse( - result=acp.schema.PermissionDecision( - decision="allow", - options=[], + outcome=acp.schema.AllowedOutcome( + outcome="selected", + option_id="allow", ) ) - async def read_text_file(self, **kwargs: Any) -> Any: + async def read_text_file( + self, + path: str, + session_id: str, + limit: int | None = None, + line: int | None = None, + **kwargs: Any, + ) -> Any: raise NotImplementedError - async def write_text_file(self, **kwargs: Any) -> Any: + async def write_text_file(self, content: str, path: str, session_id: str, **kwargs: Any) -> Any: raise NotImplementedError - async def create_terminal(self, **kwargs: Any) -> Any: + async def create_terminal( + self, + command: str, + session_id: str, + args: list[str] | None = None, + cwd: str | None = None, + env: list[acp.schema.EnvVariable] | None = None, + output_byte_limit: int | None = None, + **kwargs: Any, + ) -> Any: raise NotImplementedError - async def terminal_output(self, **kwargs: Any) -> Any: + async def terminal_output(self, session_id: str, terminal_id: str, **kwargs: Any) -> Any: raise NotImplementedError - async def wait_for_terminal_exit(self, **kwargs: Any) -> Any: + async def wait_for_terminal_exit(self, session_id: str, terminal_id: str, **kwargs: Any) -> Any: raise NotImplementedError - async def kill_terminal(self, **kwargs: Any) -> Any: + async def kill_terminal(self, session_id: str, terminal_id: str, **kwargs: Any) -> Any: raise NotImplementedError - async def release_terminal(self, **kwargs: Any) -> Any: + async def release_terminal(self, session_id: str, terminal_id: str, **kwargs: Any) -> Any: raise NotImplementedError async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]: From 4dc271ada63613b50292e1a1ef7f5b569ebae9b0 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 11 Feb 2026 18:34:27 +0800 Subject: [PATCH 5/6] feat(acp): implement session/fork (unstable) Extract _init_session from new_session/_setup_session to share session initialization logic, then implement fork_session by copying source session's context.jsonl/wire.jsonl into a new session directory. Advertise fork capability in initialize response. --- src/kimi_cli/acp/server.py | 91 +++++++++++++++++++++++------------ tests/acp/test_protocol_v1.py | 79 ++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 31 deletions(-) diff --git a/src/kimi_cli/acp/server.py b/src/kimi_cli/acp/server.py index 70231f9ed..c9088dfd1 100644 --- a/src/kimi_cli/acp/server.py +++ b/src/kimi_cli/acp/server.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import shutil import sys from datetime import datetime from pathlib import Path @@ -72,6 +73,7 @@ async def initialize( ), mcp_capabilities=acp.schema.McpCapabilities(http=True, sse=False), session_capabilities=acp.schema.SessionCapabilities( + fork=acp.schema.SessionForkCapabilities(), list=acp.schema.SessionListCapabilities(), resume=acp.schema.SessionResumeCapabilities(), ), @@ -111,15 +113,13 @@ async def initialize( agent_info=acp.schema.Implementation(name=NAME, version=VERSION), ) - async def new_session( - self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any - ) -> acp.NewSessionResponse: - logger.info("Creating new session for working directory: {cwd}", cwd=cwd) + async def _init_session( + self, session: Session, mcp_servers: list[MCPServer] | None = None + ) -> tuple[ACPSession, _ModelIDConv]: + """Initialize a KimiCLI instance for a session and register it.""" assert self.conn is not None, "ACP client not connected" assert self.client_capabilities is not None, "ACP connection not initialized" - session = await Session.create(KaosPath.unsafe_from_local_path(Path(cwd))) - mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers or []) cli_instance = await KimiCLI.create( session, @@ -140,6 +140,18 @@ async def new_session( cli_instance.soul.runtime, ) + return acp_session, model_id_conv + + async def new_session( + self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any + ) -> acp.NewSessionResponse: + logger.info("Creating new session for working directory: {cwd}", cwd=cwd) + assert self.conn is not None + + session = await Session.create(KaosPath.unsafe_from_local_path(Path(cwd))) + acp_session, model_id_conv = await self._init_session(session, mcp_servers) + config = acp_session.cli.soul.runtime.config + available_commands = [ acp.schema.AvailableCommand(name=cmd.name, description=cmd.description) for cmd in soul_slash_registry.list_commands() @@ -178,9 +190,6 @@ async def _setup_session( mcp_servers: list[MCPServer] | None = None, ) -> tuple[ACPSession, _ModelIDConv]: """Load or resume a session. Shared by load_session and resume_session.""" - assert self.conn is not None, "ACP client not connected" - assert self.client_capabilities is not None, "ACP connection not initialized" - work_dir = KaosPath.unsafe_from_local_path(Path(cwd)) session = await Session.find(work_dir, session_id) if session is None: @@ -189,27 +198,7 @@ async def _setup_session( ) raise acp.RequestError.invalid_params({"session_id": "Session not found"}) - mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers or []) - cli_instance = await KimiCLI.create( - session, - mcp_configs=[mcp_config], - ) - config = cli_instance.soul.runtime.config - acp_kaos = ACPKaos(self.conn, session.id, self.client_capabilities) - acp_session = ACPSession(session.id, cli_instance, self.conn, kaos=acp_kaos) - model_id_conv = _ModelIDConv(config.default_model, config.default_thinking) - self.sessions[session.id] = (acp_session, model_id_conv) - - if isinstance(cli_instance.soul.agent.toolset, KimiToolset): - replace_tools( - self.client_capabilities, - self.conn, - session.id, - cli_instance.soul.agent.toolset, - cli_instance.soul.runtime, - ) - - return acp_session, model_id_conv + return await self._init_session(session, mcp_servers) async def load_session( self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any @@ -253,7 +242,47 @@ async def resume_session( async def fork_session( self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any ) -> acp.schema.ForkSessionResponse: - raise NotImplementedError + logger.info("Forking session: {id} for working directory: {cwd}", id=session_id, cwd=cwd) + + work_dir = KaosPath.unsafe_from_local_path(Path(cwd)) + source_session = await Session.find(work_dir, session_id) + if source_session is None: + logger.error( + "Source session not found: {id} for working directory: {cwd}", + id=session_id, + cwd=cwd, + ) + raise acp.RequestError.invalid_params({"session_id": "Session not found"}) + + new_session = await Session.create(work_dir) + + # Copy context and wire files from source to new session + for filename in ("context.jsonl", "wire.jsonl"): + src_file = source_session.dir / filename + dst_file = new_session.dir / filename + if src_file.exists(): + shutil.copy2(src_file, dst_file) + + acp_session, model_id_conv = await self._init_session(new_session, mcp_servers) + config = acp_session.cli.soul.runtime.config + + return acp.schema.ForkSessionResponse( + session_id=new_session.id, + modes=acp.schema.SessionModeState( + available_modes=[ + acp.schema.SessionMode( + id="default", + name="Default", + description="The default mode.", + ), + ], + current_mode_id="default", + ), + models=acp.schema.SessionModelState( + available_models=_expand_llm_models(config.models), + current_model_id=model_id_conv.to_acp_model_id(), + ), + ) async def list_sessions( self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any diff --git a/tests/acp/test_protocol_v1.py b/tests/acp/test_protocol_v1.py index 70e413caa..225857b6c 100644 --- a/tests/acp/test_protocol_v1.py +++ b/tests/acp/test_protocol_v1.py @@ -148,6 +148,85 @@ async def test_resume_session_not_found( ) +async def test_fork_session( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """fork_session returns a new session_id with modes and models.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + session_resp = await conn.new_session(cwd=str(work_dir)) + + # Prompt so the session has history to fork + await conn.prompt( + prompt=[acp.text_block("Hello")], + session_id=session_resp.session_id, + ) + + fork_resp = await conn.fork_session( + cwd=str(work_dir), + session_id=session_resp.session_id, + ) + + assert isinstance(fork_resp.session_id, str) + assert fork_resp.session_id != session_resp.session_id + assert fork_resp.modes is not None + assert fork_resp.models is not None + + +async def test_fork_session_not_found( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """fork_session with a non-existent session_id raises an error.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + + with pytest.raises(acp.RequestError): + await conn.fork_session( + cwd=str(work_dir), + session_id="non-existent-session-id", + ) + + +async def test_fork_session_prompt( + acp_client: tuple[acp.ClientSideConnection, ACPTestClient], + tmp_path, +): + """After forking, the new session can handle prompts normally.""" + conn, _ = acp_client + await conn.initialize(protocol_version=1) + + work_dir = tmp_path / "workdir" + work_dir.mkdir(exist_ok=True) + session_resp = await conn.new_session(cwd=str(work_dir)) + + # Prompt the original session + await conn.prompt( + prompt=[acp.text_block("Hello")], + session_id=session_resp.session_id, + ) + + fork_resp = await conn.fork_session( + cwd=str(work_dir), + session_id=session_resp.session_id, + ) + + # Prompt on the forked session should work + prompt_resp = await conn.prompt( + prompt=[acp.text_block("Hi from forked session")], + session_id=fork_resp.session_id, + ) + + assert prompt_resp.stop_reason in ("end_turn", "max_tokens", "max_turn_requests") + + async def test_cancel_session( acp_client: tuple[acp.ClientSideConnection, ACPTestClient], tmp_path, From e3c3ef002cca002bc17c4a1426394ed51f106db6 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 11 Feb 2026 18:53:22 +0800 Subject: [PATCH 6/6] docs: update changelog and docs for ACP session/fork --- CHANGELOG.md | 1 + docs/en/release-notes/changelog.md | 1 + docs/zh/configuration/data-locations.md | 1 - docs/zh/configuration/env-vars.md | 1 - docs/zh/configuration/overrides.md | 1 - docs/zh/configuration/providers.md | 1 - docs/zh/guides/interaction.md | 1 - docs/zh/guides/use-cases.md | 1 - docs/zh/release-notes/changelog.md | 1 + 9 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4346607..73ca9bd17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Only write entries that are worth mentioning to users. - Web: Improve session stop handling with proper async cleanup and timeout - ACP: Add protocol version negotiation framework for client-server compatibility - ACP: Add session resume method to restore session state (experimental) +- ACP: Add session fork method to clone session state (experimental) ## 1.11.0 (2026-02-10) diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 3f95a9d3b..35e9b5ba4 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -9,6 +9,7 @@ This page documents the changes in each Kimi Code CLI release. - Web: Improve session stop handling with proper async cleanup and timeout - ACP: Add protocol version negotiation framework for client-server compatibility - ACP: Add session resume method to restore session state (experimental) +- ACP: Add session fork method to clone session state (experimental) ## 1.11.0 (2026-02-10) diff --git a/docs/zh/configuration/data-locations.md b/docs/zh/configuration/data-locations.md index 12686b703..fe89bf1a8 100644 --- a/docs/zh/configuration/data-locations.md +++ b/docs/zh/configuration/data-locations.md @@ -112,4 +112,3 @@ Wire 消息记录文件,以 JSONL 格式存储会话中的 Wire 事件。用 | 清理日志 | 删除 `~/.kimi/logs/` 目录 | | 清理 MCP 配置 | 删除 `~/.kimi/mcp.json` 或使用 `kimi mcp remove` | | 清理登录凭据 | 删除 `~/.kimi/credentials/` 目录或使用 `/logout` | - diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index 93e56b514..7c2098122 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -140,4 +140,3 @@ export KIMI_CLI_NO_AUTO_UPDATE="1" ::: tip 提示 如果你通过 Nix 或其他包管理器安装 Kimi Code CLI,通常会自动设置此环境变量,因为更新由包管理器处理。 ::: - diff --git a/docs/zh/configuration/overrides.md b/docs/zh/configuration/overrides.md index a4dfdaa78..0ab39b68d 100644 --- a/docs/zh/configuration/overrides.md +++ b/docs/zh/configuration/overrides.md @@ -87,4 +87,3 @@ max_context_size = 262144 | `KIMI_API_KEY=sk-env kimi` | 配置文件 | 环境变量 | 配置文件 | | `kimi --model other` | 配置文件 | 配置文件 | CLI 参数 | | `KIMI_MODEL_NAME=k2 kimi` | 配置文件 | 配置文件 | 环境变量 | - diff --git a/docs/zh/configuration/providers.md b/docs/zh/configuration/providers.md index 7e5c34655..028d1c80c 100644 --- a/docs/zh/configuration/providers.md +++ b/docs/zh/configuration/providers.md @@ -149,4 +149,3 @@ capabilities = ["thinking", "image_in"] | `moonshot_fetch` | `FetchURL` | 回退到本地抓取 | 使用其他平台时,`FetchURL` 工具仍可使用,但会回退到本地抓取。 - diff --git a/docs/zh/guides/interaction.md b/docs/zh/guides/interaction.md index 5ac9ea48b..3c90bb028 100644 --- a/docs/zh/guides/interaction.md +++ b/docs/zh/guides/interaction.md @@ -96,4 +96,3 @@ kimi --yolo ::: warning 注意 YOLO 模式会跳过所有确认,请确保你了解可能的风险。建议仅在可控环境中使用。 ::: - diff --git a/docs/zh/guides/use-cases.md b/docs/zh/guides/use-cases.md index 43010276b..790a2e0b7 100644 --- a/docs/zh/guides/use-cases.md +++ b/docs/zh/guides/use-cases.md @@ -110,4 +110,3 @@ Kimi Code CLI 可以执行各种重复性的小任务: ``` 把 images 目录下的所有 PNG 图片转换为 JPEG 格式,保存到 output 目录 ``` - diff --git a/docs/zh/release-notes/changelog.md b/docs/zh/release-notes/changelog.md index b541a0d9b..ed2d8f2e7 100644 --- a/docs/zh/release-notes/changelog.md +++ b/docs/zh/release-notes/changelog.md @@ -9,6 +9,7 @@ - Web:改进会话停止处理,使用正确的异步清理和超时机制 - ACP:添加协议版本协商框架,用于客户端与服务端之间的兼容性校验 - ACP:添加会话恢复方法,用于恢复会话状态(实验性) +- ACP:添加会话分支(fork)方法,用于克隆会话状态(实验性) ## 1.11.0 (2026-02-10)