bhu-cli 1.46.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bhu_cli-1.46.0.dist-info/METADATA +210 -0
- bhu_cli-1.46.0.dist-info/RECORD +258 -0
- bhu_cli-1.46.0.dist-info/WHEEL +4 -0
- bhu_cli-1.46.0.dist-info/entry_points.txt +4 -0
- kimi_cli/CHANGELOG.md +1281 -0
- kimi_cli/__init__.py +29 -0
- kimi_cli/__main__.py +43 -0
- kimi_cli/_build_info.py +1 -0
- kimi_cli/acp/AGENTS.md +92 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +128 -0
- kimi_cli/acp/kaos.py +291 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +468 -0
- kimi_cli/acp/session.py +499 -0
- kimi_cli/acp/tools.py +167 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/acp/version.py +45 -0
- kimi_cli/agents/default/agent.yaml +36 -0
- kimi_cli/agents/default/coder.yaml +25 -0
- kimi_cli/agents/default/explore.yaml +46 -0
- kimi_cli/agents/default/plan.yaml +30 -0
- kimi_cli/agents/default/system.md +160 -0
- kimi_cli/agents/okabe/agent.yaml +22 -0
- kimi_cli/agentspec.py +160 -0
- kimi_cli/app.py +807 -0
- kimi_cli/approval_runtime/__init__.py +29 -0
- kimi_cli/approval_runtime/models.py +42 -0
- kimi_cli/approval_runtime/runtime.py +235 -0
- kimi_cli/auth/__init__.py +5 -0
- kimi_cli/auth/oauth.py +1092 -0
- kimi_cli/auth/platforms.py +374 -0
- kimi_cli/background/__init__.py +36 -0
- kimi_cli/background/agent_runner.py +231 -0
- kimi_cli/background/ids.py +19 -0
- kimi_cli/background/manager.py +725 -0
- kimi_cli/background/models.py +105 -0
- kimi_cli/background/store.py +237 -0
- kimi_cli/background/summary.py +66 -0
- kimi_cli/background/worker.py +209 -0
- kimi_cli/cli/__init__.py +1081 -0
- kimi_cli/cli/__main__.py +34 -0
- kimi_cli/cli/_lazy_group.py +238 -0
- kimi_cli/cli/export.py +322 -0
- kimi_cli/cli/info.py +62 -0
- kimi_cli/cli/mcp.py +353 -0
- kimi_cli/cli/plugin.py +347 -0
- kimi_cli/cli/toad.py +73 -0
- kimi_cli/cli/vis.py +38 -0
- kimi_cli/cli/web.py +80 -0
- kimi_cli/config.py +429 -0
- kimi_cli/constant.py +116 -0
- kimi_cli/exception.py +43 -0
- kimi_cli/hooks/__init__.py +24 -0
- kimi_cli/hooks/config.py +34 -0
- kimi_cli/hooks/defaults/rtk.py +48 -0
- kimi_cli/hooks/engine.py +394 -0
- kimi_cli/hooks/events.py +190 -0
- kimi_cli/hooks/runner.py +103 -0
- kimi_cli/llm.py +333 -0
- kimi_cli/mcp_oauth.py +72 -0
- kimi_cli/metadata.py +79 -0
- kimi_cli/notifications/__init__.py +33 -0
- kimi_cli/notifications/llm.py +77 -0
- kimi_cli/notifications/manager.py +145 -0
- kimi_cli/notifications/models.py +50 -0
- kimi_cli/notifications/notifier.py +41 -0
- kimi_cli/notifications/store.py +118 -0
- kimi_cli/notifications/wire.py +21 -0
- kimi_cli/plugin/__init__.py +124 -0
- kimi_cli/plugin/manager.py +153 -0
- kimi_cli/plugin/tool.py +173 -0
- kimi_cli/prompts/__init__.py +6 -0
- kimi_cli/prompts/compact.md +73 -0
- kimi_cli/prompts/init.md +21 -0
- kimi_cli/py.typed +0 -0
- kimi_cli/session.py +319 -0
- kimi_cli/session_fork.py +325 -0
- kimi_cli/session_state.py +132 -0
- kimi_cli/share.py +14 -0
- kimi_cli/skill/__init__.py +727 -0
- kimi_cli/skill/flow/__init__.py +99 -0
- kimi_cli/skill/flow/d2.py +482 -0
- kimi_cli/skill/flow/mermaid.py +266 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +367 -0
- kimi_cli/soul/__init__.py +304 -0
- kimi_cli/soul/agent.py +519 -0
- kimi_cli/soul/approval.py +267 -0
- kimi_cli/soul/btw.py +237 -0
- kimi_cli/soul/compaction.py +189 -0
- kimi_cli/soul/context.py +339 -0
- kimi_cli/soul/denwarenji.py +39 -0
- kimi_cli/soul/dynamic_injection.py +84 -0
- kimi_cli/soul/dynamic_injections/__init__.py +0 -0
- kimi_cli/soul/dynamic_injections/afk_mode.py +74 -0
- kimi_cli/soul/dynamic_injections/plan_mode.py +245 -0
- kimi_cli/soul/kimisoul.py +1710 -0
- kimi_cli/soul/message.py +92 -0
- kimi_cli/soul/slash.py +341 -0
- kimi_cli/soul/toolset.py +891 -0
- kimi_cli/subagents/__init__.py +21 -0
- kimi_cli/subagents/builder.py +42 -0
- kimi_cli/subagents/core.py +86 -0
- kimi_cli/subagents/git_context.py +170 -0
- kimi_cli/subagents/models.py +54 -0
- kimi_cli/subagents/output.py +71 -0
- kimi_cli/subagents/registry.py +28 -0
- kimi_cli/subagents/runner.py +428 -0
- kimi_cli/subagents/store.py +196 -0
- kimi_cli/telemetry/__init__.py +197 -0
- kimi_cli/telemetry/crash.py +149 -0
- kimi_cli/telemetry/sink.py +143 -0
- kimi_cli/telemetry/transport.py +318 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +105 -0
- kimi_cli/tools/agent/__init__.py +277 -0
- kimi_cli/tools/agent/description.md +41 -0
- kimi_cli/tools/ask_user/__init__.py +154 -0
- kimi_cli/tools/ask_user/description.md +19 -0
- kimi_cli/tools/background/__init__.py +318 -0
- kimi_cli/tools/background/list.md +10 -0
- kimi_cli/tools/background/output.md +11 -0
- kimi_cli/tools/background/stop.md +8 -0
- kimi_cli/tools/display.py +46 -0
- kimi_cli/tools/dmail/__init__.py +38 -0
- kimi_cli/tools/dmail/dmail.md +17 -0
- kimi_cli/tools/file/__init__.py +30 -0
- kimi_cli/tools/file/glob.md +21 -0
- kimi_cli/tools/file/glob.py +178 -0
- kimi_cli/tools/file/grep.md +6 -0
- kimi_cli/tools/file/grep_local.py +590 -0
- kimi_cli/tools/file/plan_mode.py +45 -0
- kimi_cli/tools/file/read.md +16 -0
- kimi_cli/tools/file/read.py +300 -0
- kimi_cli/tools/file/read_media.md +24 -0
- kimi_cli/tools/file/read_media.py +217 -0
- kimi_cli/tools/file/replace.md +7 -0
- kimi_cli/tools/file/replace.py +195 -0
- kimi_cli/tools/file/utils.py +257 -0
- kimi_cli/tools/file/write.md +5 -0
- kimi_cli/tools/file/write.py +177 -0
- kimi_cli/tools/plan/__init__.py +339 -0
- kimi_cli/tools/plan/description.md +29 -0
- kimi_cli/tools/plan/enter.py +197 -0
- kimi_cli/tools/plan/enter_description.md +35 -0
- kimi_cli/tools/plan/heroes.py +277 -0
- kimi_cli/tools/shell/__init__.py +260 -0
- kimi_cli/tools/shell/bash.md +35 -0
- kimi_cli/tools/test.py +55 -0
- kimi_cli/tools/think/__init__.py +21 -0
- kimi_cli/tools/think/think.md +1 -0
- kimi_cli/tools/todo/__init__.py +168 -0
- kimi_cli/tools/todo/set_todo_list.md +23 -0
- kimi_cli/tools/utils.py +199 -0
- kimi_cli/tools/web/__init__.py +4 -0
- kimi_cli/tools/web/fetch.md +1 -0
- kimi_cli/tools/web/fetch.py +189 -0
- kimi_cli/tools/web/search.md +1 -0
- kimi_cli/tools/web/search.py +163 -0
- kimi_cli/ui/__init__.py +0 -0
- kimi_cli/ui/acp/__init__.py +99 -0
- kimi_cli/ui/print/__init__.py +474 -0
- kimi_cli/ui/print/visualize.py +194 -0
- kimi_cli/ui/shell/__init__.py +1540 -0
- kimi_cli/ui/shell/console.py +109 -0
- kimi_cli/ui/shell/debug.py +190 -0
- kimi_cli/ui/shell/echo.py +17 -0
- kimi_cli/ui/shell/export_import.py +117 -0
- kimi_cli/ui/shell/keyboard.py +300 -0
- kimi_cli/ui/shell/mcp_status.py +111 -0
- kimi_cli/ui/shell/oauth.py +149 -0
- kimi_cli/ui/shell/placeholders.py +531 -0
- kimi_cli/ui/shell/prompt.py +2259 -0
- kimi_cli/ui/shell/replay.py +215 -0
- kimi_cli/ui/shell/session_picker.py +227 -0
- kimi_cli/ui/shell/setup.py +212 -0
- kimi_cli/ui/shell/slash.py +893 -0
- kimi_cli/ui/shell/startup.py +32 -0
- kimi_cli/ui/shell/task_browser.py +486 -0
- kimi_cli/ui/shell/update.py +349 -0
- kimi_cli/ui/shell/usage.py +295 -0
- kimi_cli/ui/shell/visualize/__init__.py +165 -0
- kimi_cli/ui/shell/visualize/_approval_panel.py +505 -0
- kimi_cli/ui/shell/visualize/_blocks.py +640 -0
- kimi_cli/ui/shell/visualize/_btw_panel.py +224 -0
- kimi_cli/ui/shell/visualize/_input_router.py +48 -0
- kimi_cli/ui/shell/visualize/_interactive.py +524 -0
- kimi_cli/ui/shell/visualize/_live_view.py +902 -0
- kimi_cli/ui/shell/visualize/_question_panel.py +586 -0
- kimi_cli/ui/theme.py +241 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +24 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +108 -0
- kimi_cli/utils/clipboard.py +246 -0
- kimi_cli/utils/datetime.py +64 -0
- kimi_cli/utils/diff.py +135 -0
- kimi_cli/utils/editor.py +91 -0
- kimi_cli/utils/environment.py +225 -0
- kimi_cli/utils/envvar.py +22 -0
- kimi_cli/utils/export.py +696 -0
- kimi_cli/utils/file_filter.py +367 -0
- kimi_cli/utils/frontmatter.py +70 -0
- kimi_cli/utils/io.py +27 -0
- kimi_cli/utils/logging.py +124 -0
- kimi_cli/utils/media_tags.py +29 -0
- kimi_cli/utils/message.py +24 -0
- kimi_cli/utils/path.py +245 -0
- kimi_cli/utils/proctitle.py +33 -0
- kimi_cli/utils/proxy.py +31 -0
- kimi_cli/utils/pyinstaller.py +43 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/diff_render.py +481 -0
- kimi_cli/utils/rich/markdown.py +898 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/rich/syntax.py +114 -0
- kimi_cli/utils/sensitive.py +54 -0
- kimi_cli/utils/server.py +121 -0
- kimi_cli/utils/shell_quoting.py +38 -0
- kimi_cli/utils/signals.py +43 -0
- kimi_cli/utils/slashcmd.py +145 -0
- kimi_cli/utils/string.py +41 -0
- kimi_cli/utils/subprocess_env.py +73 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/utils/windows_paths.py +48 -0
- kimi_cli/vis/__init__.py +0 -0
- kimi_cli/vis/api/__init__.py +5 -0
- kimi_cli/vis/api/sessions.py +687 -0
- kimi_cli/vis/api/statistics.py +209 -0
- kimi_cli/vis/api/system.py +19 -0
- kimi_cli/vis/app.py +175 -0
- kimi_cli/web/__init__.py +5 -0
- kimi_cli/web/api/__init__.py +15 -0
- kimi_cli/web/api/config.py +208 -0
- kimi_cli/web/api/open_in.py +197 -0
- kimi_cli/web/api/sessions.py +1223 -0
- kimi_cli/web/app.py +451 -0
- kimi_cli/web/auth.py +191 -0
- kimi_cli/web/models.py +98 -0
- kimi_cli/web/runner/__init__.py +5 -0
- kimi_cli/web/runner/messages.py +57 -0
- kimi_cli/web/runner/process.py +754 -0
- kimi_cli/web/runner/worker.py +95 -0
- kimi_cli/web/store/__init__.py +1 -0
- kimi_cli/web/store/sessions.py +432 -0
- kimi_cli/wire/__init__.py +148 -0
- kimi_cli/wire/file.py +151 -0
- kimi_cli/wire/jsonrpc.py +263 -0
- kimi_cli/wire/protocol.py +2 -0
- kimi_cli/wire/root_hub.py +27 -0
- kimi_cli/wire/serde.py +26 -0
- kimi_cli/wire/server.py +1060 -0
- kimi_cli/wire/types.py +717 -0
kimi_cli/app.py
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import dataclasses
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import warnings
|
|
9
|
+
from collections.abc import AsyncGenerator, Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
import kaos
|
|
14
|
+
from kaos.path import KaosPath
|
|
15
|
+
from pydantic import SecretStr
|
|
16
|
+
|
|
17
|
+
from kimi_cli.agentspec import DEFAULT_AGENT_FILE
|
|
18
|
+
from kimi_cli.auth.oauth import KIMI_CODE_OAUTH_KEY, OAuthManager, get_device_id
|
|
19
|
+
from kimi_cli.background.models import is_terminal_status
|
|
20
|
+
from kimi_cli.cli import InputFormat, OutputFormat
|
|
21
|
+
from kimi_cli.config import Config, LLMModel, LLMProvider, load_config
|
|
22
|
+
from kimi_cli.constant import VERSION
|
|
23
|
+
from kimi_cli.llm import augment_provider_with_env_vars, create_llm, model_display_name
|
|
24
|
+
from kimi_cli.session import Session
|
|
25
|
+
from kimi_cli.share import get_share_dir
|
|
26
|
+
from kimi_cli.soul import RunCancelled, run_soul
|
|
27
|
+
from kimi_cli.soul.agent import Runtime, load_agent
|
|
28
|
+
from kimi_cli.soul.context import Context
|
|
29
|
+
from kimi_cli.soul.kimisoul import KimiSoul
|
|
30
|
+
from kimi_cli.utils.aioqueue import QueueShutDown
|
|
31
|
+
from kimi_cli.utils.envvar import get_env_bool
|
|
32
|
+
from kimi_cli.utils.logging import logger, open_original_stderr, redirect_stderr_to_logger
|
|
33
|
+
from kimi_cli.utils.path import shorten_home
|
|
34
|
+
from kimi_cli.wire import Wire, WireUISide
|
|
35
|
+
from kimi_cli.wire.types import ApprovalRequest, ApprovalResponse, ContentPart, WireMessage
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from fastmcp.mcp_config import MCPConfig
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _patch_session_id(record: dict[str, Any]) -> None:
|
|
42
|
+
"""Inject the current session ID (from ContextVar) into log records."""
|
|
43
|
+
try:
|
|
44
|
+
from kimi_cli.soul.toolset import get_session_id
|
|
45
|
+
|
|
46
|
+
sid = get_session_id()
|
|
47
|
+
record["extra"]["sid"] = sid if sid else ""
|
|
48
|
+
except Exception:
|
|
49
|
+
record["extra"].setdefault("sid", "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def enable_logging(debug: bool = False, *, redirect_stderr: bool = True) -> None:
|
|
53
|
+
# NOTE: stderr redirection is implemented by swapping the process-level fd=2 (dup2).
|
|
54
|
+
# That can hide Click/Typer error output during CLI startup, so some entrypoints delay
|
|
55
|
+
# installing it until after critical initialization succeeds.
|
|
56
|
+
logger.remove() # Remove default stderr handler
|
|
57
|
+
logger.enable("kimi_cli")
|
|
58
|
+
if debug:
|
|
59
|
+
logger.enable("kosong")
|
|
60
|
+
logger.add(
|
|
61
|
+
get_share_dir() / "logs" / "kimi.log",
|
|
62
|
+
# FIXME: configure level for different modules
|
|
63
|
+
level="TRACE" if debug else "INFO",
|
|
64
|
+
format=(
|
|
65
|
+
"{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | "
|
|
66
|
+
"{name}:{function}:{line} | {extra[sid]} - {message}"
|
|
67
|
+
),
|
|
68
|
+
rotation="06:00",
|
|
69
|
+
retention="10 days",
|
|
70
|
+
)
|
|
71
|
+
logger.configure(extra={"sid": ""}, patcher=_patch_session_id)
|
|
72
|
+
if redirect_stderr:
|
|
73
|
+
redirect_stderr_to_logger()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _write_original_stderr(text: str) -> None:
|
|
77
|
+
"""Write a user-facing notice to the terminal even if ``fd=2`` has been
|
|
78
|
+
redirected into the logger by ``redirect_stderr_to_logger``.
|
|
79
|
+
|
|
80
|
+
Falls back to ``sys.stderr`` when no redirector is installed (tests,
|
|
81
|
+
early-startup code paths), matching the semantics of ``_emit_fatal_error``
|
|
82
|
+
in ``cli/__init__.py``.
|
|
83
|
+
"""
|
|
84
|
+
with open_original_stderr() as stream:
|
|
85
|
+
if stream is not None:
|
|
86
|
+
stream.write(text.encode("utf-8", errors="replace"))
|
|
87
|
+
stream.flush()
|
|
88
|
+
return
|
|
89
|
+
sys.stderr.write(text)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _refresh_managed_models_silent(config: Config) -> None:
|
|
93
|
+
from kimi_cli.auth.platforms import refresh_managed_models
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
await refresh_managed_models(config)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.warning("Background managed-model refresh failed: {error}", error=exc)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cleanup_stale_foreground_subagents(runtime: Runtime) -> None:
|
|
102
|
+
subagent_store = getattr(runtime, "subagent_store", None)
|
|
103
|
+
if subagent_store is None:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
stale_agent_ids = [
|
|
107
|
+
record.agent_id
|
|
108
|
+
for record in subagent_store.list_instances()
|
|
109
|
+
if record.status == "running_foreground"
|
|
110
|
+
]
|
|
111
|
+
for agent_id in stale_agent_ids:
|
|
112
|
+
logger.warning(
|
|
113
|
+
"Marking stale foreground subagent instance as failed during startup: {agent_id}",
|
|
114
|
+
agent_id=agent_id,
|
|
115
|
+
)
|
|
116
|
+
subagent_store.update_instance(agent_id, status="failed")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class KimiCLI:
|
|
120
|
+
@staticmethod
|
|
121
|
+
async def create(
|
|
122
|
+
session: Session,
|
|
123
|
+
*,
|
|
124
|
+
# Basic configuration
|
|
125
|
+
config: Config | Path | None = None,
|
|
126
|
+
model_name: str | None = None,
|
|
127
|
+
thinking: bool | None = None,
|
|
128
|
+
# Run mode
|
|
129
|
+
yolo: bool = False,
|
|
130
|
+
afk: bool = False,
|
|
131
|
+
runtime_afk: bool = False,
|
|
132
|
+
plan_mode: bool = False,
|
|
133
|
+
resumed: bool = False,
|
|
134
|
+
ui_mode: str = "shell",
|
|
135
|
+
# Extensions
|
|
136
|
+
agent_file: Path | None = None,
|
|
137
|
+
mcp_configs: list[MCPConfig] | list[dict[str, Any]] | None = None,
|
|
138
|
+
skills_dirs: list[KaosPath] | None = None,
|
|
139
|
+
# Loop control
|
|
140
|
+
max_steps_per_turn: int | None = None,
|
|
141
|
+
max_retries_per_step: int | None = None,
|
|
142
|
+
max_ralph_iterations: int | None = None,
|
|
143
|
+
startup_progress: Callable[[str], None] | None = None,
|
|
144
|
+
defer_mcp_loading: bool = False,
|
|
145
|
+
) -> KimiCLI:
|
|
146
|
+
"""
|
|
147
|
+
Create a KimiCLI instance.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
session (Session): A session created by `Session.create` or `Session.continue_`.
|
|
151
|
+
config (Config | Path | None, optional): Configuration to use, or path to config file.
|
|
152
|
+
Defaults to None.
|
|
153
|
+
model_name (str | None, optional): Name of the model to use. Defaults to None.
|
|
154
|
+
thinking (bool | None, optional): Whether to enable thinking mode. Defaults to None.
|
|
155
|
+
yolo (bool, optional): Approve all actions without confirmation. Defaults to False.
|
|
156
|
+
afk (bool, optional): Invocation-level away-from-keyboard mode (no user is present
|
|
157
|
+
to answer questions or approve actions). Implies auto-approve. Defaults to False.
|
|
158
|
+
runtime_afk (bool, optional): Internal invocation-only afk overlay, used by print mode
|
|
159
|
+
so it stays non-interactive without changing persisted session afk. Defaults to
|
|
160
|
+
False.
|
|
161
|
+
agent_file (Path | None, optional): Path to the agent file. Defaults to None.
|
|
162
|
+
mcp_configs (list[MCPConfig | dict[str, Any]] | None, optional): MCP configs to load
|
|
163
|
+
MCP tools from. Defaults to None.
|
|
164
|
+
skills_dirs (list[KaosPath] | None, optional): Custom skills directories that
|
|
165
|
+
override default user/project discovery. Defaults to None.
|
|
166
|
+
max_steps_per_turn (int | None, optional): Maximum number of steps in one turn.
|
|
167
|
+
Defaults to None.
|
|
168
|
+
max_retries_per_step (int | None, optional): Maximum number of retries in one step.
|
|
169
|
+
Defaults to None.
|
|
170
|
+
max_ralph_iterations (int | None, optional): Extra iterations after the first turn in
|
|
171
|
+
Ralph mode. Defaults to None.
|
|
172
|
+
startup_progress (Callable[[str], None] | None, optional): Progress callback used by
|
|
173
|
+
interactive startup UI. Defaults to None.
|
|
174
|
+
defer_mcp_loading (bool, optional): Defer MCP startup until the interactive shell is
|
|
175
|
+
ready. Defaults to False.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
FileNotFoundError: When the agent file is not found.
|
|
179
|
+
ConfigError(KimiCLIException, ValueError): When the configuration is invalid.
|
|
180
|
+
AgentSpecError(KimiCLIException, ValueError): When the agent specification is invalid.
|
|
181
|
+
SystemPromptTemplateError(KimiCLIException, ValueError): When the system prompt
|
|
182
|
+
template is invalid.
|
|
183
|
+
InvalidToolError(KimiCLIException, ValueError): When any tool cannot be loaded.
|
|
184
|
+
MCPConfigError(KimiCLIException, ValueError): When any MCP configuration is invalid.
|
|
185
|
+
MCPRuntimeError(KimiCLIException, RuntimeError): When any MCP server cannot be
|
|
186
|
+
connected.
|
|
187
|
+
"""
|
|
188
|
+
_create_t0 = time.monotonic()
|
|
189
|
+
_phase_timings_ms: dict[str, int] = {}
|
|
190
|
+
|
|
191
|
+
if startup_progress is not None:
|
|
192
|
+
startup_progress("Loading configuration...")
|
|
193
|
+
|
|
194
|
+
_phase_t = time.monotonic()
|
|
195
|
+
config = config if isinstance(config, Config) else load_config(config)
|
|
196
|
+
_phase_timings_ms["config_ms"] = int((time.monotonic() - _phase_t) * 1000)
|
|
197
|
+
if max_steps_per_turn is not None:
|
|
198
|
+
config.loop_control.max_steps_per_turn = max_steps_per_turn
|
|
199
|
+
if max_retries_per_step is not None:
|
|
200
|
+
config.loop_control.max_retries_per_step = max_retries_per_step
|
|
201
|
+
if max_ralph_iterations is not None:
|
|
202
|
+
config.loop_control.max_ralph_iterations = max_ralph_iterations
|
|
203
|
+
logger.info("Loaded config: {config}", config=config)
|
|
204
|
+
|
|
205
|
+
_phase_t = time.monotonic()
|
|
206
|
+
oauth = OAuthManager(config)
|
|
207
|
+
|
|
208
|
+
bg_refresh_task = asyncio.create_task(_refresh_managed_models_silent(config))
|
|
209
|
+
|
|
210
|
+
model: LLMModel | None = None
|
|
211
|
+
provider: LLMProvider | None = None
|
|
212
|
+
|
|
213
|
+
# try to use config file
|
|
214
|
+
if not model_name and config.default_model:
|
|
215
|
+
# no --model specified && default model is set in config
|
|
216
|
+
model = config.models[config.default_model]
|
|
217
|
+
provider = config.providers[model.provider]
|
|
218
|
+
if model_name and model_name in config.models:
|
|
219
|
+
# --model specified && model is set in config
|
|
220
|
+
model = config.models[model_name]
|
|
221
|
+
provider = config.providers[model.provider]
|
|
222
|
+
|
|
223
|
+
if not model:
|
|
224
|
+
model = LLMModel(provider="", model="", max_context_size=100_000)
|
|
225
|
+
provider = LLMProvider(type="kimi", base_url="", api_key=SecretStr(""))
|
|
226
|
+
|
|
227
|
+
# try overwrite with environment variables
|
|
228
|
+
assert provider is not None
|
|
229
|
+
assert model is not None
|
|
230
|
+
env_overrides = augment_provider_with_env_vars(provider, model)
|
|
231
|
+
|
|
232
|
+
# determine thinking mode
|
|
233
|
+
thinking = config.default_thinking if thinking is None else thinking
|
|
234
|
+
|
|
235
|
+
# determine yolo mode
|
|
236
|
+
yolo = yolo if yolo else config.default_yolo
|
|
237
|
+
|
|
238
|
+
# determine plan mode (only for new sessions, not restored)
|
|
239
|
+
if not resumed:
|
|
240
|
+
plan_mode = plan_mode if plan_mode else config.default_plan_mode
|
|
241
|
+
|
|
242
|
+
llm = create_llm(
|
|
243
|
+
provider,
|
|
244
|
+
model,
|
|
245
|
+
thinking=thinking,
|
|
246
|
+
session_id=session.id,
|
|
247
|
+
oauth=oauth,
|
|
248
|
+
)
|
|
249
|
+
if llm is not None:
|
|
250
|
+
logger.info("Using LLM provider: {provider}", provider=provider)
|
|
251
|
+
logger.info("Using LLM model: {model}", model=model)
|
|
252
|
+
logger.info("Thinking mode: {thinking}", thinking=thinking)
|
|
253
|
+
|
|
254
|
+
if startup_progress is not None:
|
|
255
|
+
startup_progress("Scanning workspace...")
|
|
256
|
+
|
|
257
|
+
runtime = await Runtime.create(
|
|
258
|
+
config,
|
|
259
|
+
oauth,
|
|
260
|
+
llm,
|
|
261
|
+
session,
|
|
262
|
+
yolo,
|
|
263
|
+
afk=afk,
|
|
264
|
+
runtime_afk=runtime_afk,
|
|
265
|
+
skills_dirs=skills_dirs,
|
|
266
|
+
)
|
|
267
|
+
runtime.ui_mode = ui_mode
|
|
268
|
+
runtime.resumed = resumed
|
|
269
|
+
runtime.notifications.recover()
|
|
270
|
+
runtime.background_tasks.reconcile()
|
|
271
|
+
_cleanup_stale_foreground_subagents(runtime)
|
|
272
|
+
_phase_timings_ms["init_ms"] = int((time.monotonic() - _phase_t) * 1000)
|
|
273
|
+
|
|
274
|
+
# Refresh plugin configs with fresh credentials (e.g. OAuth tokens)
|
|
275
|
+
try:
|
|
276
|
+
from kimi_cli.plugin.manager import (
|
|
277
|
+
collect_host_values,
|
|
278
|
+
get_plugins_dir,
|
|
279
|
+
refresh_plugin_configs,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
host_values = collect_host_values(config, oauth)
|
|
283
|
+
if host_values.get("api_key"):
|
|
284
|
+
refresh_plugin_configs(get_plugins_dir(), host_values)
|
|
285
|
+
except Exception:
|
|
286
|
+
logger.debug("Failed to refresh plugin configs, skipping")
|
|
287
|
+
|
|
288
|
+
if agent_file is None:
|
|
289
|
+
agent_file = DEFAULT_AGENT_FILE
|
|
290
|
+
if startup_progress is not None:
|
|
291
|
+
startup_progress("Loading agent...")
|
|
292
|
+
|
|
293
|
+
_phase_t = time.monotonic()
|
|
294
|
+
agent = await load_agent(
|
|
295
|
+
agent_file,
|
|
296
|
+
runtime,
|
|
297
|
+
mcp_configs=mcp_configs or [],
|
|
298
|
+
start_mcp_loading=not defer_mcp_loading,
|
|
299
|
+
)
|
|
300
|
+
_phase_timings_ms["mcp_ms"] = int((time.monotonic() - _phase_t) * 1000)
|
|
301
|
+
|
|
302
|
+
if startup_progress is not None:
|
|
303
|
+
startup_progress("Restoring conversation...")
|
|
304
|
+
context = Context(session.context_file)
|
|
305
|
+
await context.restore()
|
|
306
|
+
|
|
307
|
+
if context.system_prompt is not None:
|
|
308
|
+
agent = dataclasses.replace(agent, system_prompt=context.system_prompt)
|
|
309
|
+
else:
|
|
310
|
+
await context.write_system_prompt(agent.system_prompt)
|
|
311
|
+
|
|
312
|
+
soul = KimiSoul(agent, context=context)
|
|
313
|
+
|
|
314
|
+
# Activate plan mode if requested (for new sessions or --plan flag)
|
|
315
|
+
if plan_mode and not soul.plan_mode:
|
|
316
|
+
await soul.set_plan_mode_from_manual(True)
|
|
317
|
+
elif plan_mode and soul.plan_mode:
|
|
318
|
+
# Already in plan mode from restored session, trigger activation reminder
|
|
319
|
+
soul.schedule_plan_activation_reminder()
|
|
320
|
+
|
|
321
|
+
# Create and inject hook engine
|
|
322
|
+
from kimi_cli.hooks.engine import HookEngine
|
|
323
|
+
|
|
324
|
+
hook_engine = HookEngine(config.hooks, cwd=str(session.work_dir))
|
|
325
|
+
soul.set_hook_engine(hook_engine)
|
|
326
|
+
runtime.hook_engine = hook_engine
|
|
327
|
+
|
|
328
|
+
# --- Initialize telemetry ---
|
|
329
|
+
from kimi_cli.telemetry import attach_sink, set_context
|
|
330
|
+
from kimi_cli.telemetry import disable as disable_telemetry
|
|
331
|
+
|
|
332
|
+
telemetry_disabled = not config.telemetry or get_env_bool("KIMI_DISABLE_TELEMETRY")
|
|
333
|
+
if telemetry_disabled:
|
|
334
|
+
disable_telemetry()
|
|
335
|
+
else:
|
|
336
|
+
device_id = get_device_id()
|
|
337
|
+
set_context(device_id=device_id, session_id=session.id)
|
|
338
|
+
from kimi_cli.telemetry.sink import EventSink
|
|
339
|
+
from kimi_cli.telemetry.transport import AsyncTransport
|
|
340
|
+
|
|
341
|
+
def _get_token() -> str | None:
|
|
342
|
+
return oauth.get_cached_access_token(KIMI_CODE_OAUTH_KEY)
|
|
343
|
+
|
|
344
|
+
transport = AsyncTransport(device_id=device_id, get_access_token=_get_token)
|
|
345
|
+
sink = EventSink(
|
|
346
|
+
transport,
|
|
347
|
+
version=VERSION,
|
|
348
|
+
model=model.model if model else "",
|
|
349
|
+
ui_mode=ui_mode,
|
|
350
|
+
)
|
|
351
|
+
attach_sink(sink)
|
|
352
|
+
|
|
353
|
+
from kimi_cli.telemetry import track, track_session_started_once
|
|
354
|
+
from kimi_cli.telemetry.crash import install_asyncio_handler, set_phase
|
|
355
|
+
|
|
356
|
+
# App init finished — enter runtime phase and hook asyncio crashes.
|
|
357
|
+
install_asyncio_handler()
|
|
358
|
+
set_phase("runtime")
|
|
359
|
+
|
|
360
|
+
if ui_mode != "wire":
|
|
361
|
+
track_session_started_once(ui_mode=ui_mode, resumed=resumed)
|
|
362
|
+
track(
|
|
363
|
+
"started",
|
|
364
|
+
resumed=resumed,
|
|
365
|
+
yolo=runtime.approval.is_yolo(),
|
|
366
|
+
afk=runtime.approval.is_afk(),
|
|
367
|
+
)
|
|
368
|
+
track(
|
|
369
|
+
"startup_perf",
|
|
370
|
+
duration_ms=int((time.monotonic() - _create_t0) * 1000),
|
|
371
|
+
config_ms=_phase_timings_ms.get("config_ms", 0),
|
|
372
|
+
init_ms=_phase_timings_ms.get("init_ms", 0),
|
|
373
|
+
mcp_ms=_phase_timings_ms.get("mcp_ms", 0),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return KimiCLI(soul, runtime, env_overrides, bg_refresh_task)
|
|
377
|
+
|
|
378
|
+
def __init__(
|
|
379
|
+
self,
|
|
380
|
+
_soul: KimiSoul,
|
|
381
|
+
_runtime: Runtime,
|
|
382
|
+
_env_overrides: dict[str, str],
|
|
383
|
+
_bg_refresh_task: asyncio.Task[None] | None = None,
|
|
384
|
+
) -> None:
|
|
385
|
+
self._soul = _soul
|
|
386
|
+
self._runtime = _runtime
|
|
387
|
+
self._env_overrides = _env_overrides
|
|
388
|
+
self._bg_refresh_task = _bg_refresh_task
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def soul(self) -> KimiSoul:
|
|
392
|
+
"""Get the KimiSoul instance."""
|
|
393
|
+
return self._soul
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def session(self) -> Session:
|
|
397
|
+
"""Get the Session instance."""
|
|
398
|
+
return self._runtime.session
|
|
399
|
+
|
|
400
|
+
async def shutdown_background_tasks(self) -> None:
|
|
401
|
+
"""Kill active background tasks on exit, unless keep_alive_on_exit is configured.
|
|
402
|
+
|
|
403
|
+
Prints a stderr notice naming each task so the user knows what is being
|
|
404
|
+
terminated, waits out the configured kill grace period so SIGTERM can
|
|
405
|
+
take effect, then reconciles and reports any workers that ignored the
|
|
406
|
+
signal.
|
|
407
|
+
|
|
408
|
+
This runs on the CLI's hard-shutdown path, so every failure mode must
|
|
409
|
+
be contained: disk IO errors from ``list_tasks`` / ``reconcile`` or
|
|
410
|
+
store corruption must not propagate and replace the real exit code
|
|
411
|
+
with a traceback.
|
|
412
|
+
"""
|
|
413
|
+
# Cancel the startup managed-model refresh task if it is still running
|
|
414
|
+
# so it does not outlive the CLI process.
|
|
415
|
+
if self._bg_refresh_task is not None and not self._bg_refresh_task.done():
|
|
416
|
+
self._bg_refresh_task.cancel()
|
|
417
|
+
|
|
418
|
+
bg_config = self._runtime.config.background
|
|
419
|
+
if bg_config.keep_alive_on_exit:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
manager = self._runtime.background_tasks
|
|
424
|
+
active_views = [
|
|
425
|
+
v
|
|
426
|
+
for v in manager.list_tasks(status=None, limit=None)
|
|
427
|
+
if not is_terminal_status(v.runtime.status)
|
|
428
|
+
]
|
|
429
|
+
if not active_views:
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
# Split by whether the task has already been kill-requested (e.g.
|
|
433
|
+
# by the ``--print`` timeout path which ran immediately before
|
|
434
|
+
# this shutdown). For those:
|
|
435
|
+
# - don't re-announce on stderr (user saw the timeout notice)
|
|
436
|
+
# - don't re-kill with a generic reason, which would overwrite
|
|
437
|
+
# the more specific ``kill_reason`` on disk
|
|
438
|
+
# We still reconcile + grace-wait for them so they reach terminal
|
|
439
|
+
# status before the process exits.
|
|
440
|
+
fresh_targets = [v for v in active_views if v.control.kill_requested_at is None]
|
|
441
|
+
|
|
442
|
+
if fresh_targets:
|
|
443
|
+
# Build and emit the kill notice via ``open_original_stderr``
|
|
444
|
+
# — ``sys.stderr.write`` alone would silently land in
|
|
445
|
+
# ``kimi.log`` because ``redirect_stderr_to_logger`` has
|
|
446
|
+
# replaced fd=2 with a pipe into the logger by this point.
|
|
447
|
+
lines = [f"\u26a0 Killing {len(fresh_targets)} background tasks:\n"]
|
|
448
|
+
for view in fresh_targets:
|
|
449
|
+
description = view.spec.description or ""
|
|
450
|
+
if len(description) > 60:
|
|
451
|
+
description = description[:57] + "..."
|
|
452
|
+
lines.append(f" {view.spec.id} {description}\n")
|
|
453
|
+
_write_original_stderr("".join(lines))
|
|
454
|
+
|
|
455
|
+
killed: list[str] = []
|
|
456
|
+
for view in fresh_targets:
|
|
457
|
+
try:
|
|
458
|
+
manager.kill(view.spec.id, reason="CLI session ended")
|
|
459
|
+
killed.append(view.spec.id)
|
|
460
|
+
except Exception:
|
|
461
|
+
logger.exception(
|
|
462
|
+
"Failed to kill task {task_id} during shutdown",
|
|
463
|
+
task_id=view.spec.id,
|
|
464
|
+
)
|
|
465
|
+
if killed:
|
|
466
|
+
logger.info(
|
|
467
|
+
"Stopped {n} background task(s) on exit: {ids}",
|
|
468
|
+
n=len(killed),
|
|
469
|
+
ids=killed,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
await asyncio.sleep(bg_config.kill_grace_period_ms / 1000)
|
|
473
|
+
manager.reconcile()
|
|
474
|
+
survivors = [
|
|
475
|
+
v
|
|
476
|
+
for v in manager.list_tasks(status=None, limit=None)
|
|
477
|
+
if not is_terminal_status(v.runtime.status)
|
|
478
|
+
]
|
|
479
|
+
if survivors:
|
|
480
|
+
# Distinguish "worker is mid-shutdown" (kill request on record,
|
|
481
|
+
# SIGTERM delivered, worker just hasn't written terminal state
|
|
482
|
+
# yet) from a genuine leak (never got kill-requested, i.e.
|
|
483
|
+
# ``manager.kill`` raised). Without this split, users saw
|
|
484
|
+
# ``killed N`` from the --print timeout path immediately
|
|
485
|
+
# followed by ``(N tasks still alive)`` here — a direct
|
|
486
|
+
# semantic contradiction.
|
|
487
|
+
terminating = [s for s in survivors if s.control.kill_requested_at is not None]
|
|
488
|
+
leaking = [s for s in survivors if s.control.kill_requested_at is None]
|
|
489
|
+
# Report leaks first — ``stop request failed`` is strictly
|
|
490
|
+
# more severe than ``still terminating`` (the latter will
|
|
491
|
+
# resolve on its own once the worker writes terminal state).
|
|
492
|
+
if leaking:
|
|
493
|
+
_write_original_stderr(
|
|
494
|
+
f" ({len(leaking)} tasks still running; stop request failed)\n"
|
|
495
|
+
)
|
|
496
|
+
if terminating:
|
|
497
|
+
_write_original_stderr(f" ({len(terminating)} tasks still terminating)\n")
|
|
498
|
+
except Exception:
|
|
499
|
+
logger.warning("Error during background task shutdown; continuing exit", exc_info=True)
|
|
500
|
+
|
|
501
|
+
async def await_bg_tasks_shutdown(self, timeout: float = 2.0) -> None:
|
|
502
|
+
"""Await completion of the model-refresh background task after cancellation."""
|
|
503
|
+
task = self._bg_refresh_task
|
|
504
|
+
if task is None or task.done():
|
|
505
|
+
return
|
|
506
|
+
# Best-effort cleanup — errors inside the task are already logged.
|
|
507
|
+
with contextlib.suppress(TimeoutError, asyncio.CancelledError, Exception):
|
|
508
|
+
await asyncio.wait_for(asyncio.shield(task), timeout=timeout)
|
|
509
|
+
|
|
510
|
+
@contextlib.asynccontextmanager
|
|
511
|
+
async def _env(self) -> AsyncGenerator[None]:
|
|
512
|
+
original_cwd = KaosPath.cwd()
|
|
513
|
+
await kaos.chdir(self._runtime.session.work_dir)
|
|
514
|
+
try:
|
|
515
|
+
# to ignore possible warnings from dateparser
|
|
516
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
517
|
+
async with self._runtime.oauth.refreshing(self._runtime):
|
|
518
|
+
yield
|
|
519
|
+
finally:
|
|
520
|
+
await kaos.chdir(original_cwd)
|
|
521
|
+
|
|
522
|
+
async def run(
|
|
523
|
+
self,
|
|
524
|
+
user_input: str | list[ContentPart],
|
|
525
|
+
cancel_event: asyncio.Event,
|
|
526
|
+
merge_wire_messages: bool = False,
|
|
527
|
+
) -> AsyncGenerator[WireMessage]:
|
|
528
|
+
"""
|
|
529
|
+
Run the Kimi Code CLI instance without any UI and yield Wire messages directly.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
user_input (str | list[ContentPart]): The user input to the agent.
|
|
533
|
+
cancel_event (asyncio.Event): An event to cancel the run.
|
|
534
|
+
merge_wire_messages (bool): Whether to merge Wire messages as much as possible.
|
|
535
|
+
|
|
536
|
+
Yields:
|
|
537
|
+
WireMessage: The Wire messages from the `KimiSoul`.
|
|
538
|
+
|
|
539
|
+
Raises:
|
|
540
|
+
LLMNotSet: When the LLM is not set.
|
|
541
|
+
LLMNotSupported: When the LLM does not have required capabilities.
|
|
542
|
+
ChatProviderError: When the LLM provider returns an error.
|
|
543
|
+
MaxStepsReached: When the maximum number of steps is reached.
|
|
544
|
+
RunCancelled: When the run is cancelled by the cancel event.
|
|
545
|
+
"""
|
|
546
|
+
async with self._env():
|
|
547
|
+
wire_future = asyncio.Future[WireUISide]()
|
|
548
|
+
stop_ui_loop = asyncio.Event()
|
|
549
|
+
approval_bridge_tasks: dict[str, asyncio.Task[None]] = {}
|
|
550
|
+
forwarded_approval_requests: dict[str, ApprovalRequest] = {}
|
|
551
|
+
|
|
552
|
+
async def _bridge_approval_request(request: ApprovalRequest) -> None:
|
|
553
|
+
try:
|
|
554
|
+
response = await request.wait()
|
|
555
|
+
assert self._runtime.approval_runtime is not None
|
|
556
|
+
self._runtime.approval_runtime.resolve(
|
|
557
|
+
request.id, response, feedback=request.feedback
|
|
558
|
+
)
|
|
559
|
+
finally:
|
|
560
|
+
approval_bridge_tasks.pop(request.id, None)
|
|
561
|
+
forwarded_approval_requests.pop(request.id, None)
|
|
562
|
+
|
|
563
|
+
def _forward_approval_request(wire: Wire, request: ApprovalRequest) -> None:
|
|
564
|
+
if request.id in forwarded_approval_requests:
|
|
565
|
+
return
|
|
566
|
+
forwarded_approval_requests[request.id] = request
|
|
567
|
+
if request.id not in approval_bridge_tasks:
|
|
568
|
+
approval_bridge_tasks[request.id] = asyncio.create_task(
|
|
569
|
+
_bridge_approval_request(request)
|
|
570
|
+
)
|
|
571
|
+
wire.soul_side.send(request)
|
|
572
|
+
|
|
573
|
+
async def _ui_loop_fn(wire: Wire) -> None:
|
|
574
|
+
wire_future.set_result(wire.ui_side(merge=merge_wire_messages))
|
|
575
|
+
assert self._runtime.root_wire_hub is not None
|
|
576
|
+
assert self._runtime.approval_runtime is not None
|
|
577
|
+
root_hub_queue = self._runtime.root_wire_hub.subscribe()
|
|
578
|
+
stop_task = asyncio.create_task(stop_ui_loop.wait())
|
|
579
|
+
queue_task = asyncio.create_task(root_hub_queue.get())
|
|
580
|
+
try:
|
|
581
|
+
for pending in self._runtime.approval_runtime.list_pending():
|
|
582
|
+
_forward_approval_request(
|
|
583
|
+
wire,
|
|
584
|
+
ApprovalRequest(
|
|
585
|
+
id=pending.id,
|
|
586
|
+
tool_call_id=pending.tool_call_id,
|
|
587
|
+
sender=pending.sender,
|
|
588
|
+
action=pending.action,
|
|
589
|
+
description=pending.description,
|
|
590
|
+
display=pending.display,
|
|
591
|
+
source_kind=pending.source.kind,
|
|
592
|
+
source_id=pending.source.id,
|
|
593
|
+
agent_id=pending.source.agent_id,
|
|
594
|
+
subagent_type=pending.source.subagent_type,
|
|
595
|
+
),
|
|
596
|
+
)
|
|
597
|
+
while True:
|
|
598
|
+
done, _ = await asyncio.wait(
|
|
599
|
+
[stop_task, queue_task],
|
|
600
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
601
|
+
)
|
|
602
|
+
if stop_task in done:
|
|
603
|
+
break
|
|
604
|
+
try:
|
|
605
|
+
msg = queue_task.result()
|
|
606
|
+
except QueueShutDown:
|
|
607
|
+
break
|
|
608
|
+
match msg:
|
|
609
|
+
case ApprovalRequest() as request:
|
|
610
|
+
_forward_approval_request(wire, request)
|
|
611
|
+
queue_task = asyncio.create_task(root_hub_queue.get())
|
|
612
|
+
continue
|
|
613
|
+
case ApprovalResponse() as response:
|
|
614
|
+
if (
|
|
615
|
+
request := forwarded_approval_requests.get(response.request_id)
|
|
616
|
+
) and not request.resolved:
|
|
617
|
+
request.resolve(response.response, response.feedback)
|
|
618
|
+
case _:
|
|
619
|
+
pass
|
|
620
|
+
wire.soul_side.send(msg)
|
|
621
|
+
queue_task = asyncio.create_task(root_hub_queue.get())
|
|
622
|
+
finally:
|
|
623
|
+
stop_task.cancel()
|
|
624
|
+
queue_task.cancel()
|
|
625
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
626
|
+
await stop_task
|
|
627
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
628
|
+
await queue_task
|
|
629
|
+
for task in list(approval_bridge_tasks.values()):
|
|
630
|
+
task.cancel()
|
|
631
|
+
for task in list(approval_bridge_tasks.values()):
|
|
632
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
633
|
+
await task
|
|
634
|
+
approval_bridge_tasks.clear()
|
|
635
|
+
forwarded_approval_requests.clear()
|
|
636
|
+
assert self._runtime.root_wire_hub is not None
|
|
637
|
+
self._runtime.root_wire_hub.unsubscribe(root_hub_queue)
|
|
638
|
+
|
|
639
|
+
run_cancel_event = asyncio.Event()
|
|
640
|
+
|
|
641
|
+
async def _mirror_external_cancel() -> None:
|
|
642
|
+
await cancel_event.wait()
|
|
643
|
+
run_cancel_event.set()
|
|
644
|
+
|
|
645
|
+
external_cancel_task = asyncio.create_task(
|
|
646
|
+
_mirror_external_cancel(),
|
|
647
|
+
name="cancel-event-mirror",
|
|
648
|
+
)
|
|
649
|
+
soul_task = asyncio.create_task(
|
|
650
|
+
run_soul(
|
|
651
|
+
self.soul,
|
|
652
|
+
user_input,
|
|
653
|
+
_ui_loop_fn,
|
|
654
|
+
run_cancel_event,
|
|
655
|
+
runtime=self._runtime,
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
wire_shut_down = False
|
|
660
|
+
try:
|
|
661
|
+
wire_ui = await wire_future
|
|
662
|
+
while True:
|
|
663
|
+
msg = await wire_ui.receive()
|
|
664
|
+
yield msg
|
|
665
|
+
except QueueShutDown:
|
|
666
|
+
wire_shut_down = True
|
|
667
|
+
pass
|
|
668
|
+
finally:
|
|
669
|
+
# stop consuming Wire messages
|
|
670
|
+
stop_ui_loop.set()
|
|
671
|
+
cleanup_cancelled_run = False
|
|
672
|
+
if not wire_shut_down and not soul_task.done() and not cancel_event.is_set():
|
|
673
|
+
cleanup_cancelled_run = True
|
|
674
|
+
run_cancel_event.set()
|
|
675
|
+
# wait for the soul task to finish, or raise
|
|
676
|
+
try:
|
|
677
|
+
await soul_task
|
|
678
|
+
except RunCancelled:
|
|
679
|
+
if not cleanup_cancelled_run:
|
|
680
|
+
raise
|
|
681
|
+
finally:
|
|
682
|
+
external_cancel_task.cancel()
|
|
683
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
684
|
+
await external_cancel_task
|
|
685
|
+
|
|
686
|
+
async def run_shell(
|
|
687
|
+
self, command: str | None = None, *, prefill_text: str | None = None
|
|
688
|
+
) -> bool:
|
|
689
|
+
"""Run the Kimi Code CLI instance with shell UI."""
|
|
690
|
+
from kimi_cli.ui.shell import Shell, WelcomeInfoItem
|
|
691
|
+
|
|
692
|
+
if command is None:
|
|
693
|
+
from kimi_cli.ui.shell.update import check_update_gate
|
|
694
|
+
|
|
695
|
+
check_update_gate()
|
|
696
|
+
|
|
697
|
+
welcome_info = [
|
|
698
|
+
WelcomeInfoItem(
|
|
699
|
+
name="Directory", value=str(shorten_home(self._runtime.session.work_dir))
|
|
700
|
+
),
|
|
701
|
+
WelcomeInfoItem(name="Session", value=self._runtime.session.id),
|
|
702
|
+
]
|
|
703
|
+
if base_url := self._env_overrides.get("KIMI_BASE_URL"):
|
|
704
|
+
welcome_info.append(
|
|
705
|
+
WelcomeInfoItem(
|
|
706
|
+
name="API URL",
|
|
707
|
+
value=f"{base_url} (from KIMI_BASE_URL)",
|
|
708
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
if self._env_overrides.get("KIMI_API_KEY"):
|
|
712
|
+
welcome_info.append(
|
|
713
|
+
WelcomeInfoItem(
|
|
714
|
+
name="API Key",
|
|
715
|
+
value="****** (from KIMI_API_KEY)",
|
|
716
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
if not self._runtime.llm:
|
|
720
|
+
welcome_info.append(
|
|
721
|
+
WelcomeInfoItem(
|
|
722
|
+
name="Model",
|
|
723
|
+
value="not set, send /login to login",
|
|
724
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
elif "KIMI_MODEL_NAME" in self._env_overrides:
|
|
728
|
+
welcome_info.append(
|
|
729
|
+
WelcomeInfoItem(
|
|
730
|
+
name="Model",
|
|
731
|
+
value=f"{self._soul.model_name} (from KIMI_MODEL_NAME)",
|
|
732
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
else:
|
|
736
|
+
welcome_info.append(
|
|
737
|
+
WelcomeInfoItem(
|
|
738
|
+
name="Model",
|
|
739
|
+
value=model_display_name(
|
|
740
|
+
self._soul.model_name,
|
|
741
|
+
self._runtime.llm.model_config if self._runtime.llm else None,
|
|
742
|
+
),
|
|
743
|
+
level=WelcomeInfoItem.Level.INFO,
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
model_name = self._soul.model_name
|
|
747
|
+
if model_name not in (
|
|
748
|
+
"kimi-for-coding",
|
|
749
|
+
"kimi-code",
|
|
750
|
+
) and not model_name.startswith("kimi-k2"):
|
|
751
|
+
welcome_info.append(
|
|
752
|
+
WelcomeInfoItem(
|
|
753
|
+
name="Tip",
|
|
754
|
+
value="send /login to use Kimi for Coding",
|
|
755
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
welcome_info.append(
|
|
759
|
+
WelcomeInfoItem(
|
|
760
|
+
name="\nTip",
|
|
761
|
+
value=(
|
|
762
|
+
"Spot a bug or have feedback? Type /feedback right in this session"
|
|
763
|
+
" — every report makes Kimi better."
|
|
764
|
+
),
|
|
765
|
+
level=WelcomeInfoItem.Level.INFO,
|
|
766
|
+
)
|
|
767
|
+
)
|
|
768
|
+
async with self._env():
|
|
769
|
+
shell = Shell(self._soul, welcome_info=welcome_info, prefill_text=prefill_text)
|
|
770
|
+
return await shell.run(command)
|
|
771
|
+
|
|
772
|
+
async def run_print(
|
|
773
|
+
self,
|
|
774
|
+
input_format: InputFormat,
|
|
775
|
+
output_format: OutputFormat,
|
|
776
|
+
command: str | None = None,
|
|
777
|
+
*,
|
|
778
|
+
final_only: bool = False,
|
|
779
|
+
) -> int:
|
|
780
|
+
"""Run the Kimi Code CLI instance with print UI."""
|
|
781
|
+
from kimi_cli.ui.print import Print
|
|
782
|
+
|
|
783
|
+
async with self._env():
|
|
784
|
+
print_ = Print(
|
|
785
|
+
self._soul,
|
|
786
|
+
input_format,
|
|
787
|
+
output_format,
|
|
788
|
+
self._runtime.session.context_file,
|
|
789
|
+
final_only=final_only,
|
|
790
|
+
)
|
|
791
|
+
return await print_.run(command)
|
|
792
|
+
|
|
793
|
+
async def run_acp(self) -> None:
|
|
794
|
+
"""Run the Kimi Code CLI instance as ACP server."""
|
|
795
|
+
from kimi_cli.ui.acp import ACP
|
|
796
|
+
|
|
797
|
+
async with self._env():
|
|
798
|
+
acp = ACP(self._soul)
|
|
799
|
+
await acp.run()
|
|
800
|
+
|
|
801
|
+
async def run_wire_stdio(self) -> None:
|
|
802
|
+
"""Run the Kimi Code CLI instance as Wire server over stdio."""
|
|
803
|
+
from kimi_cli.wire.server import WireServer
|
|
804
|
+
|
|
805
|
+
async with self._env():
|
|
806
|
+
server = WireServer(self._soul)
|
|
807
|
+
await server.serve()
|