Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
26 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "aim467/mini-code-assistant", "files": {"mini_code_assistant/cli.py": {"e0947de8c580e4c2df044bf8db00b9a6e309f822": {"t": 1786068188.6040792, "n": 21223, "h": "81108dcd5b7ddcba", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\ncli.py - Mini Code Assistant 命令行入口\n\n核心原理:\n 这是整个编程助手的入口,实现了 **REPL**(Read-Eval-Print Loop)交互循环\n 和 **Agent Loop**(智能体循环)。\n\n REPL 循环(外层):\n 用户输入 → 处理 → 等待下一次输入\n\n Agent Loop(内层):\n 用户消息 → 发给 LLM(流式)→ LLM 逐块返回文本/工具调用\n → 如果有工具调用 → 执行工具 → 结果喂回 LLM → 继续推理\n → 如果只有文本 → 显示最终回复,结束循环\n\n 这两个嵌套循环就是 Claude Code、Codex 等工具的基本架构。\n 流式输出(SSE)让用户实时看到 LLM 的生成过程,无需等待完整响应。\n\n使用方式:\n # 全局安装后(推荐)\n mca # 在当前目录运行\n mca /path/to/project # 在指定目录运行\n mca --model deepseek-chat # 覆盖模型\n mca --api-key sk-xxx # 覆盖 API 密钥\n\n # 未安装,直接运行模块\n python -m mini_code_assistant [目录] [选项]\n\n配置优先级(从高到低):\n 1. 命令行参数 (--model, --api-key, --base-url)\n 2. Shell 环境变量 (export API_KEY=xxx)\n 3. 本地 ./.env 文件(项目级配置)\n 4. 全局 ~/.mca/.env 文件(用户级配置)\n 5. 内置默认值\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport argparse\nfrom pathlib import Path\n\nfrom .llm import LLMClient\nfrom .tools import ToolSystem\nfrom .context import Context\nfrom .token_counter import format_token_count, HAS_TIKTOKEN\nfrom . import __version__\n\n# ── Rich 可选依赖 ────────────────────────────────────────────\n# 如果安装了 rich,用 Markdown 渲染 + 面板展示;否则降级为纯文本\ntry:\n from rich.console import Console\n from rich.markdown import Markdown\n from rich.panel import Panel\n\n _console = Console()\n HAS_RICH = True\nexcept ImportError:\n _console = None\n HAS_RICH = False\n\n# ── prompt_toolkit 可选依赖 ──────────────────────────────────\n# 如果安装了 prompt_toolkit,用增强 REPL(命令历史、自动补全、多行编辑);\n# 否则降级为 input() 原始 REPL\ntry:\n from prompt_toolkit import PromptSession\n from prompt_toolkit.completion import Completer, Completion\n from prompt_toolkit.history import FileHistory\n\n HAS_PROMPT_TOOLKIT = True\nexcept ImportError:\n HAS_PROMPT_TOOLKIT = False\n\n\n# ── 多路径 .env 加载器 ───────────────────────────────────────────\n# 不依赖 python-dotenv,手动解析 .env 文件\n# 支持两级配置:全局 ~/.mca/.env + 本地 ./.env(本地覆盖全局)\ndef load_env():\n \"\"\"\n 加载配置文件,优先级(从高到低):\n 1. Shell 环境变量(已存在的不会被覆盖)\n 2. 本地 ./.env(项目级配置,覆盖全局)\n 3. 全局 ~/.mca/.env(用户级配置)\n\n 实现方式:先加载全局配置到字典,再用本地配置覆盖,\n 最后用 setdefault 写入环境变量(不覆盖已有的 shell 变量)。\n \"\"\"\n config = {}\n\n # 1. 先加载全局配置(~/.mca/.env)\n global_path = Path.home() / \".mca\" / \".env\"\n _parse_env_file(global_path, config)\n\n # 2. 再加载本地配置(./.env),覆盖全局\n local_path = Path.cwd() / \".env\"\n _parse_env_file(local_path, config)\n\n # 3. 应用到环境变量(不覆盖已有的 shell 变量)\n for key, value in config.items():\n os.environ.setdefault(key, value)\n\n\ndef _parse_env_file(path: Path, config: dict):\n \"\"\"解析 .env 文件,将键值对写入 config 字典。\"\"\"\n if not path.exists():\n return\n for line in path.read_text(encoding=\"utf-8\").splitlines():\n line = line.strip()\n if not line or line.startswith(\"#\"):\n continue\n if \"=\" in line:\n key, value = line.split(\"=\", 1)\n config[key.strip()] = value.strip().strip('\"').strip(\"'\")\n\n\n# ── 终端颜色 ───────────────────────────────────────────────────\nclass C:\n \"\"\"ANSI 颜色码简写。\"\"\"\n CYAN = \"\\033[96m\"\n GREEN = \"\\033[92m\"\n YELLOW = \"\\033[93m\"\n RED = \"\\033[91m\"\n DIM = \"\\033[2m\"\n BOLD = \"\\033[1m\"\n RESET = \"\\033[0m\"\n\n\ndef print_banner(workdir: Path, model: str, context_limit: int):\n \"\"\"打印启动横幅。\"\"\"\n token_mode = \"tiktoken\" if HAS_TIKTOKEN else \"estimated\"\n repl_mode = \"prompt_toolkit\" if HAS_PROMPT_TOOLKIT else \"basic\"\n stream_mode = \"streaming\" # 流式输出始终启用\n if HAS_RICH:\n _console.print(Panel(\n f\"[bold cyan]Mini Code Assistant[/] v{__version__}\\n\"\n f\"[dim]workdir:[/] {workdir}\\n\"\n f\"[dim]model: [/] {model}\\n\"\n f\"[dim]context:[/] {context_limit:,} tokens [dim]token count: {token_mode}[/]\\n\"\n f\"[dim]repl: [/] {repl_mode} [dim]output:[/] {stream_mode}\",\n border_style=\"cyan\",\n padding=(1, 2),\n ))\n if HAS_PROMPT_TOOLKIT:\n _console.print(\" type [yellow]/help[/] for help, [yellow]/exit[/] to quit [dim](↑↓ history, Tab complete, Ctrl+R search)[/]\\n\")\n else:\n _console.print(\" type [yellow]/help[/] for help, [yellow]/exit[/] to quit\\n\")\n else:\n print(f\"{C.CYAN}{C.BOLD}\")\n print(\" +======================================+\")\n print(\" | Mini Code Assistant |\")\n print(\" | v\" + __version__ + \" - \" + \" \" * (22 - len(__version__)) + \" |\")\n print(\" +======================================+\")\n print(f\"{C.RESET}\")\n print(f\" {C.DIM}workdir:{C.RESET} {workdir}\")\n print(f\" {C.DIM}model: {C.RESET} {model}\")\n print(f\" {C.DIM}context:{C.RESET} {context_limit:,} tokens {C.DIM}(token count: {token_mode}){C.RESET}\")\n print(f\" {C.DIM}repl: {C.RESET} {repl_mode} {C.DIM}output:{C.RESET} {stream_mode}\")\n print(f\" {C.DIM}--------------------------------------{C.RESET}\")\n if HAS_PROMPT_TOOLKIT:\n print(f\" type {C.YELLOW}/help{C.RESET} for help, {C.YELLOW}/exit{C.RESET} to quit {C.DIM}(↑↓ history, Tab complete, Ctrl+R search){C.RESET}\\n\")\n else:\n print(f\" type {C.YELLOW}/help{C.RESET} for help, {C.YELLOW}/exit{C.RESET} to quit\\n\")\n\n\ndef print_help():\n \"\"\"打印帮助信息。\"\"\"\n if HAS_RICH:\n repl_hint = \"\"\n if HAS_PROMPT_TOOLKIT:\n repl_hint = (\n \"\\n[bold]REPL Shortcuts:[/]\\n\"\n \" [yellow]↑/↓[/] navigate command history\\n\"\n \" [yellow]Tab[/] auto-complete /commands\\n\"\n \" [yellow]Ctrl+R[/] search command history\\n\"\n )\n _console.print(Panel(\n \"[bold]Commands:[/]\\n\"\n \" [yellow]/help[/] show this help\\n\"\n \" [yellow]/exit[/] quit\\n\"\n \" [yellow]/clear[/] clear conversation history\\n\"\n \" [yellow]/files[/] list files in workdir\\n\"\n \" [yellow]/tokens[/] show token usage\\n\"\n f\"{repl_hint}\\n\"\n \"[bold]Usage:[/]\\n\"\n \" Just type your question or task, e.g.:\\n\"\n ' [dim]\"read main.py and find issues\"[/]\\n'\n ' [dim]\"add a logging function to utils.py\"[/]\\n'\n ' [dim]\"search for all uses of requests\"[/]',\n title=\"Help\",\n border_style=\"yellow\",\n ))\n else:\n print(f\"\\n {C.BOLD}Commands:{C.RESET}\")\n print(f\" {C.YELLOW}/help{C.RESET} show this help\")\n print(f\" {C.YELLOW}/exit{C.RESET} quit\")\n print(f\" {C.YELLOW}/clear{C.RESET} clear conversation history\")\n print(f\" {C.YELLOW}/files{C.RESET} list files in workdir\")\n print(f\" {C.YELLOW}/tokens{C.RESET} show token usage\")\n if HAS_PROMPT_TOOLKIT:\n print(f\" {C.DIM}--------------------------------------{C.RESET}\")\n print(f\" {C.BOLD}REPL Shortcuts:{C.RESET}\")\n print(f\" {C.YELLOW}↑/↓{C.RESET} navigate command history\")\n print(f\" {C.YELLOW}Tab{C.RESET} auto-complete /commands\")\n print(f\" {C.YELLOW}Ctrl+R{C.RESET} search command history\")\n print(f\" {C.DIM}--------------------------------------{C.RESET}\")\n print(f\" {C.BOLD}Usage:{C.RESET}\")\n print(f\" Just type your question or task, e.g.:\")\n print(f' {C.DIM}\"read main.py and find issues\"{C.RESET}')\n print(f' {C.DIM}\"add a logging function to utils.py\"{C.RESET}')\n print(f' {C.DIM}\"search for all uses of requests\"{C.RESET}\\n')\n\n\n# ── prompt_toolkit 增强交互 ──────────────────────────────────\n# 命令补全器:为 /help, /exit 等斜杠命令提供 Tab 补全\nclass SlashCommandCompleter(Completer):\n \"\"\"斜杠命令补全器,支持 Tab 自动补全。\"\"\"\n\n COMMANDS = {\n \"/help\": \"show this help\",\n \"/exit\": \"quit\",\n \"/clear\": \"clear conversation history\",\n \"/files\": \"list files in workdir\",\n \"/tokens\": \"show token usage\",\n }\n\n def get_completions(self, document, complete_event):\n text = document.text_before_cursor\n # 只在输入以 / 开头时触发补全\n if text.startswith(\"/\"):\n for cmd, desc in self.COMMANDS.items():\n if cmd.startswith(text):\n yield Completion(\n cmd,\n start_position=-len(text),\n display=f\"{cmd} ({desc})\",\n )\n\n\ndef create_prompt_session() -> \"PromptSession | None\":\n \"\"\"\n 创建 prompt_toolkit 的 PromptSession。\n\n 功能:\n - 命令历史:持久化到 ~/.mca/history,跨会话保留\n - Tab 补全:输入 / 时自动补全斜杠命令\n - 多行编辑:Shift+Enter 换行,Enter 提交\n - Emacs/Vi 模式:自动检测用户偏好\n\n Returns:\n PromptSession 实例,或 None(如果 prompt_toolkit 未安装)\n \"\"\"\n if not HAS_PROMPT_TOOLKIT:\n return None\n\n # 历史文件路径:~/.mca/history\n history_dir = Path.home() / \".mca\"\n history_dir.mkdir(parents=True, exist_ok=True)\n history_file = history_dir / \"history\"\n\n return PromptSession(\n history=FileHistory(str(history_file)),\n completer=SlashCommandCompleter(),\n multiline=False, # 单行模式,与原始 REPL 行为一致\n enable_history_search=True, # Ctrl+R 搜索历史\n )\n\n\ndef _prompt_style():\n \"\"\"返回 prompt_toolkit 的样式配置。\"\"\"\n if not HAS_PROMPT_TOOLKIT:\n return None\n from prompt_toolkit.styles import Style\n\n return Style.from_dict({\n \"prompt\": \"bold ansigreen\", # >>> 提示符:绿色加粗,与原始 REPL 一致\n })\n\n\n# ── 核心:Agent Loop ───────────────────────────────────────────\ndef run_agent_loop(llm: LLMClient, tools: ToolSystem, context: Context):\n \"\"\"\n Agent 循环——编程助手的核心引擎。\n\n 流程:\n 1. 把对话历史(含用户最新消息)发给 LLM(流式)\n 2. LLM 返回回复:\n a. 如果包含 tool_calls → 执行工具,把结果加入历史,回到步骤 1\n b. 如果只有文本 → 这是最终回复,显示给用户,结束循环\n\n 流式输出原理:\n 使用 chat_stream() 逐块接收 LLM 响应,文本增量实时打印,\n 工具调用增量逐步拼接。用户无需等待完整响应即可看到输出。\n\n 这个循环会重复执行,直到:\n - LLM 不再请求工具调用(给出最终回复)\n - 达到最大迭代次数(防止无限循环)\n - 发生错误\n \"\"\"\n MAX_ITERATIONS = 20 # 安全限制:防止 LLM 陷入无限工具调用循环\n\n # 记录 Agent Loop 开始前的消息数,用于检测是否发生了摘要\n msg_count_before = len(context.messages)\n\n for iteration in range(MAX_ITERATIONS):\n # ── 流式调用 LLM ────────────────────────────────────\n # 逐块接收响应,实时显示文本,拼接工具调用\n content = \"\" # 累积完整文本\n tool_calls_map = {} # 累积工具调用:{index: {id, function: {name, arguments}}}\n finish_reason = None\n has_error = False\n\n # ── 实时显示文本流 ──────────────────────────────────\n # 流式输出时,文本片段逐个到达,立即打印让用户看到\n if HAS_RICH:\n _console.print() # 输出前空一行\n else:\n print() # 输出前空一行\n\n for chunk in llm.chat_stream(context.messages, tools.definitions):\n chunk_type = chunk[\"type\"]\n\n # ── 文本增量:实时打印 ──────────────────────────\n if chunk_type == \"text_delta\":\n text_delta = chunk[\"content\"]\n content += text_delta\n # 逐块打印,无需等待完整响应\n if HAS_RICH:\n _console.print(text_delta, end=\"\", highlight=False)\n else:\n print(f\"{C.CYAN}{text_delta}{C.RESET}\", end=\"\", flush=True)\n\n # ── 工具调用增量:逐步拼接 ──────────────────────\n elif chunk_type == \"tool_call_delta\":\n idx = chunk[\"index\"]\n if idx not in tool_calls_map:\n # 第一个 chunk:包含 id 和 name\n tool_calls_map[idx] = {\n \"id\": chunk.get(\"id\") or \"\",\n \"type\": \"function\",\n \"function\": {\"name\": \"\", \"arguments\": \"\"},\n }\n tc = tool_calls_map[idx]\n # 拼接 id\n if chunk.get(\"id\"):\n tc[\"id\"] = chunk[\"id\"]\n # 拼接 name\n if chunk.get(\"name\"):\n tc[\"function\"][\"name\"] = chunk[\"name\"]\n # 拼接 arguments 片段\n if chunk.get(\"arguments_delta\"):\n tc[\"function\"][\"arguments\"] += chunk[\"arguments_delta\"]\n\n # ── 流结束 ──────────────────────────────────────\n elif chunk_type == \"done\":\n finish_reason = chunk[\"finish_reason\"]\n\n # ── 错误 ────────────────────────────────────────\n elif chunk_type == \"error\":\n has_error = True\n if HAS_RICH:\n _console.print(f\"\\n [yellow]{chunk['content']}[/]\")\n else:\n print(f\"\\n {C.YELLOW}{chunk['content']}{C.RESET}\")\n return\n\n # 流式输出结束,换行\n if HAS_RICH:\n _console.print()\n else:\n print()\n\n # ── 将工具调用 map 转为列表格式 ─────────────────────\n # 与非流式 chat() 返回的 tool_calls 格式保持一致\n tool_calls = []\n for idx in sorted(tool_calls_map.keys()):\n tool_calls.append(tool_calls_map[idx])\n\n # ── 没有工具调用 → LLM 给出了最终回复,结束循环 ──\n if not tool_calls:\n context.add_assistant_message(content)\n _print_token_usage(context, msg_count_before)\n return\n\n # ── 有工具调用 → 记录助手消息(含 tool_calls)─────\n context.add_assistant_message(content, tool_calls)\n\n # ── 执行每个工具调用 ──────────────────────────────\n for tc in tool_calls:\n func = tc[\"function\"]\n name = func[\"name\"]\n\n # 解析参数(LLM 返回的是 JSON 字符串)\n try:\n args = json.loads(func[\"arguments\"])\n except json.JSONDecodeError:\n args = {}\n\n # 展示工具调用信息\n args_str = \", \".join(f\"{k}={repr(v)[:60]}\" for k, v in args.items())\n if HAS_RICH:\n _console.print(f\" [dim]tool:[/] [bold]{name}[/]({args_str})\")\n else:\n print(f\"\\n {C.DIM}tool: {C.RESET}{name}({args_str})\")\n\n # 执行工具\n result = tools.execute(name, args)\n\n # 展示结果(截断过长的输出)\n preview = result\n if len(preview) > 600:\n preview = preview[:600] + f\"\\n ...(total {len(result)} chars)\"\n if HAS_RICH:\n _console.print(Panel(\n preview,\n title=f\"[dim]result:[/] {name}\",\n border_style=\"dim\",\n padding=(0, 1),\n ))\n else:\n print(f\" {C.DIM}result:{C.RESET}\\n{preview}\")\n\n # 把工具结果加入对话历史(LLM 下一轮会看到)\n context.add_tool_result(tc[\"id\"], result)\n\n # 达到最大迭代次数\n if HAS_RICH:\n _console.print(f\"\\n [yellow]Max iterations ({MAX_ITERATIONS}) reached.[/]\")\n else:\n print(f\"\\n {C.YELLOW}Max iterations ({MAX_ITERATIONS}) reached.{C.RESET}\")\n _print_token_usage(context, msg_count_before)\n\n\ndef _print_token_usage(context: Context, msg_count_before: int):\n \"\"\"打印 token 用量和摘要通知。\"\"\"\n token_count = context.token_count()\n token_str = format_token_count(token_count, context.context_limit)\n\n # 检查是否发生了摘要(消息数减少了)\n summarized = len(context.messages) < msg_count_before\n\n if HAS_RICH:\n parts = [f\"[dim]tokens:[/] {token_str}\"]\n if summarized:\n parts.append(\"[yellow](auto-summarized)[/]\")\n if context._summary:\n parts.append(f\"[dim]({len(context.messages)} msgs)[/]\")\n _console.print(\" \" + \" \".join(parts))\n else:\n parts = [f\"{C.DIM}tokens:{C.RESET} {token_str}\"]\n if summarized:\n parts.append(f\"{C.YELLOW}(auto-summarized){C.RESET}\")\n if context._summary:\n parts.append(f\"{C.DIM}({len(context.messages)} msgs){C.RESET}\")\n print(\" \" + \" \".join(parts))\n\n\n# ── 主函数 ─────────────────────────────────────────────────────\ndef main():\n # ── 解析命令行参数 ──────────────────────────────────────\n parser = argparse.ArgumentParser(\n prog=\"mca\",\n description=\"Mini Code Assistant - a mini AI coding assistant for learning\",\n epilog=(\n \"Config via env vars or .env file: API_KEY, BASE_URL, MODEL\\n\"\n \" Local config: ./.env\\n\"\n \" Global config: ~/.mca/.env\"\n ),\n )\n parser.add_argument(\n \"directory\",\n nargs=\"?\",\n default=\".\",\n help=\"working directory (default: current directory)\",\n )\n parser.add_argument(\n \"-m\", \"--model\",\n help=\"override model name (e.g. gpt-4o, deepseek-chat)\",\n )\n parser.add_argument(\n \"-k\", \"--api-key\",\n help=\"override API key\",\n )\n parser.add_argument(\n \"-u\", \"--base-url\",\n help=\"override API base URL\",\n )\n parser.add_argument(\n \"-t\", \"--temperature\",\n type=float,\n help=\"override sampling temperature (default: 0.3)\",\n )\n parser.add_argument(\n \"-V\", \"--version\",\n action=\"version\",\n version=f\"Mini Code Assistant {__version__}\",\n )\n args = parser.parse_args()\n\n # ── 加载配置 ────────────────────────────────────────────\n load_env()\n\n # 配置优先级:命令行参数 > 环境变量 > .env 文件 > 默认值\n api_key = args.api_key or os.getenv(\"API_KEY\", \"\")\n base_url = args.base_url or os.getenv(\"BASE_URL\", \"https://api.openai.com/v1\")\n model = args.model or os.getenv(\"MODEL\", \"gpt-4o\")\n temperature = args.temperature if args.temperature is not None else float(os.getenv(\"TEMPERATURE\", \"0.3\"))\n\n # 工作目录\n working_dir = Path(args.directory).resolve()\n if not working_dir.exists():\n print(f\"Error: working directory does not exist: {working_dir}\")\n sys.exit(1)\n\n if not api_key:\n print(\"Error: API_KEY is required.\")\n print(\"\\nYou can configure it via:\")\n print(\" 1. CLI flag: mca --api-key sk-xxx\")\n print(\" 2. Local env: create .env in current directory\")\n print(\" 3. Global env: create ~/.mca/.env\")\n print(\" 4. Shell env: export API_KEY=sk-xxx\")\n sys.exit(1)\n\n # 初始化核心组件\n llm = LLMClient(api_key, base_url, model, temperature=temperature)\n tools = ToolSystem(working_dir)\n context = Context(working_dir, llm=llm, model=model)\n\n print_banner(working_dir, model, context.context_limit)\n\n # ── 创建增强 REPL 会话(如果 prompt_toolkit 可用)────\n session = create_prompt_session()\n\n # ── REPL 循环 ──────────────────────────────────────\n while True:\n try:\n if session is not None:\n # prompt_toolkit 增强 REPL:支持历史、补全、搜索\n user_input = session.prompt(\n [(\"class:prompt\", \">>> \")],\n style=_prompt_style() if HAS_PROMPT_TOOLKIT else None,\n ).strip()\n else:\n # 降级为原始 input()\n user_input = input(f\"{C.GREEN}{C.BOLD}>>> {C.RESET}\").strip()\n except (EOFError, KeyboardInterrupt):\n if HAS_RICH:\n _console.print(\"[dim]Bye![/]\\n\")\n else:\n print(f\"\\n{C.DIM}Bye!{C.RESET}\\n\")\n break\n\n if not user_input:\n continue\n\n # ── 处理特殊命令 ─────────────────────────────────\n if user_input.startswith(\"/\"):\n cmd = user_input.lower()\n if cmd == \"/exit\":\n if HAS_RICH:\n _console.print(\"[dim]Bye![/]\\n\")\n else:\n print(f\"{C.DIM}Bye!{C.RESET}\\n\")\n break\n elif cmd == \"/help\":\n print_help()\n continue\n elif cmd == \"/clear\":\n context.clear()\n if HAS_RICH:\n _console.print(\"[dim]Conversation history cleared.[/]\\n\")\n else:\n print(f\"{C.DIM}Conversation history cleared.{C.RESET}\\n\")\n continue\n elif cmd == \"/files\":\n result = tools.execute(\"list_files\", {})\n print(result)\n continue\n elif cmd == \"/tokens\":\n token_str = format_token_count(context.token_count(), context.context_limit)\n msg_count = len(context.messages)\n if HAS_RICH:\n _console.print(f\"[dim]messages:[/] {msg_count} [dim]tokens:[/] {token_str}\", style=\"white\")\n if context._summary:\n _console.print(\"[dim]context has been auto-summarized[/]\")\n else:\n print(f\"\\n {C.DIM}messages:{C.RESET} {msg_count} {C.DIM}tokens:{C.RESET} {token_str}\")\n if context._summary:\n print(f\" {C.DIM}(context has been auto-summarized){C.RESET}\")\n print()\n continue\n else:\n if HAS_RICH:\n _console.print(f\"[yellow]Unknown command: {user_input}[/]\")\n else:\n print(f\"{C.YELLOW}Unknown command: {user_input}{C.RESET}\")\n continue\n\n # ── 正常对话:启动 Agent Loop ────────────────────\n context.add_user_message(user_input)\n try:\n run_agent_loop(llm, tools, context)\n except KeyboardInterrupt:\n if HAS_RICH:\n _console.print(\"[yellow]Task interrupted.[/]\")\n else:\n print(f\"\\n{C.YELLOW}Task interrupted.{C.RESET}\")\n except Exception as e:\n if HAS_RICH:\n _console.print(f\"[yellow]Error: {e}[/]\")\n else:\n print(f\"\\n{C.YELLOW}Error: {e}{C.RESET}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}}}}