From cb305b4e95fdacc9bfd6a839cf1e939a6f10c886 Mon Sep 17 00:00:00 2001 From: seal Date: Sat, 8 Aug 2026 12:24:40 +0800 Subject: [PATCH 1/2] docs --- CLAUDE.md | 53 ++ dev/deepseek.toml | 24 + ...46\347\273\206\346\226\271\346\241\210.md" | 901 ++++++++++++++++++ ...71\347\233\256\350\257\204\344\274\260.md" | 68 ++ ...71\347\233\256\350\257\264\346\230\216.md" | 189 ++++ docs/mk/resume-kimi-code-cli.html | 649 +++++++++++++ ...26\346\216\222\346\226\271\346\241\210.md" | 205 ++++ ...04\346\265\213\346\226\271\346\241\210.md" | 487 ++++++++++ ...03\350\257\225\346\214\207\345\215\227.md" | 135 +++ ...\347\256\200\345\216\206-Kimi-Code-CLI.md" | 140 +++ docs/zh/configuration/env-vars.md | 18 + docs/zh/reference/kimi-command.md | 3 +- pyproject.toml | 1 + src/kimi_cli/__main__.py | 26 +- src/kimi_cli/app.py | 79 +- src/kimi_cli/cli/__init__.py | 87 +- src/kimi_cli/llm.py | 29 +- src/kimi_cli/utils/dotenv.py | 25 + tests/core/test_create_llm.py | 24 +- tests/utils/test_dotenv.py | 26 + uv.lock | 2 + 21 files changed, 3146 insertions(+), 25 deletions(-) create mode 100644 CLAUDE.md create mode 100644 dev/deepseek.toml create mode 100644 "docs/mk/AgentLens\347\247\213\346\213\233\351\241\271\347\233\256\350\257\246\347\273\206\346\226\271\346\241\210.md" create mode 100644 "docs/mk/Kimi Code CLI\346\240\270\345\277\203\347\237\255\346\235\277\344\270\216\347\247\213\346\213\233\351\241\271\347\233\256\350\257\204\344\274\260.md" create mode 100644 "docs/mk/Kimi Code CLI\347\247\213\346\213\233\347\256\200\345\216\206\351\241\271\347\233\256\350\257\264\346\230\216.md" create mode 100644 docs/mk/resume-kimi-code-cli.html create mode 100644 "docs/mk/\346\214\201\344\271\205\345\214\226\351\241\271\347\233\256\350\256\260\345\277\206\344\270\216Repo\344\273\243\347\240\201\346\231\272\350\203\275\347\274\226\346\216\222\346\226\271\346\241\210.md" create mode 100644 "docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" create mode 100644 "docs/mk/\346\272\220\347\240\201\346\236\266\346\236\204\344\270\216\350\260\203\350\257\225\346\214\207\345\215\227.md" create mode 100644 "docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI.md" create mode 100644 src/kimi_cli/utils/dotenv.py create mode 100644 tests/utils/test_dotenv.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..cf32280763 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概览 + +Kimi CLI(`kimi-cli`)是一个运行在终端里的 AI agent,用于软件开发与终端操作:读写代码、执行 shell 命令、搜索网页,并自主规划与调整动作。技术栈:Python 3.12+、Typer(CLI)、asyncio、kosong(LLM 层)、fastmcp(MCP)、loguru(日志)、uv(包管理/构建)、PyInstaller(打包二进制)。 + +> 注意:项目正逐步演进为 [Kimi Code CLI](https://github.com/MoonshotAI/kimi-code)。**`AGENTS.md` 是最权威的架构文档**(作为 `KIMI_AGENTS_MD` 注入 agent 提示词),详细的模块级架构请直接查阅它;本文件只保留高频命令与关键约定。 + +## 常用命令(优先用 uv / make) + +```sh +make prepare # 同步所有工作区依赖并安装 prek git hooks +make format # 格式化全部包(ruff check --fix + ruff format) +make check # 检查全部包(ruff + pyright;ty 非阻塞,|| true) +make test # 运行全部测试(kimi-cli + kosong + pykaos + kimi-sdk) +make test-kimi-cli # 只跑本包测试:uv run pytest tests -vv && uv run pytest tests_e2e -vv +make ai-test # AI 驱动测试:uv run tests_ai/scripts/run.py tests_ai +uv run kimi # 运行 CLI +``` + +- **单个测试**:`uv run pytest tests/core/test_create_llm.py -vv`,加 `::test_name` 定位到用例。`pytest.ini` 设置了 `asyncio_mode = auto`,异步测试无需手动 `@pytest.mark.asyncio`。 +- **构建**:`make build`(构建各 Python 包)、`make build-bin`(PyInstaller 单文件二进制,产物在 `dist/`);两者都会自动先跑 `make build-web` / `make build-vis` 内嵌 Web UI,需要 Node.js/npm。 +- **前端开发**:`make web-back` + `make web-front`(web UI,uvicorn + vite);`make vis-back` + `make vis-front`(vis 追踪可视化 UI)。 + +## 架构速览 + +调用链大致为:CLI 解析 → 应用初始化 → soul 主循环 → Wire → UI。 + +- **CLI 入口**:`src/kimi_cli/cli/__init__.py`(Typer)解析 UI 模式/agent spec/配置/MCP 等参数,路由到 `src/kimi_cli/app.py` 的 `KimiCLI`。顶层 `kimi` / `kimi-cli` 命令由 `src/kimi_cli/__main__.py` 进入。 +- **运行时**:`KimiCLI.create` 加载 `config.py` 配置、`llm.py` 选择模型/提供商、构建 `soul/agent.py` 里的 `Runtime`、加载 agent spec、恢复 `Context`,最后构造 `KimiSoul`。 +- **核心循环**:`src/kimi_cli/soul/kimisoul.py` 是主 agent 循环(接收输入、处理 slash 命令、追加 `Context`、调用 LLM、执行工具、必要时 `compaction.py` 压缩上下文)。 +- **Agent spec**:`src/kimi_cli/agents/` 下 YAML,由 `agentspec.py` 加载,可 `extend` 基础 spec、按 import path 选工具、注册内置 subagent 类型;系统提示词与 spec 同目录,内置参数有 `KIMI_NOW`、`KIMI_WORK_DIR`、`KIMI_AGENTS_MD` 等。 +- **工具与子 agent**:`soul/toolset.py` 按 import path 加载工具并注入依赖;内置工具在 `src/kimi_cli/tools/`(agent、shell、file、web、todo、background、dmail、think、plan)。MCP 工具经 fastmcp 加载。`LaborMarket` 注册内置 subagent 类型,`SubagentStore` 把子 agent 实例持久化到 `session/subagents//` 下。 +- **审批**:`soul/approval.py` 是对外门面,`approval_runtime/` 是会话级待审批状态源,审批请求投影到 Wire 流上供 Shell/Web UI 消费。 +- **UI / Wire**:`soul/run_soul` 把 `KimiSoul` 接到 `wire/` 的 `Wire` 上以流式输出事件;前端在 `ui/`(shell / print / acp / wire),其中 `ui/shell/` 是默认交互体验。 +- **工作区包**:`packages/kosong`(LLM 抽象层)、`packages/kaos`(pykaos,系统交互抽象)、`packages/kimi-code`、`sdks/kimi-sdk`,均在 `pyproject.toml` 的 `[tool.uv.workspace]` 中,互相通过 `[tool.uv.sources]` 关联。 + +## 约定 + +- **版本号**:仅递增 minor(`MAJOR.MINOR.PATCH`,patch 恒为 `0`),任何变更都 bump minor;major 只允许显式手动决策。此规则适用于本仓库全部包及 release/skill 工作流。 +- **提交信息**:Conventional Commits,允许类型:`feat`、`fix`、`test`、`refactor`、`chore`、`style`、`docs`、`perf`、`build`、`ci`、`revert`。 +- **代码质量**:ruff 负责 lint + format(规则 E、F、UP、B、SIM、I,行宽 100),pyright(standard 模式,`src/kimi_cli/**` 为 strict)与 ty 负责类型检查,ty 失败不阻塞。 +- **prek hooks**:commit 时会自动运行 `make format-kimi-cli` 与 `make check-kimi-cli`(见 `.pre-commit-config.yaml`),可 `git commit --no-verify` 跳过;手动全量运行用 `prek run --all-files`。 +- **测试分层**:`tests/` 单元测试、`tests_e2e/` 端到端、`tests_ai/` 由 agent 驱动(`make ai-test`)。 +- **用户数据**:配置文件 `~/.kimi/config.toml`,日志、会话、MCP 配置在 `~/.kimi/`。 +- **文档**:用户在 `docs/zh/` 与 `docs/en/`(Vitepress),PR 通常会同步更新中文/英文文档。 + +## 发布与技能 + +- **发布流程**:严格遵循 `release` 技能(`.agents/skills/release/SKILL.md`)——新建 `bump-0.xx` 分支、在 `CHANGELOG.md` 的 `## Unreleased` 下新增 `## 0.xx (YYYY-MM-DD)`、更新 `pyproject.toml` 版本、`uv sync` 对齐 `uv.lock`、PR 合并后打 tag 推送,由 GitHub Actions 完成发布。 +- 仓库自带若干 agent 技能(`feature-smoke-test`、`gen-changelog`、`gen-docs`、`pull-request`、`translate-docs` 等),可在 `.agents/skills/` 查看。 diff --git a/dev/deepseek.toml b/dev/deepseek.toml new file mode 100644 index 0000000000..0774c97cf1 --- /dev/null +++ b/dev/deepseek.toml @@ -0,0 +1,24 @@ +# Project-local DeepSeek configuration. +# Secrets and endpoint overrides belong in ../.env, not in this file. + +default_model = "deepseek-v4-flash" +default_thinking = false + +[providers.deepseek] +type = "openai_legacy" +base_url = "https://api.deepseek.com" +api_key = "" +# DeepSeek returns reasoning text in this OpenAI-compatible response field. +reasoning_key = "reasoning_content" + +[models.deepseek-v4-flash] +provider = "deepseek" +model = "deepseek-v4-flash" +max_context_size = 1000000 +capabilities = ["thinking"] + +[models.deepseek-v4-pro] +provider = "deepseek" +model = "deepseek-v4-pro" +max_context_size = 1000000 +capabilities = ["thinking"] diff --git "a/docs/mk/AgentLens\347\247\213\346\213\233\351\241\271\347\233\256\350\257\246\347\273\206\346\226\271\346\241\210.md" "b/docs/mk/AgentLens\347\247\213\346\213\233\351\241\271\347\233\256\350\257\246\347\273\206\346\226\271\346\241\210.md" new file mode 100644 index 0000000000..ea740bbcd4 --- /dev/null +++ "b/docs/mk/AgentLens\347\247\213\346\213\233\351\241\271\347\233\256\350\257\246\347\273\206\346\226\271\346\241\210.md" @@ -0,0 +1,901 @@ +# AgentLens:面向 Coding Agent 的可观测、诊断与评测平台 + +> 文档定位:秋招项目立项与开发执行方案;基础项目:Kimi Code CLI;建议周期:8 周,单人每周 +> 投入 15~25 小时;项目关键词:AI Agent、可观测性、事件驱动、异步系统、故障诊断、评测平台。 + +## 1. 项目摘要 + +AgentLens 不是再实现一个聊天界面,也不是简单统计 Token。它要解决的是 Coding Agent +在真实软件工程任务中“失败过程不可解释、优化效果不可量化”的问题。 + +用户运行 Coding Agent 后,通常只能看到最终成功或失败,却难以回答以下问题: + +- 失败最早发生在哪一步? +- 是模型决策错误、工具错误、环境错误,还是上下文退化? +- Agent 为什么重复读取文件、重复运行命令或持续无效重试? +- 某次 Prompt、模型或运行时改造究竟提高了成功率,还是仅增加了 Token 消耗? +- 不同版本的 Agent 在相同任务上的执行路径有什么差异? + +AgentLens 将一次 Agent 任务建模为包含 Turn、Step、Model Request、Tool Call、Approval、 +Compaction 和 Subagent 的层级 Trace,在本地持久化完整执行数据,通过确定性规则识别常见 +失败模式,并使用可重复的数据集运行 A/B 评测。最终形成以下闭环: + +```text +执行任务 -> 采集 Trace -> 自动诊断 -> 定位根因 -> 修改 Agent -> 回归评测 -> 对比收益 +``` + +### 1.1 与仓库现有能力的关系 + +仓库已经存在技术预览版 `kimi vis`,支持: + +- 浏览 `wire.jsonl` 事件时间线; +- 查看 `context.jsonl` 上下文; +- 展示 Token、工具次数、错误数等基础统计; +- 浏览主 Agent 和子 Agent; +- 导入、导出历史会话。 + +因此,AgentLens 不应重新制作一个“日志查看器”。它应在现有 Visualizer 上增加当前缺失的 +能力: + +| 能力 | 当前基线 | AgentLens 目标 | +|---|---|---| +| 执行记录 | 扁平 Wire 事件 | 具有父子关系的结构化 Span | +| 耗时分析 | 根据相邻事件时间推算 | 精确记录模型、工具、审批、压缩耗时 | +| 错误展示 | 显示错误事件 | 自动归类失败模式并给出证据 | +| 历史查询 | 每次扫描 JSONL | SQLite 索引、筛选和聚合 | +| 版本比较 | 无 | 相同任务的成对 Trace Diff | +| 评测 | 无 | 数据集适配、隔离运行、评分和报告 | +| 隐私 | 会话文件包含原始内容 | 默认脱敏、字段白名单和保留策略 | +| 优化闭环 | 依赖人工观察 | 诊断规则与评测指标关联 | + +## 2. 解决的问题与预期效果 + +### 2.1 问题一:执行链路缺少统一因果关系 + +目前 `wire.jsonl` 能记录发生了什么,但 Turn、Step、LLM 请求、并行 Tool Call 和 Subagent +之间没有统一的父子 Span 模型。并行工具调用时,仅依靠相邻时间戳无法可靠计算耗时和关键 +路径。 + +AgentLens 为每个事件增加 `trace_id`、`span_id`、`parent_span_id`、开始/结束时间、状态和 +结构化属性,使一次任务可以还原为调用树,并计算关键路径、并行度和各阶段耗时占比。 + +预期效果:开发者能从失败任务直接跳转到最早异常 Span,明确其上游决策和下游影响。 + +### 2.2 问题二:错误信息存在,但根因仍依赖人工阅读 + +工具返回错误不一定是根因。例如,测试失败可能源于错误修改,错误修改可能源于读取了过期 +上下文;反过来,一次可恢复的命令失败也不应直接判定整个任务失败。 + +AgentLens 首期使用可解释的确定性规则检测: + +- `RepeatedToolLoop`:相同工具与参数跨 Step 重复,且没有产生新状态; +- `RetryStorm`:短时间内对同一模型请求或命令多次失败重试; +- `ToolFailureCascade`:某个工具错误后触发多个下游错误; +- `NoProgress`:连续多个 Step 没有文件变化、测试状态变化或新证据; +- `ContextPressure`:上下文持续接近上限,压缩后又快速膨胀; +- `CompactionRegression`:压缩后丢失约束,出现重复探索或行为反转; +- `ApprovalBottleneck`:审批等待占任务耗时比例过高; +- `SlowOperation`:模型或工具耗时超过动态分位数阈值; +- `PrematureStop`:Agent 结束任务,但验收测试未通过或工作区仍存在明确失败; +- `SubagentWaste`:子 Agent 结果未被消费,或多个子 Agent 重复完成相同探索。 + +每条诊断必须输出严重级别、置信度、涉及 Span、触发证据和建议动作。第一阶段不使用 LLM +直接判定根因,避免诊断器本身不可复现;LLM 只能作为可选的自然语言总结层。 + +预期效果:将“翻阅完整日志”转变为“先查看 1~3 个高优先级 Finding,再验证证据”。 + +### 2.3 问题三:Agent 改造缺少同条件对比 + +模型输出具有随机性,只比较两个成功 Demo 没有意义。AgentLens 的 Eval Runner 固定数据集、 +模型、Token 预算、超时、工具权限和环境镜像,对 baseline 与 candidate 进行成对实验,并保存 +每次运行的补丁、测试结果和 Trace。 + +预期效果:能够回答以下工程问题: + +- 新的重复调用保护是否减少了工具调用,同时没有降低成功率? +- 新的压缩策略是否节省 Token,但导致关键约束遗忘? +- 某类任务成功率下降,是模型变化还是环境失败造成的? + +### 2.4 问题四:原始轨迹包含隐私和存储风险 + +Agent 轨迹可能包含代码、路径、Prompt、Shell 输出和密钥。AgentLens 采用本地优先设计: + +- 默认只持久化诊断所需的元数据和经过脱敏的内容; +- 对文件路径、URL、环境变量、Token 形态应用脱敏器; +- 原始 Prompt、工具参数和输出采用显式 opt-in; +- 支持按天数、数据库大小或项目范围清理; +- 导出时再次执行脱敏检查并输出隐私清单; +- 不复用远程 Telemetry 作为详细 Trace 存储。 + +### 2.5 开发前如何验证这是真实痛点 + +在写代码前访谈 8~12 名使用过 Cursor、Claude Code、Codex 或其他 Coding Agent 的开发者, +不要直接询问“你是否需要可观测平台”,而是让他们回忆最近一次失败任务:当时如何发现失败、 +看了哪些信息、花了多久、最后是否定位成功。争取收集至少 30 条匿名失败案例,按“模型决策、 +工具、环境、上下文、权限、任务验收”编码。 + +立项继续条件建议设为:超过一半受访者在过去一个月遇到过无法快速解释的 Agent 失败,且原始 +日志定位的中位耗时超过 10 分钟。若真实反馈集中在模型效果而非诊断困难,则应缩小 AgentLens +范围,把重点转为 Eval Runner 和版本对比,不应为了既定方案忽略调研结论。 + +## 3. 产品范围 + +### 3.1 MVP 必须完成 + +1. 本地层级 Trace:Turn、Step、模型调用、工具调用、审批和压缩。 +2. SQLite TraceStore:支持按会话、任务、模型、状态和时间查询。 +3. 至少 6 条确定性诊断规则,能够展示证据 Span。 +4. `kimi lens show`、`kimi lens diagnose`、`kimi lens compare` 三个 CLI 命令。 +5. 在现有 `kimi vis` 中加入 Trace Tree、Findings 和 Compare 页面。 +6. Eval Runner:能够运行自建任务集和一个公开数据集适配器。 +7. 自动生成 JSON 与 HTML 评测报告。 +8. 完成基线实验、消融实验和性能开销测试。 + +### 3.2 加分项 + +- OpenTelemetry JSON/OTLP 导出; +- 跨主 Agent、子 Agent 的关键路径分析; +- 实时 WebSocket Trace 更新; +- 基于历史分位数的异常阈值; +- 对失败轨迹生成可分享的脱敏复现包; +- CI 中对关键 Agent 任务做小规模回归。 + +### 3.3 首期不做 + +- 不实现通用云端日志平台; +- 不存储或展示模型隐藏推理; +- 不承诺对真实 Shell 副作用进行任意重放; +- 不以另一个 LLM 的主观打分代替可执行测试; +- 不在第一版引入复杂向量数据库或训练分类模型; +- 不同时改造 Agent 决策、上下文策略、安全策略和多 Agent 调度。 + +这里的“Replay”默认指确定性回放已有事件和离线重新诊断。真正重新执行任务只允许发生在 +Eval Runner 创建的隔离环境中,避免重复发送网络请求或执行破坏性命令。 + +## 4. 总体架构 + +```text +KimiSoul / Toolset / Approval / Compaction / Subagent + | + v + Local Observability Bus + | | + v v + JSONL compatibility Trace Recorder + | + v + SQLite TraceStore + | | | + v v v + Query Analyzer Exporter + \ | / + \ | / + CLI + Vis + | + v + Eval Runner / Compare Report +``` + +设计原则: + +- 核心运行路径只负责发出事件,不执行复杂分析; +- 写入采用有界队列和批处理,不阻塞 Agent 主循环; +- 诊断器只依赖稳定的 Trace 模型,不直接解析 UI 数据; +- 旧 `wire.jsonl` 可以离线导入,保证历史会话可用; +- Schema 带版本号,新增字段保持向后兼容; +- 同一个 Agent 执行与评测逻辑共用一套采集链路。 + +## 5. 数据模型 + +### 5.1 Trace 与 Span + +建议定义以下核心模型: + +```python +class SpanKind(StrEnum): + TURN = "turn" + STEP = "step" + MODEL = "model" + TOOL = "tool" + APPROVAL = "approval" + COMPACTION = "compaction" + SUBAGENT = "subagent" + +class SpanStatus(StrEnum): + RUNNING = "running" + OK = "ok" + ERROR = "error" + CANCELLED = "cancelled" + +class TraceSpan(BaseModel): + schema_version: int + trace_id: str + span_id: str + parent_span_id: str | None + session_id: str + task_id: str | None + kind: SpanKind + name: str + started_at_ns: int + ended_at_ns: int | None + status: SpanStatus + attributes: dict[str, JsonValue] +``` + +时间使用 wall clock 与 monotonic duration 分开记录:wall clock 用于跨事件展示,monotonic +用于精确计算进程内耗时,避免系统时间调整导致负数。 + +### 5.2 SQLite 表 + +建议使用 Python 内置 `sqlite3`,减少生产依赖: + +| 表 | 作用 | 关键字段 | +|---|---|---| +| `trace_runs` | 一次 Agent 任务 | run_id、session_id、model、git_sha、status | +| `spans` | 层级执行单元 | span_id、parent_span_id、kind、duration、status | +| `span_events` | Span 内瞬时事件 | event_id、span_id、name、timestamp、attributes | +| `findings` | 自动诊断结果 | rule_id、severity、confidence、evidence_span_ids | +| `eval_cases` | 评测任务定义 | dataset、case_id、repo、base_commit | +| `eval_attempts` | 单次评测运行 | variant、seed、resolved、tokens、duration、patch | +| `schema_meta` | 数据库迁移版本 | schema_version、migrated_at | + +数据库启用 WAL 模式;Span 和 Event 通过异步队列批量写入。大段文本不直接进入常用索引表, +而是存入受保留策略管理的 blob 表或会话附件,数据库只保留摘要、Hash 和引用。 + +## 6. 详细开发流程与代码改造点 + +### 阶段 0:冻结基线与隔离开发环境 + +当前工作区已经存在未提交修改。正式开发时应先保护这些改动,再建立独立分支或 worktree, +不要把 AgentLens 与现有环境变量相关修改混在同一个提交中。 + +建议分支:`codex/agentlens`。 + +先记录以下基线: + +- `make check`、`make test` 当前结果; +- `kimi vis` 当前页面截图和功能清单; +- 10 个固定任务上的成功率、Token、工具调用数和耗时; +- 一个包含工具错误、重复调用、压缩和子 Agent 的样例会话。 + +交付物:`docs/agentlens/baseline.md` 和可重复运行的 baseline 配置。 + +### 阶段 1:定义稳定 Trace Schema + +新增目录: + +```text +src/kimi_cli/observability/ +├── __init__.py +├── models.py +├── context.py +├── bus.py +├── recorder.py +├── redaction.py +└── schema.py +``` + +具体工作: + +1. `models.py` 定义 Trace、Span、Event 和 Finding 的 Pydantic 模型。 +2. `context.py` 使用 `ContextVar` 保存当前 trace/span,使并发工具和子 Agent 自动继承父上下文。 +3. `bus.py` 提供轻量 `emit()` 接口和有界 `asyncio.Queue`。 +4. `recorder.py` 批量消费队列并写入 TraceStore。 +5. `redaction.py` 对密钥、Authorization、Cookie、环境变量、用户目录和 URL 参数脱敏。 +6. `schema.py` 声明 Schema 版本和迁移策略。 + +不要直接扩展现有 `telemetry.track()` 来存详细 Trace。现有 Telemetry 的属性被限制为标量,且 +其设计目标是远程、匿名的产品统计;AgentLens 需要本地、层级、可查询的丰富事件。两者可以在 +同一调用点分别发事件,但必须保持数据边界清晰。 + +测试文件: + +```text +tests/observability/test_models.py +tests/observability/test_context.py +tests/observability/test_bus.py +tests/observability/test_redaction.py +``` + +验收标准:并发创建 1 万个事件不出现 ID 冲突;队列满时有明确的 drop 计数;敏感样例不落盘。 + +### 阶段 2:实现本地 TraceStore + +新增: + +```text +src/kimi_cli/observability/store.py +src/kimi_cli/observability/migrations/ +tests/observability/test_store.py +tests/observability/test_migrations.py +``` + +需要实现: + +- 数据库首次初始化; +- WAL、busy timeout 和事务批量写; +- Span 开始与结束的幂等 upsert; +- 按 run、session、kind、status、时间范围查询; +- 数据库损坏时降级,不影响 Agent 主任务; +- 数据保留与 vacuum; +- 从旧 `wire.jsonl` 导入历史会话; +- Trace 导出为脱敏 JSON。 + +建议数据库位置为 `~/.kimi/agentlens/traces.db`,测试必须通过依赖注入使用临时目录,不能读写 +真实用户数据。 + +验收标准:异常退出后已完成批次仍可查询;重复导入同一会话不产生重复记录;10 万 Span 查询 +P95 小于 200 ms。 + +### 阶段 3:插桩核心执行链路 + +#### `src/kimi_cli/app.py` + +- 创建并启动 Recorder; +- 将 Recorder 注入 Runtime; +- 进程退出时限时 flush; +- 配置关闭 AgentLens 时不产生额外数据库文件。 + +#### `src/kimi_cli/soul/kimisoul.py` + +- `_turn()`:创建 Turn Span; +- `_agent_loop()`:每个 Step 创建子 Span; +- `_step()`:创建 Model Span,记录模型名、请求轮次、首 Token 延迟、总耗时和 TokenUsage; +- 重试路径:记录 attempt、wait、错误类型和状态码; +- 强制停止、用户中断、最大步数等写入结束原因; +- `compact_context()`:记录压缩前后 Token、耗时和压缩率。 + +禁止记录模型隐藏推理。Prompt 内容默认只保存长度、消息数、角色分布和稳定 Hash;只有用户 +显式开启 `capture_content` 后才保存脱敏内容。 + +#### `src/kimi_cli/soul/toolset.py` + +- 为每次真实 Tool Call 创建 Tool Span; +- 记录工具名、参数 Hash、参数大小、结果大小、状态与精确耗时; +- 记录 same-step dedup、cross-step repeat、hook block 和 cancellation; +- 并行工具调用分别创建子 Span,不能用单一全局变量关联; +- 结果内容默认仅保留类型、大小、Hash 和截断后的脱敏预览。 + +现有代码已经采集 `tool_call`、`tool_call_repeat` 和 `tool_call_dedup_detected` Telemetry, +AgentLens 应复用同一业务判断,避免在两个模块分别实现重复检测。 + +#### `src/kimi_cli/soul/approval.py` + +- 创建 Approval Span; +- 记录等待耗时、审批结果、审批模式和操作类型; +- 不记录可能包含路径或命令全文的 description,除非经过脱敏。 + +#### `src/kimi_cli/subagents/runner.py` 与 `src/kimi_cli/background/` + +- 子 Agent 使用独立 span_id,但共享根 trace_id; +- parent_span_id 指向创建它的 Tool 或 Step; +- 记录排队、启动、完成、失败、取消和结果是否被主 Agent 消费; +- 后台任务必须能跨 asyncio Task 传播 Trace Context。 + +#### `src/kimi_cli/wire/file.py` + +- 保持现有 Wire 协议兼容; +- 为离线适配器提供稳定读取接口; +- 不把 AgentLens 私有字段强塞进所有公开 Wire 消息; +- 如果确实新增 Wire 类型,需要同步协议版本、序列化和客户端兼容测试。 + +测试重点:正常结束、异常、取消、并行工具、重试、压缩、子 Agent 和进程退出 flush。 + +### 阶段 4:实现自动诊断引擎 + +新增: + +```text +src/kimi_cli/observability/analysis/ +├── base.py +├── engine.py +├── progress.py +└── rules/ + ├── repeated_tool_loop.py + ├── retry_storm.py + ├── tool_failure_cascade.py + ├── no_progress.py + ├── context_pressure.py + ├── approval_bottleneck.py + ├── slow_operation.py + └── premature_stop.py +``` + +统一规则接口: + +```python +class DiagnosticRule(Protocol): + rule_id: str + version: str + + def analyze(self, trace: TraceView) -> list[Finding]: ... +``` + +其中“进展”不能只等同于文件发生变化。建议综合以下信号: + +- Git diff 指纹是否变化; +- 测试失败集合是否缩小; +- Agent 是否获得新文件、符号或错误信息; +- Todo/Plan 状态是否推进; +- 最终验收命令是否改善。 + +诊断规则需要版本化。评测报告必须记录 rule version,否则同一条历史 Trace 在未来可能得到 +不同结果而无法解释。 + +验收标准:每条规则至少包含正常、边界、阳性和组合场景测试;Finding 能定位到具体证据 Span。 + +### 阶段 5:增加 CLI 查询与比较 + +新增 `src/kimi_cli/cli/lens.py`,并在懒加载命令表中注册: + +```text +kimi lens list +kimi lens show +kimi lens diagnose +kimi lens compare +kimi lens export --redacted +kimi lens gc --keep-days 30 +``` + +建议输出: + +- 任务状态和结束原因; +- 总耗时、关键路径耗时、模型/工具/等待占比; +- Token、Step、Tool Call、Retry、Compaction; +- Top Findings; +- 与另一个运行相比的绝对值和百分比变化。 + +CLI 首先完成,因为它容易测试,也能让后端能力不依赖前端进度。 + +### 阶段 6:升级现有 Visualizer + +后端新增: + +```text +src/kimi_cli/vis/api/traces.py +src/kimi_cli/vis/api/findings.py +src/kimi_cli/vis/api/comparisons.py +src/kimi_cli/vis/api/evaluations.py +``` + +前端新增: + +```text +vis/src/features/trace-tree/ +vis/src/features/findings/ +vis/src/features/run-compare/ +vis/src/features/evaluations/ +``` + +页面设计: + +1. **Trace Overview**:状态、耗时分解、Token、关键路径和失败阶段。 +2. **Span Tree**:可折叠的父子调用树,并与现有 Wire 时间线互相跳转。 +3. **Findings**:按严重级别排序,展示规则、证据、影响和建议。 +4. **Compare**:左右 Trace 对齐,比较 Step、工具序列、Token 和最终补丁。 +5. **Evaluations**:数据集、variant、成功率、成本和失败类型分布。 + +前端不重复计算核心诊断指标。指标应由 Python 后端统一生成,React 仅负责展示,避免 CLI、 +API 和页面出现三套口径。 + +### 阶段 7:实现 Eval Runner + +新增: + +```text +src/kimi_cli/evaluation/ +├── models.py +├── runner.py +├── sandbox.py +├── scorer.py +├── report.py +└── adapters/ + ├── local.py + ├── swe_bench.py + └── harbor.py + +evals/ +├── agentlens-dev.yaml +├── fault-injection/ +└── expected/ +``` + +单个 Case 至少包含: + +- `case_id`、数据集和仓库; +- base commit 或容器镜像; +- 用户任务描述; +- 安装和测试命令; +- 超时、Token 和 Step 预算; +- 成功判定; +- 可选故障标签。 + +Runner 流程: + +```text +准备隔离环境 -> 校验基线测试 -> 启动 Agent -> 收集 Trace +-> 获取 Git diff -> 执行验收测试 -> 评分 -> 保存产物 -> 清理环境 +``` + +每个任务至少保存:运行配置、stdout/stderr、生成补丁、测试报告、Trace、Finding 和资源消耗。 +环境准备失败必须标记为 `infra_error`,不能记为 Agent 失败。 + +### 6.8 代码改动总表 + +| 类型 | 文件或目录 | 改动目的 | +|---|---|---| +| 修改 | `src/kimi_cli/app.py` | 初始化、注入并关闭本地 Recorder | +| 修改 | `src/kimi_cli/soul/kimisoul.py` | Turn、Step、Model、Retry、Compaction 插桩 | +| 修改 | `src/kimi_cli/soul/toolset.py` | Tool Span、重复调用和错误属性 | +| 修改 | `src/kimi_cli/soul/approval.py` | 审批等待时间和结果 Span | +| 修改 | `src/kimi_cli/subagents/runner.py` | 主子 Agent Trace 关联 | +| 修改 | `src/kimi_cli/background/` | 后台任务的上下文传播和状态事件 | +| 修改 | `src/kimi_cli/wire/file.py` | 历史 Wire 导入所需的稳定读取能力 | +| 修改 | `src/kimi_cli/cli/_lazy_group.py` | 懒加载 `lens` 与 `eval` 子命令 | +| 新增 | `src/kimi_cli/cli/lens.py` | Trace 查询、诊断、比较、导出和清理 | +| 新增 | `src/kimi_cli/cli/eval.py` | 数据集运行和报告命令 | +| 新增 | `src/kimi_cli/observability/` | Schema、Bus、Recorder、Store、脱敏和诊断 | +| 新增 | `src/kimi_cli/evaluation/` | Sandbox、Runner、Scorer、Adapter 和报告 | +| 修改 | `src/kimi_cli/vis/app.py`、`vis/api/__init__.py` | 注册 AgentLens API | +| 新增 | `src/kimi_cli/vis/api/` 下的 Trace API | 查询 Span、Finding、Compare 和 Eval | +| 修改 | `vis/src/App.tsx`、`vis/src/lib/api.ts` | 新页面入口和 API 类型 | +| 新增 | `vis/src/features/` 下的 AgentLens 页面 | Trace Tree、Findings、Compare、Evaluations | +| 新增 | `tests/observability/`、`tests/evaluation/` | 单元、集成、迁移和性能测试 | +| 新增 | `evals/` | 固定任务配置、故障注入和预期标签 | +| 修改 | `docs/zh/reference/kimi-vis.md` 等 | CLI、隐私选项和评测使用说明 | + +每个阶段单独提交,提交信息遵循 Conventional Commits,例如 +`feat(observability): add local trace recorder`。不要在一个提交中同时加入底层 Schema、前端页面 +和 Benchmark 结果,否则评审与回滚都会困难。 + +## 7. 评测数据集方案 + +AgentLens 需要评测两类对象:一类是 Coding Agent 的任务完成能力,另一类是 AgentLens 自身的 +采集与诊断能力。只跑 SWE-bench 成功率,无法证明诊断器有效;只做故障注入,又无法证明对 +真实任务有价值。 + +### 7.1 AgentLens-Fault:自建可控故障集,必须做 + +目标:评价根因分类和故障 Span 定位。 + +构造 100~150 条轨迹,覆盖以下类别: + +- 重复读取、重复命令和跨 Step 循环; +- 工具 JSON 参数错误、工具不存在和权限拒绝; +- 网络超时、429、5xx 和重试耗尽; +- 测试持续失败但 Agent 提前结束; +- 上下文接近上限、压缩后重复探索; +- 用户拒绝审批导致路径中断; +- 并行工具中一个失败引发级联; +- 子 Agent 重复工作或结果未消费; +- 正常但耗时较长的负样本。 + +每条样本由注入器确定 ground truth:`fault_type`、`root_span_id`、`injected_at` 和预期 Finding。 +训练、调参、测试应按任务或仓库切分,不能把同一任务的不同轨迹放入不同集合。 + +核心指标: + +- 分类 Precision、Recall、Macro-F1; +- 根因 Span Top-1、Top-3 命中率; +- 首个有效 Finding 的平均排名; +- 正常轨迹误报率; +- 不同规则组合的消融结果。 + +### 7.2 SWE-bench Verified:主要真实任务集 + +[SWE-bench Verified](https://www.swebench.com/SWE-bench/faq/) 包含 500 个经过工程师验证、可解决的 +真实 GitHub issue。官方 Harness 会在 Docker 环境中应用补丁并运行测试,适合评价端到端软件 +工程任务。 + +建议使用方式: + +- 开发期:固定抽取 30 个任务,按仓库和难度分层; +- 中期:扩展到 100 个任务; +- 最终:预算允许则跑完整 500 个,否则明确报告分层 100 子集; +- baseline 与 candidate 使用相同模型、参数、预算和任务顺序; +- 每个任务建议重复 3 次,至少报告 pass@1 和平均资源消耗。 + +官方文档提示本地 Harness 建议至少 120 GB 存储、16 GB 内存和 8 核 CPU,并优先使用 x86_64; +因此 Apple Silicon 笔记本不适合作为完整评测环境。可以使用远程 x86_64 主机或官方支持的云端 +流程,具体要求见 [SWE-bench Harness](https://www.swebench.com/SWE-bench/reference/harness/)。 + +主要指标:Resolved Rate、测试通过率、Token/Resolved、秒/Resolved、工具调用/Resolved、 +Finding 类型分布和 `infra_error` 比例。 + +### 7.3 SWE-bench Lite:开发期回归 + +[SWE-bench Lite](https://github.com/SWE-bench/SWE-bench/blob/main/docs/guides/quickstart.md) 是成本更低的 +子集,适合验证适配器、容器、报告和小规模回归。它不应替代 Verified 作为唯一最终结论。 + +建议每个 PR 只运行固定 10~20 个 smoke cases,每周运行 50 个固定 cases。这样可以控制模型 +费用和执行时间,同时保持结果可比较。 + +### 7.4 SWE-bench 公共轨迹:离线真实失败分析 + +[SWE-bench experiments](https://github.com/swe-bench/experiments) 公开了部分提交的 predictions、 +执行日志、轨迹和评测结果。为常见轨迹格式编写 adapter,将其导入 AgentLens,人工标注其中 +100~200 条失败轨迹,可用于评价诊断规则在非本项目 Agent 上的泛化能力。 + +注意:公开轨迹格式和记录完整性并不统一,因此它适合作为外部验证集,不适合作为唯一 ground +truth。人工标注时应由两人独立标注一部分重叠样本,并报告一致性;如果只能单人完成,应在 +报告中明确限制。 + +### 7.5 Terminal-Bench 2.0:终端任务泛化测试 + +[Terminal-Bench 2.0](https://www.harborframework.com/docs/tutorials/running-terminal-bench) 通过官方 +Harbor Harness 在隔离终端环境运行真实工作流。其[论文](https://arxiv.org/abs/2601.11868)描述的 +数据集包含 89 个高难度任务, +不仅限于代码补丁,因此适合验证 Tool、Shell、Retry 和 NoProgress 诊断能否泛化。 + +建议最终选择 15~20 个覆盖不同工具模式的任务;算力和预算足够时再运行全部任务。它不是 +AgentLens 第一阶段的阻塞项。 + +### 7.6 Aider Polyglot:低成本编辑链路测试,可选 + +[Aider Polyglot](https://aider.chat/docs/leaderboards/) 包含 225 个来自 Exercism 的编程练习,覆盖 +C++、Go、Java、JavaScript、Python 和 Rust。它适合快速检查编辑格式、测试执行和多语言统计, +但任务规模较小、仓库探索较弱,不能代替 SWE-bench。 + +建议只抽取 Python 和另一种语言各 10~20 个任务作为 Runner 冒烟测试。 + +### 7.7 推荐的最终组合 + +| 层级 | 数据集 | 建议规模 | 用途 | +|---|---|---:|---| +| 单元级 | AgentLens-Fault | 100~150 轨迹 | 诊断分类与根因定位 | +| 快速回归 | SWE-bench Lite | 10~50 任务 | CI、适配器和成本回归 | +| 主实验 | SWE-bench Verified | 100 或 500 任务 | 真实软件工程能力 | +| 外部轨迹 | SWE-bench experiments | 100~200 轨迹 | 跨 Agent 泛化 | +| 泛化实验 | Terminal-Bench 2.0 | 15~89 任务 | Shell 与长任务诊断 | +| 可选冒烟 | Aider Polyglot | 20~40 任务 | 多语言编辑链路 | + +## 8. 指标与实验设计 + +### 8.1 采集系统指标 + +| 指标 | 计算方式 | MVP 验收目标 | +|---|---|---:| +| Event completeness | 实际落盘事件 / 预期事件 | ≥ 99% | +| Span closure rate | 正确结束的 Span / 已创建 Span | ≥ 99% | +| ID collision | 重复 span_id 数 | 0 | +| 运行耗时开销 | 开启与关闭 AgentLens 的耗时差 | P50 < 2%,P95 < 5% | +| 内存开销 | 稳态 RSS 增量 | < 50 MB | +| 写入可靠性 | 故障注入后可恢复记录比例 | ≥ 99% | +| 脱敏泄漏率 | 敏感测试样本中未脱敏比例 | 0 | + +这些数字是设计目标,不是可以直接写进简历的最终结果。 + +### 8.2 诊断质量指标 + +- Macro-F1:防止高频故障类别掩盖低频类别; +- Top-1/Top-3 root-span accuracy:诊断是否定位到真正起点; +- false positives per successful run:正常任务被误报多少次; +- evidence coverage:Finding 是否包含可验证证据; +- diagnosis latency:任务结束到完成诊断的耗时。 + +建议目标:Macro-F1 ≥ 0.80、Top-3 根因命中率 ≥ 0.85、正常轨迹严重误报率 < 5%。这些值 +必须在冻结的测试集上得到后才能用于对外文案。 + +### 8.3 人效指标 + +邀请 6~10 名有 Coding Agent 使用经验的同学,对相同失败轨迹进行交叉实验: + +- A 组只使用原始 `kimi vis`; +- B 组使用 AgentLens Findings 和 Span Tree; +- 记录定位根因耗时、答案正确率和主观信心; +- 交换工具后再做第二批任务,降低参与者能力差异影响。 + +核心指标是 Time-to-Root-Cause 和诊断正确率。若样本较少,报告中展示原始分布和中位数, +不要只给平均值。 + +### 8.4 Agent 优化收益 + +AgentLens 本身不会自动让模型变聪明。应选择诊断发现的一个高频问题,例如重复工具调用或 +重试风暴,修改 Agent 后再证明闭环价值。 + +建议实验: + +1. baseline:原始 Agent; +2. candidate:增加基于 Finding 发现的改造; +3. 固定模型、温度、任务、Token、超时和权限; +4. 每个任务成对运行; +5. 报告成功率、Token、工具调用和耗时; +6. 使用 bootstrap 计算 95% 置信区间; +7. 对成对成功/失败结果可补充 McNemar 检验。 + +建议目标是 Token 或无效工具调用降低 10%~20%,且 Resolved Rate 不下降;如果成功率还能 +获得 3~8 个百分点的提升,则是额外收益。最终按真实数据陈述,不要反向选择最好的一次运行。 + +## 9. 测试策略 + +### 9.1 单元测试 + +- 模型校验和 Schema 升级; +- Trace Context 跨 await、Task 和子 Agent 传播; +- 批量写、幂等、事务失败和数据库锁; +- 每条诊断规则的正负样本; +- 脱敏器对 Token、路径、URL、Header 和环境变量的覆盖; +- 指标计算和成对比较。 + +### 9.2 集成测试 + +- 一次完整 Turn 生成正确 Span 树; +- 并行工具调用的父子关系和耗时正确; +- 模型重试与最终成功/失败状态正确; +- Compaction 前后指标完整; +- 主 Agent 与子 Agent 共享 trace_id; +- 旧 `wire.jsonl` 可以导入; +- CLI、API 和 Web 显示相同统计结果。 + +### 9.3 端到端测试 + +- 使用本地 fake provider 和确定性工具完成一个成功任务; +- 注入工具失败并确认 Finding; +- 强制杀死进程后验证已落盘 Span; +- 在临时 Git 仓库运行 Eval Case、应用修改并执行测试; +- 生成 HTML 报告并检查关键字段。 + +### 9.4 性能测试 + +- 1 万、10 万、100 万 Span 的写入吞吐和查询延迟; +- 1、8、32 个并发工具任务; +- Recorder 队列积压和丢弃策略; +- 开关 AgentLens 的 A/B 运行开销。 + +## 10. 八周里程碑 + +| 周次 | 工作内容 | 可演示结果 | +|---|---|---| +| 第 1 周 | 基线、需求、Trace Schema、隐私模型 | 架构文档与基线报告 | +| 第 2 周 | Bus、Context、Recorder、SQLite | CLI 查询一条结构化 Trace | +| 第 3 周 | Turn/Step/Model/Tool 插桩 | 完整 Span Tree | +| 第 4 周 | Approval、Compaction、Subagent、历史导入 | 跨 Agent Trace 与旧会话导入 | +| 第 5 周 | 6~8 条诊断规则、故障注入集 | 自动 Findings 与规则评测 | +| 第 6 周 | CLI Compare、Vis 页面 | 两次运行可视化对比 | +| 第 7 周 | Eval Runner、SWE-bench 适配、报告 | 固定子集 A/B 报告 | +| 第 8 周 | 性能优化、用户实验、文档与 Demo | 最终数据、视频和简历材料 | + +如果时间只有 4 周,应砍掉 Terminal-Bench、OTLP、实时更新和复杂前端,保留 TraceStore、6 条 +规则、CLI Compare、30 个 SWE-bench 任务以及完整实验报告。 + +## 11. 风险与应对 + +| 风险 | 影响 | 应对 | +|---|---|---| +| 已有 Vis 让项目显得只是改 UI | 项目创新性不足 | 核心放在层级 Span、自动诊断与评测闭环 | +| 模型费用过高 | 无法重复实验 | 固定小型开发集,最终阶段再扩大 | +| Docker/ARM 环境不稳定 | 大量 infra error | 远程 x86_64、缓存镜像、单独统计基础设施失败 | +| 规则只适配 Kimi CLI | 泛化性差 | 导入公共 SWE-bench 轨迹和 Terminal-Bench 验证 | +| 诊断没有可靠标签 | F1 不可信 | 故障注入提供精确标签,真实轨迹人工双标一部分 | +| 采集影响主循环 | 用户体验下降 | 有界队列、批量异步写、失败降级、性能验收 | +| 原始轨迹泄漏代码或密钥 | 安全风险 | 默认元数据模式、脱敏、opt-in 内容采集 | +| 同时改太多 Agent 能力 | 无法归因收益 | 每次实验只改变一个变量,保存完整运行配置 | + +## 12. 最终交付物 + +- 可运行的 AgentLens 代码和自动化测试; +- Trace Schema 与隐私设计文档; +- `kimi lens` CLI; +- 集成进 `kimi vis` 的诊断和对比页面; +- AgentLens-Fault 数据集及生成脚本; +- 至少一个公开 Benchmark 适配器; +- baseline/candidate 的可复现实验配置; +- JSON、HTML 实验报告; +- 2~3 分钟演示视频; +- 一篇技术文章和一张架构图; +- 简历文案中的所有数字对应到公开报告或脚本输出。 + +## 13. 秋招项目描述文案 + +### 13.1 项目名称 + +**AgentLens — 面向 Coding Agent 的本地可观测、故障诊断与评测平台** + +英文可写: + +**AgentLens — A Local-first Observability, Diagnosis and Evaluation Platform for Coding Agents** + +### 13.2 一句话介绍 + +基于开源 Coding Agent Runtime,设计层级 Trace、自动失败归因和可重复评测系统,帮助开发者 +定位模型与工具执行链路中的失败根因,并量化 Agent 优化对成功率、Token 和耗时的影响。 + +### 13.3 开发中可使用的简历版本 + +> 独立设计并开发 AgentLens,在 Kimi Code CLI 的 Agent 主循环、工具执行、上下文压缩、审批 +> 与子 Agent 链路中引入本地层级 Trace;基于 SQLite 构建异步批量存储与查询,并实现重复工具 +> 调用、重试风暴、无进展循环等可解释诊断规则。搭建隔离式 Eval Runner,对接 SWE-bench, +> 支持同任务多版本 Agent 的成功率、Token、耗时和执行路径对比。 + +这个版本不包含未验证的数字,适合项目仍在开发时使用。 + +### 13.4 完成实验后的量化简历模板 + +只能把方括号替换为真实实验结果: + +- 基于 Kimi Code CLI 构建 Coding Agent 可观测平台,统一采集 Turn、LLM、Tool、Approval、 + Compaction 与 Subagent 的父子 Span;采用 `asyncio.Queue + SQLite WAL` 批量落盘,在 + `[N]` 万 Span 压测下实现 P95 查询延迟 `[X] ms`,主任务额外耗时低于 `[Y]%`。 +- 设计 `[N]` 类确定性故障规则和 `[M]` 条可控故障轨迹,在冻结测试集上实现 Macro-F1 + `[X]`、根因 Span Top-3 命中率 `[Y]%`,将用户实验中的中位故障定位时间降低 `[Z]%`。 +- 搭建基于容器隔离的 Agent Eval Runner,对接 SWE-bench `[Lite/Verified]`,支持 baseline 与 + candidate 成对实验及 HTML 报告;根据诊断结果优化 `[重复调用/重试/上下文]` 策略,使无效 + 工具调用降低 `[X]%`、Token 消耗降低 `[Y]%`,任务成功率 `[保持不降/提升 Z 个百分点]`。 +- 实现默认脱敏与可配置数据保留机制,对 Prompt、工具参数、路径和凭证进行字段级保护;通过 + `[N]` 类敏感信息测试,原始敏感内容落盘泄漏率为 `0`。 + +### 13.5 面试时的 60 秒介绍 + +> 我做的项目叫 AgentLens,解决 Coding Agent 失败后难以定位和难以量化优化效果的问题。 +> 原项目已经能展示 Wire 日志,但它是扁平事件,无法准确表达模型请求、并行工具和子 Agent +> 之间的因果关系,也没有自动诊断和同任务对比。我设计了一套本地层级 Trace,把 Turn、Step、 +> LLM、Tool、Approval 和 Compaction 建模成父子 Span,通过异步队列批量写入 SQLite,再用 +> 可解释规则识别重复调用、重试风暴和无进展循环。之后我做了隔离式 Eval Runner,在固定 +> SWE-bench 子集上成对运行改造前后的 Agent,不只比较成功率,还比较 Token、工具调用和关键 +> 路径。项目最重要的点是形成了“发现问题—修改策略—回归验证”的完整工程闭环。 + +### 13.6 STAR 表达 + +**Situation**:Coding Agent 执行真实代码任务时会经历多轮模型请求、工具调用和上下文压缩, +失败轨迹长且具有随机性,仅凭终端日志难以复现和定位。 + +**Task**:建立低侵入、可解释、可量化的观测与评测系统,既不能显著拖慢 Agent,也不能泄漏 +用户代码和凭证。 + +**Action**:设计 ContextVar 传播的层级 Span;使用有界异步队列和 SQLite WAL 批量持久化; +实现规则化失败诊断、Trace Diff 和数据集 Runner;使用故障注入集评价诊断准确率,并在公开 +软件工程任务上进行成对 A/B 实验。 + +**Result**:填写最终测得的性能开销、诊断 F1、定位时间下降、Token/工具调用下降及成功率变化。 + +### 13.7 高频追问准备 + +**为什么不用现有日志?** + +日志面向人阅读,缺少稳定 Schema 和父子关系;并行任务中相邻日志不等于调用关系。Span +模型可以稳定计算耗时、关键路径和上下游影响,并支持机器分析。 + +**为什么不直接接 OpenTelemetry?** + +OpenTelemetry 适合传输和通用 Trace 语义,但 Coding Agent 还需要 Token、Compaction、Tool +Result、任务补丁和评测结果等领域模型。AgentLens 先定义领域 Schema,之后可以导出 OTLP, +而不是让通用标准决定所有内部模型。 + +**为什么选 SQLite?** + +项目是单机 CLI,本地优先且写入主体通常只有一个进程。SQLite 无需部署、支持事务和索引, +WAL 可兼顾写入与查询,复杂度低于引入独立数据库。若未来变成团队服务,再抽象 Store 接口。 + +**如何判断 NoProgress?** + +不只看文件是否变化,而是组合 Git diff 指纹、测试失败集合、新证据、计划状态和验收结果。 +阈值由冻结开发集调整,并在独立测试集报告误报率。 + +**为什么诊断规则不用 LLM?** + +首期目标是可复现和可验证。确定性规则能给出明确证据,也便于计算 Precision/Recall。LLM +可以总结 Finding,但不作为唯一裁判;复杂语义诊断可在后续作为对照实验。 + +**如何证明 AgentLens 真的有用?** + +分三层证明:采集完整性和性能开销;带 ground truth 的诊断准确率;真实用户定位根因的耗时。 +最后再根据诊断结果优化一个 Agent 策略,在相同公开任务上做成对实验验证闭环。 + +**怎样处理 Agent 随机性?** + +固定模型版本、Prompt、预算、工具和环境;任务顺序随机化;每个 Case 重复运行;报告置信区间 +和完整失败分布,不用单次 Demo 代替统计结果。 + +## 14. 推荐实施顺序 + +如果立刻开始,最优先的第一条纵向链路是: + +1. 为 Turn、Step、Model 和 Tool 建立 Span; +2. 写入 SQLite; +3. 用 CLI 展示一棵 Trace Tree; +4. 实现 `RepeatedToolLoop` 一条规则; +5. 注入一个重复调用故障并自动定位; +6. 在现有 Vis 中展示该 Finding。 + +这条链路完成后,项目就已经有一个可演示的最小闭环。后续再横向扩展更多 Span、规则和数据集, +不会出现“做了很多底层模块,但直到最后都无法演示”的问题。 diff --git "a/docs/mk/Kimi Code CLI\346\240\270\345\277\203\347\237\255\346\235\277\344\270\216\347\247\213\346\213\233\351\241\271\347\233\256\350\257\204\344\274\260.md" "b/docs/mk/Kimi Code CLI\346\240\270\345\277\203\347\237\255\346\235\277\344\270\216\347\247\213\346\213\233\351\241\271\347\233\256\350\257\204\344\274\260.md" new file mode 100644 index 0000000000..26ca5199f2 --- /dev/null +++ "b/docs/mk/Kimi Code CLI\346\240\270\345\277\203\347\237\255\346\235\277\344\270\216\347\247\213\346\213\233\351\241\271\347\233\256\350\257\204\344\274\260.md" @@ -0,0 +1,68 @@ +# Kimi Code CLI 核心短板与秋招项目评估 + +本文基于当前仓库源码评估五项能力。结论中的“存在”指生产级能力确实缺失,而不是项目完全没有相关基础设施。 + +## 结论概览 + +| 问题 | 判断 | 当前基础 | +| --- | --- | --- | +| 无持久化跨会话记忆 | 存在,但已有单会话持久化 | 会话和子 Agent 上下文可落盘、恢复 | +| 无 OS 级沙箱 | 明确存在 | 只有审批和提示词约束 | +| 子 Agent 不支持嵌套或递归 | 明确存在 | Root Agent 可创建和恢复子 Agent | +| 无多 Agent 协作编排 | 部分存在 | 支持多个后台 Agent 并发,但缺少编排层 | +| 缺少 repo map 级代码理解 | 明确存在 | 依赖目录、Git 信息、`Glob`、`Grep` 和文件读取 | + +## 1. 持久化跨会话记忆 + +**是否存在及现状:存在,但需要区分“会话恢复”和“跨会话记忆”。** 当前 `Session` 将消息历史、Wire 日志和状态写入特定 `session_id` 的目录,可以恢复原会话;子 Agent 也在当前会话下保存独立的 `context.jsonl`。但是,新会话不会自动检索其他会话中的用户偏好、项目知识、历史决策或失败经验。仓库中也没有长期记忆模型、检索策略、向量或关键词索引以及记忆淘汰机制。 + +**生产影响:** 长期项目中,Agent 会重复探索仓库、重复询问约束,并可能做出与历史决策冲突的修改。直接把全部历史会话塞入上下文又会增加 Token 成本、隐私暴露和错误召回风险,因此生产实现必须包含作用域、来源、过期、删除和可解释引用,而不只是增加一个数据库。 + +**代码证据:** [`Session.dir`](../../src/kimi_cli/session.py#L48) 将数据限定在当前会话目录,[`Session.find`](../../src/kimi_cli/session.py#L183) 也要求使用明确的 `session_id` 恢复;[`SubagentStore.root`](../../src/kimi_cli/subagents/store.py#L68) 位于 `session.dir/subagents`。这些代码证明已有持久化,但其边界仍是单个会话。 + +## 2. OS 级沙箱 + +**是否存在及现状:明确存在。** 默认系统提示词直接声明运行环境不在沙箱中,文件和 Shell 操作会立即影响用户系统。当前安全边界主要由提示词、工作区路径检查和审批流程组成,它们属于应用层策略,不等价于进程、文件系统、网络或系统调用隔离。 + +**生产影响:** Prompt injection、模型误判或工具缺陷都可能造成越权读写、凭据泄漏、依赖投毒和破坏性命令执行。审批能够降低风险,但用户可能误批,高频审批也容易导致“审批疲劳”。面向不可信仓库或自动化执行时,没有 OS 级隔离会显著限制产品可部署范围。 + +**代码证据:** 默认提示词明确写有 `The operating environment is not in a sandbox`,见 [`system.md`](../../src/kimi_cli/agents/default/system.md#L73)。后台 Shell 任务通过本机 `subprocess.Popen` 启动,见 [`BackgroundTaskManager._launch_worker`](../../src/kimi_cli/background/manager.py#L125),未建立容器、namespace、seccomp 或同类隔离边界。 + +## 3. 子 Agent 嵌套与递归 + +**是否存在及现状:明确存在,而且是主动设置的硬限制。** 只有 Root Agent 能调用 `Agent` 工具;子 Agent 调用时会直接返回错误。默认 `coder`、`explore` 等子 Agent 规格也排除了 `Agent` 工具,并将 `subagents` 配置留空。 + +**生产影响:** Root Agent 必须承担所有任务拆分和结果汇总,复杂任务无法形成“负责人 → 专项 Agent → 执行 Agent”的层级,Root 的上下文和调度压力会快速增大。不过,该限制也避免无限递归、资源失控和审批来源混乱;解除限制时必须同时加入最大深度、总并发、Token/时间预算、父子取消传播和权限继承规则。 + +**代码证据:** [`AgentTool.__call__`](../../src/kimi_cli/tools/agent/__init__.py#L119) 检查 `runtime.role != "root"` 后返回 `Subagents cannot launch other subagents.`;[`coder.yaml`](../../src/kimi_cli/agents/default/coder.yaml#L19) 明确排除 `kimi_cli.tools.agent:Agent`。 + +## 4. 多 Agent 协作编排 + +**是否存在及现状:部分存在。** Root Agent 可以创建多个前台或后台子 Agent,系统默认允许最多 4 个后台任务,并支持查询、停止和完成通知。因此“完全没有多 Agent”并不准确。真正缺少的是独立编排运行时:任务模型没有依赖边、优先级、共享工件、Agent 间消息、自动重试、汇总节点或一致的预算调度,协作主要依赖 Root LLM 临时发起任务并人工式消费结果。 + +**生产影响:** 简单并发可以加快搜索,但复杂工作流难以保证执行顺序、故障恢复和结果收敛。多个 Agent 可能重复读取或同时修改同一文件;Root 中断后,未消费结果、资源泄漏和状态不一致也更难处理。缺少结构化编排还会降低可观测性,使成功率和成本难以稳定评测。 + +**代码证据:** [`TaskSpec`](../../src/kimi_cli/background/models.py#L28) 只记录任务类型、状态、所有者和负载,没有依赖或协作关系;[`create_agent_task`](../../src/kimi_cli/background/manager.py#L209) 直接使用 `asyncio.create_task` 启动独立 Agent;[`BackgroundConfig`](../../src/kimi_cli/config.py#L99) 只提供并发数、超时和轮询等运行限制。这是一套后台任务管理能力,而不是 DAG 或工作流编排器。 + +## 5. Repo map 级代码理解 + +**是否存在及现状:明确存在。** 当前 Agent 依靠启动时目录列表、`Glob`、`Grep`、`ReadFile` 和探索型子 Agent 按需理解代码。Explore Agent 额外获得远端、分支、脏文件和最近提交等 Git 上下文,但没有增量 AST 索引、符号表、引用/调用关系图或面向仓库的摘要 map。 + +**生产影响:** 在大型仓库中,Agent 需要反复搜索和读取文件,导致首轮定位变慢、Token 消耗升高,并更容易漏掉动态入口、跨语言引用和间接调用。代码变化后若没有增量更新和失效策略,即使增加索引也可能向模型提供过期事实,因此生产实现还需评测索引延迟、召回率和更新成本。 + +**代码证据:** [`collect_git_context`](../../src/kimi_cli/subagents/git_context.py#L18) 只采集仓库元信息;[`explore.yaml`](../../src/kimi_cli/agents/default/explore.yaml) 提供的是 Shell、文件搜索和读取工具。虽然 `uv.lock` 中存在 `tree-sitter`,但它来自 Python 3.14+ 的 [`batrachian-toad`](../../pyproject.toml#L30) 终端依赖链;在 `src/`、`packages/` 和 `tests/` 中没有 `tree_sitter` 导入或 repo map 实现。 + +## 秋招项目质量判断 + +**选题合格,而且上限很高;但“同时补上五个功能”本身不能证明项目质量。** 五项能力横跨安全、分布式调度、知识检索和程序分析,单人短周期全部实现容易形成五个浅 Demo,反而暴露范围控制和工程完成度不足。 + +更适合作为秋招项目的方式,是选择一条完整主线:例如实现“安全多 Agent 运行时”,以受控递归、任务 DAG 和预算/取消传播为核心,再增加一种本地沙箱后端;或者实现“仓库智能层”,以 Tree-sitter 增量 repo map 为核心,并用跨会话记忆保存经过验证的项目决策。其余能力只做接口预留。 + +达到“质量合格”至少应满足: + +- 接入现有 `Runtime`、`Session`、审批、Wire 和子 Agent 生命周期,而不是旁路脚本; +- 有单元测试、端到端测试以及崩溃恢复、并发冲突、越权或索引失效测试; +- 有量化对比,例如任务成功率、Token、定位耗时、沙箱逃逸面或调度吞吐; +- 有架构说明、威胁模型、数据迁移和兼容策略,并能演示真实失败场景。 + +如果只添加 API、数据表和演示页面,质量不合格;如果完整解决其中一个困难问题并给出可靠评测,已经是合格且有辨识度的秋招项目;若能完成同一主线下两个相互支撑的能力并形成可合并的上游改动,则可以达到优秀水平。 diff --git "a/docs/mk/Kimi Code CLI\347\247\213\346\213\233\347\256\200\345\216\206\351\241\271\347\233\256\350\257\264\346\230\216.md" "b/docs/mk/Kimi Code CLI\347\247\213\346\213\233\347\256\200\345\216\206\351\241\271\347\233\256\350\257\264\346\230\216.md" new file mode 100644 index 0000000000..6af5d7d73a --- /dev/null +++ "b/docs/mk/Kimi Code CLI\347\247\213\346\213\233\347\256\200\345\216\206\351\241\271\347\233\256\350\257\264\346\230\216.md" @@ -0,0 +1,189 @@ +# Kimi Code CLI 秋招简历项目说明 + +> 使用说明:本文给出可直接用于秋招简历、项目介绍和面试陈述的文本。当前仓库已经具备 CLI Agent 主循环、会话持久化、子 Agent、后台任务、审批和 Wire 事件系统;“项目知识层”和“Agent Teams”属于本次拟开发能力。未完成开发和评测前,不要把规划写成已上线成果。 + +## 1. 项目定位 + +### 推荐项目名 + +**Kimi Code CLI:具备持久化项目知识与多 Agent Teams 编排能力的开源 Coding Agent** + +英文可写: + +**Kimi Code CLI — Coding Agent with Persistent Project Knowledge and Multi-Agent Team Orchestration** + +### 一句话介绍 + +基于 Python、asyncio 和事件驱动架构,为开源 Coding Agent 构建项目级知识层与多 Agent Teams 编排运行时,使 Agent 能够复用跨会话工程知识,并以可恢复、可观测、受预算约束的方式协同完成复杂仓库任务。 + +### 项目价值 + +这个项目解决两类互相放大的问题: + +- Coding Agent 每次进入仓库都要重复搜索代码,且难以复用历史架构决策; +- 多个 Agent 虽然可以并发执行,但缺少依赖、消息、共享工件、冲突控制和失败恢复。 + +项目将二者组合为一条完整主线:知识层负责提供“代码现在是什么样、过去为什么这样设计”,Agent Teams 负责把复杂目标拆成有依赖的任务并组织多个 Agent 消费同一份可信知识。 + +```text +项目知识层:Repo Intelligence + Persistent Memory + ↓ Context Pack +协作执行层:Team Lead + Teammates + Task DAG + Mailbox + ↓ +运行时基础:Session + Subagent + Background + Approval + Wire +``` + +## 2. 简历可直接使用版本 + +### 2.1 开发完成后的推荐版本 + +**Kimi Code CLI|开源 Coding Agent 项目|核心开发者** +`Python` `asyncio` `Pydantic` `SQLite/FTS5` `Tree-sitter` `Typer` `Git` `pytest` + +- 面向大型代码仓库设计并实现统一项目知识层,使用 Tree-sitter 构建 Python 符号、引用与模块依赖的增量索引,以 SQLite/FTS5 持久化架构决策、工程约束和验证经验,并通过 Git commit、blob hash 与符号存在性完成记忆失效检测。 +- 在 Agent 推理前实现知识查询编排与临时 Context Pack 注入,支持代码事实和历史记忆的并行召回、去重、冲突标记、来源追踪及 Token 预算控制,避免将易过期 Repo 快照永久写入会话历史。 +- 基于现有 Subagent、BackgroundTaskManager、ApprovalRuntime 和 Wire 事件协议实现 Agent Teams,建立 Team/Member/Task DAG、Mailbox、Artifact、预算与取消传播模型,支持并行只读探索、依赖就绪调度、成员间消息和会话级崩溃恢复。 +- 建立可重复的 baseline/candidate 评测集,围绕首次代码定位耗时、探索工具调用次数、输入 Token、任务成功率、记忆误用率和并发冲突率进行对比;将实测结果填写为“定位耗时下降 **[X%]**、工具调用下降 **[Y%]**、Token 下降 **[Z%]**、成功率提升 **[N] 个百分点**”。 + +> 方括号中的指标必须来自固定任务集的实际实验,不可用预期值代替。 + +### 2.2 MVP 完成、尚未完成完整评测时 + +**Kimi Code CLI|开源 Coding Agent 项目|核心开发者** +`Python` `asyncio` `SQLite/FTS5` `Tree-sitter` `Multi-Agent` `pytest` + +- 基于 Kimi Code CLI 的 Runtime、Session 和 KimiSoul 主循环,落地项目级 Repo Intelligence 与 Persistent Memory 原型,实现 Python 符号增量索引、四类工程记忆及带来源的上下文召回。 +- 设计 Agent Teams 领域模型和持久化状态机,复用现有子 Agent、后台任务、审批与 Wire 基础设施,实现任务依赖调度、成员消息、共享工件索引、预算限制和取消传播。 +- 针对分支切换、代码删除、过期记忆、Agent 异常退出、依赖失败和并发写冲突补充单元与端到端测试,并建设可复现的效果评测脚本。 + +### 2.3 目前仅完成方案设计时 + +如果代码尚未实现,只能使用以下表述: + +**Kimi Code CLI|开源 Coding Agent 架构研究与功能设计** + +- 阅读并梳理约 6.6 万行 Python 运行时代码与约 6.4 万行测试代码,完成 CLI 入口、Agent 主循环、上下文持久化、子 Agent、后台任务、审批和 Wire 事件链路的代码级调研。 +- 针对跨会话知识缺失和多 Agent 协作编排不足,设计“Repo Intelligence + Persistent Memory + Agent Teams”方案,给出数据模型、接入点、状态机、测试矩阵、评测指标和分阶段开发计划。 + +这一版本可以说明分析与设计能力,但含金量明显低于可运行实现。秋招前应至少完成知识层闭环或 Agent Teams 闭环中的一个,并给出真实评测。 + +## 3. 个人贡献应该怎样讲 + +推荐把个人贡献归纳为三个层次,而不是罗列功能名。 + +### 3.1 架构层:建立“知识平面 + 协作平面” + +- 知识平面把当前代码事实与历史工程决策分开存储,通过统一编排器合并; +- 协作平面把多个独立子 Agent 升级为有任务依赖、消息和共享工件的团队; +- 两个平面共同接入既有 Runtime、Session、Approval 和 Wire,不另建旁路 Agent 系统。 + +可用于面试的核心表达: + +> 我没有简单加入向量数据库或批量启动多个 Agent,而是先划清事实、记忆、编排和执行的边界。代码索引只保存可验证的当前事实,长期记忆保存决策和经验,Team Scheduler 只处理任务状态与资源约束,LLM 负责语义判断但不充当唯一状态机。 + +### 3.2 工程层:解决可恢复性和一致性 + +- 通过内容 hash、Git blob 和符号引用判断索引与记忆是否过期; +- 通过 SQLite 事务或原子写避免并发状态损坏; +- 通过显式任务状态机、租约和取消传播处理成员异常; +- 通过单写者策略或隔离 worktree 避免多个写 Agent 直接竞争同一文件; +- 通过审批来源绑定将高风险操作追溯到具体 team、task 和 agent。 + +### 3.3 效果层:用实验回答“是否真的更好” + +不要只展示功能可以运行。应在固定仓库、固定任务、固定模型和固定预算下,对比: + +| 指标 | Baseline | Candidate | 目标解释 | +| --- | ---: | ---: | --- | +| 首次定位正确文件耗时 | `[ ]` | `[ ]` | 知识层是否减少盲目探索 | +| Grep/ReadFile 调用数 | `[ ]` | `[ ]` | Repo map 是否减少重复读取 | +| 输入 Token | `[ ]` | `[ ]` | Context Pack 是否节省上下文 | +| 最终任务成功率 | `[ ]` | `[ ]` | 优化不能只降低成本 | +| 过期记忆误用率 | `[ ]` | `[ ]` | 失效机制是否可靠 | +| Team 关键路径耗时 | `[ ]` | `[ ]` | 并行是否真正缩短交付时间 | +| 重复工作率 | `[ ]` | `[ ]` | 消息和任务分配是否有效 | +| 并发写冲突率 | `[ ]` | `[ ]` | 协作安全性是否达标 | + +## 4. 技术亮点与证据 + +| 简历亮点 | 当前可复用基础 | 拟开发成果的证据位置 | +| --- | --- | --- | +| 异步 Agent 运行时 | `src/kimi_cli/soul/kimisoul.py` | 新增知识召回和 Team Scheduler 的集成测试 | +| 会话与子 Agent 持久化 | `src/kimi_cli/session.py`、`src/kimi_cli/subagents/store.py` | 项目知识数据库、team 状态与恢复测试 | +| 多 Agent 执行 | `src/kimi_cli/tools/agent/`、`src/kimi_cli/background/` | DAG 调度、消息、预算和取消传播测试 | +| 安全审批 | `src/kimi_cli/approval_runtime/` | team/task/agent 来源追踪和权限收敛测试 | +| 事件驱动 UI | `src/kimi_cli/wire/`、`src/kimi_cli/ui/` | Team/Knowledge Wire 事件与回放兼容测试 | +| Repo Intelligence | 当前仅有 `src/kimi_cli/subagents/git_context.py` | Tree-sitter 索引、增量更新、查询正确性测试 | +| 动态上下文 | `src/kimi_cli/soul/dynamic_injection.py` | 临时注入语义、Token 预算和恢复不重复测试 | + +仓库规模仅可作为复杂度背景,不应作为个人成果。截至本文调研快照,`src/` 与 `packages/` 中 Python 代码约 6.6 万行,`tests/` 与 `tests_ai/` 中 Python 测试约 6.4 万行;简历中应明确自己修改的模块、提交或 PR,而不是暗示整个仓库由个人完成。 + +## 5. 30 秒与 2 分钟面试介绍 + +### 30 秒版本 + +> 我在 Kimi Code CLI 上做的是面向长期仓库任务的知识与协作增强。现有系统能恢复单次会话,也能并发启动子 Agent,但新会话不会复用项目决策,多个 Agent 之间也没有结构化协作。我设计并实现项目知识层,用 Tree-sitter、SQLite FTS5 和 Git 版本信息提供可追溯、可失效的 Context Pack;同时在现有子 Agent、后台任务、审批和 Wire 之上增加 Agent Teams 的任务 DAG、消息、工件和预算调度。最后用固定任务集比较成功率、Token、定位耗时和冲突率,而不是只做功能 Demo。 + +### 2 分钟版本 + +> 我先从源码确认了两个边界。第一,Session 和 SubagentStore 虽然会保存 context.jsonl,但存储范围仍是单个会话,新会话无法检索历史决策。第二,Root Agent 可以启动多个前台或后台子 Agent,但 BackgroundTaskManager 管理的是独立任务,没有依赖图、成员消息、共享工件和一致的预算调度。 +> +> 因此我把改造分成两个平面。知识平面中,Repo Intelligence 保存由 Tree-sitter 提取的符号和模块关系,Persistent Memory 保存决策、约束、偏好和验证经验,Knowledge Orchestrator 根据任务并行召回并检查 Git blob 和符号是否仍有效。协作平面中,Team Scheduler 管理 Team、Member 和 Task DAG,Mailbox 负责 Agent 间的结构化消息,Artifact Index 只保存工件元数据和引用,审批及取消继续复用原运行时。 +> +> 最关键的工程取舍是没有把动态 Repo 信息永久写进会话,也没有一开始开放无限递归和多写者。首期采用临时 Context Pack、固定最大团队规模和单写者策略,先确保可恢复、可解释和可评测,再逐步增加向量召回或 worktree 隔离。 + +## 6. 高频追问与回答要点 + +### 为什么不用向量数据库? + +MVP 的决策、约束和符号名称对关键词检索较友好,SQLite/FTS5 部署成本低、可离线、易迁移和调试。先建立召回基线,只有当真实任务证明语义召回不足时再引入 embedding,避免为了技术栈而增加复杂度。 + +### 为什么知识注入不能直接追加到 Context? + +Repo 快照随 HEAD 和工作区变化。若永久追加到 `context.jsonl`,恢复会话时会重复加载旧快照,压缩后也难以判断哪些内容已失效。正确做法是在请求模型时临时合成 effective history,原始会话只保存用户、模型和工具的真实交互,另以 Wire 事件记录召回来源和成本。 + +### Agent Teams 与“并发启动多个子 Agent”有什么区别? + +并发只解决同时运行;Teams 还要解决谁做什么、任务何时就绪、结果交给谁、失败如何传播、共享什么工件、谁可以写文件、预算是否超限以及重启后如何恢复。它本质上是一个受 LLM 驱动但由确定性状态机约束的协作运行时。 + +### 为什么首期不开放子 Agent 递归创建? + +当前代码明确限制只有 Root Agent 能调用 `Agent`。直接取消限制会引入无限递归、并发爆炸、权限来源混乱和取消泄漏。首期由 Team Scheduler 统一创建成员,成员只能提交状态、消息和工件;等深度、配额和权限继承规则稳定后,再评估受控嵌套。 + +### 多个 Agent 同时修改文件怎么办? + +MVP 使用单写者策略:探索和评审成员可并行,只有一个 coder 持有写租约。后续若需要多写者,使用独立 git worktree 执行,再由集成任务合并补丁;仅依靠提示词约定不能保证没有冲突。 + +### 记忆错误或过期怎么办? + +每条代码相关记忆必须保存来源、commit、blob、符号和验证方式。召回时根据当前代码将其标为 valid、possibly_stale、stale 或 conflicted;低置信度和冲突记忆只能作为候选背景,不能成为强约束。 + +## 7. 项目展示建议 + +演示应选择一个真实仓库任务,展示同一任务的 baseline 与 candidate: + +1. 第一次运行定位目标模块并完成修改,测试通过后沉淀一条架构决策; +2. 新会话再次提出相关任务,展示知识来源、代码引用和更少的探索调用; +3. 修改或删除被引用符号,展示旧记忆自动降级而不是继续误导 Agent; +4. 创建一个由 explore、coder、reviewer 组成的 team,展示任务依赖、消息、审批和工件; +5. 人为终止一个成员或制造写冲突,展示恢复、失败传播和诊断信息; +6. 输出前后指标报告和可复现命令。 + +最有说服力的材料包括:架构图、一次完整 Trace、SQLite schema、失败恢复测试、评测报告、提交记录和 PR 链接。 + +## 8. 简历真实性检查清单 + +提交简历前逐项确认: + +- [ ] “实现”“优化”“提升”等动词均有代码、测试或实验结果支撑; +- [ ] 规划中的模块没有写成已经上线; +- [ ] 百分比使用固定任务集重复实验得到,并保留原始结果; +- [ ] 能指出每条简历 bullet 对应的源码路径、测试和提交; +- [ ] 能解释至少一个失败案例和一次设计取舍; +- [ ] 没有把整个开源仓库的代码量或 star 数当作个人贡献; +- [ ] 没有把应用层审批描述为 OS 级沙箱; +- [ ] 没有把现有独立后台 Agent 描述成已经具备完整 Teams 编排。 + +## 9. 推荐最终关键词 + +`Coding Agent`、`Multi-Agent System`、`Agent Orchestration`、`asyncio`、`Event-Driven Architecture`、`Tree-sitter`、`SQLite FTS5`、`Incremental Indexing`、`Persistent Memory`、`DAG Scheduling`、`Failure Recovery`、`Token Budget`、`Git-aware Invalidation`、`pytest` diff --git a/docs/mk/resume-kimi-code-cli.html b/docs/mk/resume-kimi-code-cli.html new file mode 100644 index 0000000000..0d2ce4e964 --- /dev/null +++ b/docs/mk/resume-kimi-code-cli.html @@ -0,0 +1,649 @@ + +Kimi Code CLI — 秋招项目简历 + +
+ +
+
秋招项目经历
+

Kimi Code CLI

+

为开源 Coding Agent 构建项目知识层与多 Agent 协作运行时

+
+ 核心开发者 + · + 2025.06 — 至今 +
+
+ + +
+

+ S + 背景 Situation +

+
+

Kimi Code CLI 是一个终端里的 AI Coding Agent,具备异步主循环、会话持久化、子 Agent、后台任务、审批和事件驱动 UI 等能力。但在大型仓库长期开发场景中暴露出两个瓶颈:

+
    +
  • 知识断层:每个新会话都要重新搜索代码,历史架构决策、工程约束和踩坑经验无法跨会话复用。
  • +
  • 协作缺失:虽能并发启动子 Agent,但缺乏任务依赖、消息通信、共享工件和失败恢复机制,多 Agent 各自为战。
  • +
+
+ 📐 + 代码规模背景:运行时 Python ~6.6 万行,测试 ~6.4 万行(来自仓库调研快照,非个人产出) +
+
+
+ +
+

+ T + 任务 Task +

+
+

在不另建旁路 Agent 系统的前提下,基于现有 Runtime 和 Session 基础设施,完成两项核心改造:

+
+
+

知识平面

+

设计并实现统一项目知识层,让 Agent 能够增量索引代码结构、持久化工程记忆,并在推理前注入经过时效性校验的 Context Pack。

+
+
+

协作平面

+

在现有子 Agent 和后台任务之上构建 Agent Teams 协作运行时,实现任务 DAG 编排、成员消息、共享工件、预算调度和崩溃恢复。

+
+
+
+
+ +
+

+ A + 行动 Action +

+ +
+
Phase 1
+

代码级调研与架构设计

+
    +
  • 阅读并梳理全部运行时代码(CLI 入口 → Agent 主循环 → Session 持久化 → 子 Agent → 后台任务 → 审批 → Wire 事件链路),建立完整调用链认知。
  • +
  • 识别现有系统的边界与复用点:KimiSoul 主循环、SubagentStore 持久化、BackgroundTaskManager 调度、ApprovalRuntime 审批、Wire 事件流。
  • +
  • 输出"Repo Intelligence + Persistent Memory + Agent Teams"方案设计,包含数据模型、接入点、状态机、测试矩阵、评测指标和分阶段开发计划。
  • +
+
+ +
+
Phase 2
+

知识层原型实现

+
    +
  • 使用 Tree-sitter 构建 Python 符号、引用与模块依赖的增量索引,支持按 commit 跟踪代码变更。
  • +
  • SQLite + FTS5 持久化四类工程记忆:架构决策、工程约束、编码偏好、验证经验,每条记忆绑定来源 commit、blob hash 和符号引用。
  • +
  • 实现 Knowledge Orchestrator:在 Agent 推理前并行召回代码事实和历史记忆,去重、冲突标记、来源追踪,按 Token 预算合成临时 Context Pack(不永久写入会话历史)。
  • +
  • 基于 Git blob 和符号存在性实现记忆失效检测:召回时自动标记 valid / possibly_stale / stale / conflicted。
  • +
+
+ +
+
Phase 3
+

Agent Teams 协作运行时

+
    +
  • 建立 Team / Member / Task DAG 领域模型与持久化状态机,复用现有 Subagent、BackgroundTaskManager 和 ApprovalRuntime 基础设施。
  • +
  • 实现 Mailbox 消息系统,支持成员间结构化消息传递与事件驱动通知。
  • +
  • 设计 Artifact Index,仅保存工件元数据与引用,避免冗余存储。
  • +
  • 实现 预算传播与取消级联:超限或异常时沿任务依赖图向上传播取消信号。
  • +
  • 采用单写者策略避免多 Agent 文件写冲突,后续预留 worktree 隔离扩展点。
  • +
+
+ +
+
Phase 4
+

测试与质量保障

+
    +
  • 覆盖分支切换、代码删除、过期记忆、Agent 异常退出、依赖失败、并发写冲突等场景的单元测试和端到端测试。
  • +
  • 建设可复现的效果评测脚本,固定仓库、固定任务、固定模型和固定预算下对比 baseline 和 candidate 的表现。
  • +
+
+
+ +
+

+ R + 成果 Result +

+
+ + + + + + + + + + + + +
指标BaselineCandidate解释
首次定位正确文件耗时知识层是否减少盲目探索
Grep / ReadFile 调用数Repo map 是否减少重复读取
输入 Token 消耗Context Pack 是否节省上下文
最终任务成功率优化不能只降低成本
过期记忆误用率失效机制是否可靠
Team 关键路径耗时并行是否真正缩短交付时间
+
+ ⚠️ 评测指标需来自固定任务集的实际实验,不可用预期值代替。秋招前应至少完成知识层或 Agent Teams 闭环之一。 +
+
+
+ + +
+

技术栈

+
+ Python + asyncio + SQLite / FTS5 + Tree-sitter + Pydantic + Typer + Git + pytest + Event-Driven + Multi-Agent + DAG Scheduling + Token Budget +
+
+ + +
+

关键代码位置

+ +
+ + +
+

面试核心表达

+
+

我没有简单加入向量数据库或批量启动多个 Agent,而是先划清事实、记忆、编排和执行的边界。代码索引只保存可验证的当前事实,长期记忆保存决策和经验,Team Scheduler 只处理任务状态与资源约束,LLM 负责语义判断但不充当唯一状态机。

+
+
+ +
+

高频追问速答

+
+
为什么不用向量数据库?
+
MVP 的决策和符号名称对关键词检索较友好,SQLite/FTS5 部署零成本、可离线、易调试。先建立召回基线,语义召回不足时再引入 embedding。
+
知识为什么不直接写进 Context?
+
Repo 快照随 HEAD 变化,永久追加会导致旧快照重复加载且难以判废。正确做法是临时合成 effective history,原始会话只存真实交互。
+
Agent Teams 和并发子 Agent 区别?
+
并发只解决同时运行;Teams 还要解决任务分配、依赖就绪、结果路由、失败传播、工件共享、写权限、预算和恢复。本质上是 LLM 驱动 + 确定性状态机约束的协作运行时。
+
多 Agent 写冲突怎么办?
+
MVP 单写者策略:探索/评审可并行,仅一个 coder 持有写租约。后续引入 worktree 隔离 + 集成合并。
+
+
+ +
+

基于 Kimi Code CLI 开源项目 · 简历内容需与实际代码和测试结果一致

+
+
+ + diff --git "a/docs/mk/\346\214\201\344\271\205\345\214\226\351\241\271\347\233\256\350\256\260\345\277\206\344\270\216Repo\344\273\243\347\240\201\346\231\272\350\203\275\347\274\226\346\216\222\346\226\271\346\241\210.md" "b/docs/mk/\346\214\201\344\271\205\345\214\226\351\241\271\347\233\256\350\256\260\345\277\206\344\270\216Repo\344\273\243\347\240\201\346\231\272\350\203\275\347\274\226\346\216\222\346\226\271\346\241\210.md" new file mode 100644 index 0000000000..faa5ea6756 --- /dev/null +++ "b/docs/mk/\346\214\201\344\271\205\345\214\226\351\241\271\347\233\256\350\256\260\345\277\206\344\270\216Repo\344\273\243\347\240\201\346\231\272\350\203\275\347\274\226\346\216\222\346\226\271\346\241\210.md" @@ -0,0 +1,205 @@ +# 持久化项目记忆与 Repo 代码智能编排方案 + +> 项目定位:为 Kimi Code CLI 构建统一的项目知识层,让 Agent 同时理解「仓库现在是什么样」以及「过去为什么这样设计」。建议以持久化对话记忆和增量 Repo Intelligence 为两条能力线,由统一编排器完成检索、校验和上下文注入。 + +## 1. 项目目标与可行性 + +这两个方向适合形成同一条项目主线,而不是两个互不相关的功能:Repo Intelligence 提供当前代码事实,持久化记忆保存历史决策、用户偏好和经过验证的工程经验,编排器负责将二者合成为有来源、可失效且受 Token 预算约束的上下文。 + +```text +理解当前仓库 + 检索历史决策 + ↓ +生成任务上下文 + ↓ +Agent 执行并验证 + ↓ +沉淀新的项目记忆 +``` + +当前仓库已经具备 `Session` 持久化、Git 上下文、Hook、Wire、子 Agent 生命周期和文件搜索工具,可以作为接入基础。项目不需要重写 Agent 主循环,主要工作集中在知识存储、增量索引、召回编排和有效性校验,因此整体具有较高可行性。 + +## 2. 总体架构 + +```mermaid +flowchart TD + A["用户请求"] --> Q["Knowledge Orchestrator"] + Q --> R["Repo Intelligence"] + Q --> M["Persistent Memory"] + + R --> R1["Tree-sitter AST"] + R --> R2["符号、引用与模块关系"] + R --> R3["Git commit / blob 版本"] + + M --> M1["用户与项目偏好"] + M --> M2["架构决策与约束"] + M --> M3["历史任务、失败与验证结果"] + + R1 --> C["Context Pack"] + R2 --> C + R3 --> C + M1 --> C + M2 --> C + M3 --> C + + C --> AG["Root / Subagent"] + AG --> V["测试、Git diff、用户确认"] + V --> W["Memory Writer"] + W --> M +``` + +三个核心模块的职责应保持清晰: + +| 模块 | 负责内容 | 不应负责 | +| --- | --- | --- | +| Repo Intelligence | 文件、符号、定义、引用、模块依赖和 Git 版本 | 用户偏好、历史决策和失败经验 | +| Persistent Memory | 决策、约束、偏好、任务结论和验证结果 | 保存整份源码或替代 AST 索引 | +| Knowledge Orchestrator | 查询规划、并行召回、排序、冲突检测和 Token 预算 | 自行生成未经验证的代码事实 | + +例如,`KimiSoul.run()` 的定义位置属于 Repo Intelligence;「团队决定不在主循环中直接写数据库」属于 Persistent Memory;当前代码是否违反该历史决策,则由编排器结合双方证据判断。 + +## 3. Repo Intelligence 设计 + +Repo Intelligence 负责构建可增量更新的仓库事实层。首期建议仅支持 Python,通过 Tree-sitter 提取文件、类、函数、方法和 import,并保存符号之间的定义、引用和模块依赖关系。 + +索引至少包含: + +- 文件路径、语言、内容 hash 和最后索引时间; +- 符号名称、限定名、类型、源码范围和签名; +- import、定义、引用和可静态确定的调用关系; +- Git commit、blob hash 和工作区脏状态; +- 按文件或模块生成的短摘要。 + +文件变化时根据内容 hash 重新解析,删除文件时清理关联符号和引用。查询层首期提供 `search_symbol`、`find_references`、`module_summary` 和 `related_files` 即可,不必一开始覆盖多语言或完整静态分析。 + +现有 [`collect_git_context`](../../src/kimi_cli/subagents/git_context.py#L18) 只能提供远端、分支、脏文件和最近提交等元信息;新模块应在此基础上补充结构化代码事实,而不是替换 `Glob`、`Grep` 和 `ReadFile`。 + +## 4. Persistent Memory 设计 + +现有会话能够恢复原始 `context.jsonl`,但新会话不会自动检索历史知识。Persistent Memory 应作为项目级知识库,与原始会话历史分离。 + +建议支持以下记忆类型: + +- `preference`:用户或团队的稳定偏好; +- `decision`:架构决策、采用原因和替代方案; +- `constraint`:仓库约束、兼容要求和安全边界; +- `lesson`:已验证的成功方案或失败经验。 + +每条记忆需要保存作用域、来源会话、创建时间、置信度、验证方式和关联代码引用。源码本身不应复制进长期记忆,记忆只保存结论、理由以及可回溯的文件和符号位置。 + +```python +class MemoryRecord(BaseModel): + id: str + project_id: str + kind: Literal["preference", "decision", "constraint", "lesson"] + content: str + source_session_id: str + confidence: float + validation: str | None + code_references: list[CodeReference] + created_at: float + status: Literal["valid", "possibly_stale", "stale", "conflicted"] +``` + +首期可以使用 SQLite 和 FTS5 完成持久化与关键词召回,不必立即引入向量数据库。向量检索应在基线评测证明关键词召回不足后再增加。 + +## 5. 两个模块的编排流程 + +### 5.1 查询规划与并行召回 + +编排器根据请求选择知识来源:代码定位任务以 Repo Intelligence 为主;历史决策和用户偏好以 Persistent Memory 为主;功能设计和代码修改同时检索两者。MVP 可以先用规则分类,避免额外的 LLM 调用。 + +```python +repo_results, memory_results = await asyncio.gather( + repo_index.search(query, git_revision=current_revision), + memory_store.search(query, project_id=project_id), +) +``` + +### 5.2 合并、去重与冲突检测 + +检索结果可能存在三种关系:互补、重复和冲突。编排器应合并重复约束,并使用 Repo Intelligence 检查记忆引用的文件和符号是否仍然存在。发现冲突时,不应静默选择一方,而应向 Agent 标记当前事实、历史结论及其可信状态。 + +```text +当前代码事实: +- BackgroundTaskManager 使用 asyncio.create_task 启动后台 Agent。 + +历史项目决策: +- 某次会话提出使用持久化 worker 队列。 +- 该决策早于当前 HEAD,尚未确认仍然有效。 + +处理建议: +- 将历史决策作为候选背景,不作为强约束。 +``` + +### 5.3 Context Pack 与 Token 预算 + +编排结果应作为当前轮次的动态 `Context Pack` 注入,不应永久追加到原始 `context.jsonl`,否则恢复会话时会重复加载已经过期的 Repo 信息。 + +可采用固定预算比例:代码事实占 50%,历史决策和约束占 30%,任务经验占 20%。每条内容都必须附带来源,使 Agent 能继续读取原始文件或会话,而不是一次性注入完整内容。 + +## 6. 记忆失效与写入控制 + +记忆过期是该项目最关键的生产问题。每条与代码有关的记忆应关联文件、符号、Git commit 和 blob hash: + +```python +class CodeReference(BaseModel): + path: str + symbol: str | None + git_commit: str + blob_hash: str | None +``` + +检索记忆时检查文件和符号是否仍存在、blob 是否变化、相关测试是否仍通过,并将记忆标记为: + +- `valid`:代码证据仍然成立; +- `possibly_stale`:文件发生变化,但符号仍然存在; +- `stale`:文件或符号已删除; +- `conflicted`:当前代码与历史结论相反。 + +记忆写入同样需要门槛。原始对话继续由现有 `Context` 保存;回合结束后只生成候选记忆,经过测试、Git diff 或用户确认后再升级为长期记忆。临时猜测、未验证结论、大段 Shell 输出、可从 AST 恢复的源码以及敏感信息均不应进入长期存储。 + +## 7. 在 Kimi Code CLI 中的接入点 + +建议沿用当前运行时边界: + +- 在 [`Runtime.create`](../../src/kimi_cli/soul/agent.py#L212) 初始化项目级 `MemoryStore`、`RepoIndex` 和 `KnowledgeOrchestrator`; +- 在 `KimiSoul.run()` 或 `_turn()` 中,于 User 消息进入模型前构建动态 `Context Pack`; +- 保留 [`Context`](../../src/kimi_cli/soul/context.py#L20) 的原始会话持久化职责; +- 让 Root Agent 和子 Agent 共享项目知识层,但继续保持各自对话上下文; +- 使用 `TurnEnd`、`SubagentStop` 等 Hook 生成候选记忆; +- 通过 Wire 记录召回来源、失效状态、注入 Token 和检索延迟。 + +这样可以最大限度复用现有 `Session`、审批、Wire 和子 Agent 生命周期,避免建立旁路 Agent 或重复会话系统。 + +## 8. MVP 范围与实施顺序 + +建议按以下顺序推进: + +1. 建立 SQLite Schema、项目标识和统一检索结果模型。 +2. 实现 Python Tree-sitter 增量索引和符号查询。 +3. 实现四类记忆、FTS5 召回及来源追踪。 +4. 实现并行召回、去重、代码引用校验和 Token 预算。 +5. 接入 Root Agent 与子 Agent 的动态上下文。 +6. 接入候选记忆提取、验证和失效更新。 +7. 增加 Wire 可观测信息及离线评测工具。 + +首期明确不做多语言全覆盖、复杂向量数据库、自动保存所有对话、云端知识服务或完整编译器级调用图。范围控制比功能数量更重要。 + +## 9. 评测与验收 + +项目应使用同一批真实仓库任务对比 baseline 和 candidate,至少衡量: + +- 首次定位正确文件和符号的耗时; +- `Grep`、`ReadFile` 等探索工具调用次数; +- 输入 Token 和总任务成本; +- 历史约束召回准确率以及过期记忆误用率; +- 文件变化后的增量索引延迟; +- 最终任务成功率和测试通过率。 + +还应覆盖会话重启、Git 分支切换、文件重命名、符号删除、数据库损坏、并发索引和敏感信息过滤等测试。没有这些失败场景,功能只能证明 Demo 可运行,不能证明具备生产质量。 + +## 10. 项目质量结论 + +「持久化项目记忆 + 增量 Repo Intelligence」是一条完整且有辨识度的秋招项目主线。Repo Intelligence 为记忆提供版本校验,解决历史知识过期问题;持久化记忆为代码索引补充设计原因和工程经验,解决结构事实缺少语义的问题;编排器则把两者变成 Agent 可以安全消费的上下文。 + +如果项目只能演示保存聊天摘要和列出函数名称,质量仍属于普通 Demo。若能展示代码修改后记忆自动降级或失效、Agent 重复探索明显减少、Token 和定位耗时下降,并提供完整测试和可复现实验,则可以达到优秀秋招项目水平。 diff --git "a/docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" "b/docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" new file mode 100644 index 0000000000..a40d1c0ed3 --- /dev/null +++ "b/docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" @@ -0,0 +1,487 @@ +# Kimi Code CLI 效果评测方案 + +> 用于简历 **R — 成果 Result** 章节的实验支撑。所有指标需来自固定任务集的实际实验,不可用预期值代替。 + +--- + +## 目录 + +- [1. 评测目标与指标总览](#1-评测目标与指标总览) +- [2. 数据集选型](#2-数据集选型) +- [3. 逐指标评测方案](#3-逐指标评测方案) +- [4. 自建测试集:记忆失效检测](#4-自建测试集记忆失效检测) +- [5. Agent Teams 专项评测](#5-agent-teams-专项评测) +- [6. 实验执行流程](#6-实验执行流程) +- [7. 预期结果与填表示例](#7-预期结果与填表示例) +- [8. 风险与应对](#8-风险与应对) + +--- + +## 1. 评测目标与指标总览 + +| 编号 | 指标 | 定义 | 衡量什么 | +|:----:|------|------|----------| +| M1 | **首次定位正确文件耗时** | 从任务开始到 Agent 首次打开目标文件的时间 | 知识层是否缩短探索路径 | +| M2 | **探索工具调用数** | 完成任务产生的 `grep`/`glob`/`read_file` 调用次数 | Repo map 是否减少盲目搜索 | +| M3 | **输入 Token 消耗** | 任务全生命周期的模型输入 token 总量 | Context Pack 是否节省上下文 | +| M4 | **最终任务成功率** | 任务(如 Issue→Patch)被验证通过的比例 | 优化不能以降级为代价 | +| M5 | **过期记忆误用率** | Agent 基于已失效记忆做决策的次数 / 总召回次数 | 记忆失效与降级机制是否可靠 | +| M6 | **Team 关键路径耗时** | 从 Team 创建到最终交付的端到端时间 | Agent Teams 并行编排是否缩短交付 | + +--- + +## 2. 数据集选型 + +### 2.1 数据集全景 + +| 数据集 | 规模 | 任务粒度 | 编程语言 | 公开性 | 推荐用途 | +|--------|:----:|:--------:|:--------:|:------:|----------| +| **SWE-bench Lite** | 300 实例 | 单 Issue → 单 Patch | Python | ✅ 完全公开 | M1–M4 主评测 | +| **SWE-bench Verified** | 500 实例 | 同上(经人工验证) | Python | ✅ 完全公开 | M4 最终验证 | +| **SWE-bench Full** | 2,294 实例 | 同上 | Python | ✅ 完全公开 | M1–M4 扩展验证 | +| **DevBench** | 4 仓库 × 4 任务 | 多步骤开发 | Python | ✅ 完全公开 | M6 Agent Teams | +| **RepoBench** | 多维度子任务 | 仓库级检索 | Python 等 | ✅ 完全公开 | M2 补充 | +| **CodeRagBench** | 7 维度 | RAG 检索问答 | Python | ✅ 完全公开 | M3 补充 | +| **自建记忆失效测试集** | 20–30 组 | 代码变更-记忆过期 | Python | 🔧 自定义构建 | M5 专属 | + +### 2.2 数据集详细介绍 + +#### SWE-bench(主评测集) + +- **来源**:https://github.com/SWE-bench/SWE-bench +- **数据格式**:真实 GitHub Issue 描述 + 目标仓库版本 + 验证 patch + 通过的测试用例 +- **任务**:给定一个 Issue,Agent 需定位并修改代码使其通过测试 +- **Lite 子集优势**:300 个实例,覆盖 12 个知名 Python 仓库(django、flask、requests、scikit-learn 等),迭代快速、失败噪声低 +- **Verified 子集**:500 个经人工验证确认可解的实例,评测结论更可靠 + +``` +SWE-bench Lite 仓库分布(示例): + django/django ~100 实例 + scikit-learn/scikit-learn ~40 实例 + psf/requests ~15 实例 + sympy/sympy ~40 实例 + ...共 12 个仓库 +``` + +#### DevBench(Agent Teams 专用) + +- **来源**:https://github.com/open-compass/DevBench +- **数据格式**:完整开发任务链——需求分析 → 架构设计 → 编码实现 → 测试验证 +- **特点**:任务间存在依赖关系,天然适合验证 Task DAG 调度 +- **仓库**:4 个不同领域的中等规模 Python 项目 + +#### RepoBench(检索补充) + +- **来源**:https://github.com/Leolty/repobench +- **子任务**:RepoSearch(跨文件检索)、RepoPlan(仓库级规划)、RepoEdit(仓库级编辑) +- **用途**:单独量化 Agent 在无知识辅助下的"搜索浪费" + +#### CodeRagBench(RAG 专项) + +- **来源**:https://huggingface.co/datasets/nickrosh/CodeRAGBench +- **七维度**:代码检索、文档检索、代码问答、库使用、Bug 定位、API 推荐、代码生成 +- **用途**:验证 Context Pack 的召回精度与覆盖率 + +--- + +## 3. 逐指标评测方案 + +### M1:首次定位正确文件耗时 + +| 维度 | 说明 | +|------|------| +| **数据集** | SWE-bench Lite(300 实例) | +| **Baseline** | 不带知识层的 Kimi Code CLI | +| **Candidate** | 带知识层(Repo Intelligence + Persistent Memory) | +| **测量方式** | 在工具调用日志中记录时间戳,计算 `T(first_open_correct_file) - T(task_start)` | +| **平均/中位数** | 报告均值和中位数,中位数可排除极端值干扰 | +| **正确文件判断** | 与 SWE-bench 提供的 golden patch 中修改的文件做交集,首次命中任一文件即计为"定位到正确文件" | + +``` +记录格式: + instance_id: "django__django-11099" + correct_files: ["django/db/models/fields/__init__.py"] + first_open_timestamp: 18.7s # 首次打开正确文件的耗时 + total_exploration_calls: 3 # 定位前的探索调用次数 +``` + +**预期**:Candidate 相比 Baseline 首次定位耗时下降 **40%–60%**。原理:知识层提供符号索引和模块依赖图,Agent 可以定向跳转而非逐文件搜索。 + +--- + +### M2:探索工具调用数 + +| 维度 | 说明 | +|------|------| +| **数据集** | SWE-bench Lite(300 实例) | +| **统计范围** | `grep`、`glob`、`read_file`、`list_directory` 等探索类工具 | +| **不统计** | `write_file`、`shell_run`、`git_commit` 等执行类工具 | +| **统计口径** | 每个实例的总探索调用数 + 定位到目标文件前的探索调用数,分别报告 | + +``` +对比维度: + Baseline: 平均 8.4 次探索调用才能定位目标 + Candidate: 平均 3.1 次探索调用(Repo map 直接导航) + 减少比例: ↓63% +``` + +**预期**:Candidate 相比 Baseline 探索调用数减少 **50%–65%**。原理:Tree-sitter 索引提供符号粒度的依赖导航,替代了逐文件 `grep` 和全量 `read_file`。 + +--- + +### M3:输入 Token 消耗 + +| 维度 | 说明 | +|------|------| +| **数据集** | SWE-bench Lite(300 实例) | +| **Baseline** | Agent 直接通过 `grep`/`read_file` 将代码片段追加到会话上下文 | +| **Candidate** | 知识层 Orchestrator 在推理前合成临时 Context Pack(含压缩后的符号摘要和记忆),原始会话不持久化快照 | +| **测量方式** | 从模型 API 日志中提取每个请求的 `input_tokens`,对任务内所有回合求和 | +| **Token 构成** | 区分"有效 token"(协助定位的代码)和"浪费 token"(无关文件/重复读取),仅报告总量 | + +``` +Token 分解(示例,单个实例): + Baseline Candidate + 系统 prompt: 2,100 2,100 + Context Pack: 0 1,800 ← 知识层注入(压缩后) + 代码读取: 8,400 2,200 ← 文件探索引入的代码 + 工具结果: 5,300 3,100 ← 其他工具(shell/测试)输出 + 对话历史: 2,400 2,300 + ───────────────────────────────────── + 合计: 18,200 11,500 + 节省: — ↓37% +``` + +**预期**:Candidate 相比 Baseline 输入 Token 减少 **30%–45%**。原理:① Context Pack 用压缩后的符号摘要替代原始文件内容;② 探索调用的减少连带降低了工具结果 token。 + +--- + +### M4:最终任务成功率 + +| 维度 | 说明 | +|------|------| +| **数据集** | **主测**:SWE-bench Lite(300 实例);**验证**:SWE-bench Verified(500 实例) | +| **评判标准** | 使用 SWE-bench 官方 evaluation harness。Agent 生成的 patch 通过该 Issue 关联的全部测试用例即计为"成功" | +| **指标** | Resolved Rate = 成功数 / 总实例数 | + +``` +SWE-bench 官方评测脚本逻辑: + 1. 将 Agent 生成的 patch 应用到目标仓库的正确 commit + 2. 运行该 Issue 的 FAIL_TO_PASS 测试(Issue 提交时失败、Fix 后应通过的测试) + 3. 运行该 Issue 的 PASS_TO_PASS 测试(始终应通过的回归测试) + 4. 全部通过 → resolved;任一失败 → unresolved +``` + +**与已知基线对比**: + +| 系统 | SWE-bench Lite Resolved Rate | 备注 | +|------|:---:|------| +| SWE-agent | 18.0% | 2024 年基线 | +| Aider | 26.3% | 2025 年基线 | +| Devin | ~35% | 闭源商业系统(近似值) | +| **Kimi Code CLI (Baseline)** | **待测** | 不加知识层 | +| **Kimi Code CLI (Candidate)** | **待测** | 加知识层 | + +**预期**:Candidate 相比 Baseline 成功率提升 **5–10 个百分点**。原理:更准确的代码定位 + 更少的无关上下文干扰,提升 Agent 生成正确 patch 的概率。 + +> 注意:成功率不会被优化过头导致降级——Context Pack 是增量补充而非替换,最坏情况等价于无知识层。 +> 如果 Candidate 成功率低于 Baseline,说明知识召回引入了噪声,需要缩小召回范围或提高精度阈值。 + +--- + +### M5:过期记忆误用率(自建测试集) + +详见 [第 4 章](#4-自建测试集记忆失效检测)。 + +**预期**:Candidate 相比 Baseline(无失效检测)误用率从 ~22% 降至 **5% 以下**。 + +--- + +### M6:Team 关键路径耗时 + +详见 [第 5 章](#5-agent-teams-专项评测)。 + +**预期**:Agent Teams(3–4 成员多 Agent)相比单 Agent 顺序执行,关键路径耗时下降 **30%–50%**。 + +--- + +## 4. 自建测试集:记忆失效检测 + +这是整个评测方案中**最具原创性**的部分,专门验证 Persistent Memory 的时效性管理。 + +### 4.1 测试集构造方法 + +从 SWE-bench 中利用 Git 历史自然构造"代码变更-记忆过期"场景: + +``` +构造流程: + 仓库时间线 + commit A (v1) ────────── commit B (v2) ──────────→ main + │ │ + │ Issue X (较老) │ Issue Y (较新) + │ 在 v1 上解决 │ 在 v2 上解决 + │ │ + 步骤 1:在 v1 上跑 Issue X │ + 步骤 2:沉淀记忆 M │ + (记录修改了哪些函数、为什么这样改) │ + │ │ + │ 步骤 3:切换到 v2(commit A→commit B) + │ 步骤 4:在 v2 上跑 Issue Y + │ 步骤 5:Agent 自动召回记忆 M + │ 步骤 6:检查 Agent 是否正确判断 M 的时效性 +``` + +### 4.2 四类记忆失效场景 + +| 场景编号 | 场景 | 构造方式 | 期望行为 | +|:--------:|------|----------|----------| +| F1 | **代码被删除** | 记忆 M 引用的函数/类在 commit A→B 中被删除 | Agent 标记 M 为 `stale`,不使用 | +| F2 | **代码被重构** | 记忆 M 引用的函数被改名或移动,blob hash 变化 | Agent 标记 M 为 `possibly_stale`,仅作弱参考 | +| F3 | **代码仍有效** | 记忆 M 引用的符号在两个 commit 间未变化,blob hash 一致 | Agent 标记 M 为 `valid`,正常使用 | +| F4 | **验证经验冲突** | 记忆 M 中的约束(如"禁止直接调用 X")在新版本中与代码实际情况矛盾 | Agent 标记 M 为 `conflicted`,触发人工确认 | + +### 4.3 指标计算 + +``` +过期记忆误用率 = 误用次数 / 总召回次数 + +定义: + - 总召回次数:Agent 在实验中从记忆库召回的记忆条目总数 + - 误用次数:Agent 基于已失效/冲突的记忆做出了错误决策的次数 + +细分: + - valid 召回准确率 = valid 判定且确实有效 / valid 判定总数 + - stale 召回率 = stale 判定且确实失效 / 实际失效总数 + - 冲突检出率 = 检出的冲突 / 实际存在的冲突 +``` + +### 4.4 测试规模 + +| 组合维度 | 数量 | +|----------|:----:| +| 仓库 | 3–4 个(从 SWE-bench 选活跃仓库如 django、flask、requests) | +| 每仓库 Issue 对 | 6–8 组 | +| 每 Issue 对沉淀记忆数 | 2–3 条 | +| **总测试用例** | **20–30 组,含 60–80 条记忆的失效判断** | + +--- + +## 5. Agent Teams 专项评测 + +### 5.1 数据集 + +**主选 DevBench**,辅以手动构造的多步骤任务。 + +DevBench 的四类任务天然形成依赖链: + +``` + 需求分析 ──→ 架构设计 ──→ 编码实现 ──→ 测试验证 + │ │ │ │ + (explorer) (architect) (coder) (reviewer+tester) + + explorer 和 architect 可部分并行(architect 拿到部分需求即可开始) + coder 严格依赖 architect 输出(设计文档) + reviewer 可和 tester 并行(各自独立验证代码的不同方面) +``` + +### 5.2 评估指标(DevBench 官方 + 自定义) + +| 指标 | 测量方式 | 说明 | +|------|----------|------| +| **任务完成率** | DevBench 官方评测脚本 | 每个阶段的产出物是否通过验收标准 | +| **端到端耗时** | 墙钟时间 `T(team_done) - T(team_start)` | 比较单 Agent vs Agent Teams | +| **关键路径耗时** | 从 Task DAG 中提取关键路径上的任务耗时之和 | 理论最短时间 | +| **并行效率** | `关键路径耗时 / 端到端耗时` | 越接近 1 说明并行越充分 | +| **消息有效性** | 成员间消息被实际用于任务决策的比例 | Mailbox 是否产生有效协作 | +| **失败恢复时间** | 注入成员异常后,恢复并重跑任务的花费 | 恢复机制的效率 | + +### 5.3 测试矩阵 + +``` + 单 Agent(顺序) Agent Teams(并行) + ───────────────────────────────────────────────────────── + DevBench-Repo1 一次完整运行 一次完整运行 + DevBench-Repo2 同上 同上 + DevBench-Repo3 同上 同上 + DevBench-Repo4 同上 同上 + 自建任务-1 同上 同上 + 自建任务-2 同上 同上 + ───────────────────────────────────────────────────────── + 总计 6 组 6 组 +``` + +### 5.4 额外测试:异常恢复 + +在 Agent Teams 实验中有意注入以下异常,验证恢复机制: + +| 异常类型 | 注入方式 | 衡量 | +|----------|----------|------| +| **成员崩溃** | 中途 kill 一个子 Agent 进程 | 恢复耗时、任务是否最终完成 | +| **依赖失败** | 架构设计阶段产出错误文档 | 下游 coder 是否被正确阻塞,Mailbox 是否有错误通知 | +| **预算超限** | 限制 Token 预算为正常量的 60% | 取消信号是否沿 DAG 传播,已完成的成员工作是否保留 | +| **写冲突** | 两个 Agent 尝试修改同一文件(模拟未来多写者场景) | 冲突是否被单写者策略阻止,错误信息是否可诊断 | + +--- + +## 6. 实验执行流程 + +### 6.1 环境标准化 + +``` +固定条件(必须保持一致,否则不可对比): + - 模型:如 claude-sonnet-5-20251001 + - 模型温度:temperature=0 + - 最大 Token 预算:每个任务 128K tokens + - 硬件:同一台机器,排除网络/负载波动(取多次运行中位数) + - SWE-bench 版本:固定一个 commit hash +``` + +### 6.2 实验矩阵(全量) + +| 实验组 | 数据集 | 样本数 | 模型 | 知识层 | Agent Teams | 重复次数 | +|--------|--------|:------:|------|:------:|:-----------:|:--------:| +| B-SWE-Lite | SWE-bench Lite | 300 | Sonnet | ❌ | ❌ | 1 | +| C-SWE-Lite | SWE-bench Lite | 300 | Sonnet | ✅ | ❌ | 1 | +| B-SWE-Verified | SWE-bench Verified | 500 | Sonnet | ❌ | ❌ | 1 | +| C-SWE-Verified | SWE-bench Verified | 500 | Sonnet | ✅ | ❌ | 1 | +| B-Memory | 自建记忆失效 | 25 | Sonnet | ❌ | ❌ | 2 | +| C-Memory | 自建记忆失效 | 25 | Sonnet | ✅ | ❌ | 2 | +| B-Team | DevBench + 自建 | 6 | Sonnet | ❌ | ❌ | 2 | +| C-Team | DevBench + 自建 | 6 | Sonnet | ✅ | ✅ | 2 | + +> B = Baseline, C = Candidate +> 总计约 1,168 次任务执行。如果时间有限,优先保证 SWE-bench Lite 主评测和记忆失效测试。 + +### 6.3 执行脚本模板 + +```bash +#!/bin/bash +# 单次评测运行脚本示例 +MODEL="claude-sonnet-5-20251001" +DATASET="SWE-bench Lite" +OUTPUT_DIR="results/$(date +%Y%m%d_%H%M%S)" + +mkdir -p "$OUTPUT_DIR" + +for instance in $(swebench list --subset lite); do + echo "=== Running $instance ===" + uv run kimi \ + --model "$MODEL" \ + --task "$(swebench get-issue "$instance")" \ + --repo "$(swebench get-repo "$instance")" \ + --base-commit "$(swebench get-base "$instance")" \ + --output "$OUTPUT_DIR/${instance}.json" \ + 2>&1 | tee "$OUTPUT_DIR/${instance}.log" +done + +# 汇总结果 +python scripts/eval/summarize.py "$OUTPUT_DIR" --dataset "$DATASET" +``` + +### 6.4 结果汇总脚本 + +```python +"""scripts/eval/summarize.py — 汇总评测结果并生成报告""" + +import json +import statistics +from pathlib import Path + +def summarize(result_dir: Path, dataset: str): + results = [] + for f in result_dir.glob("*.json"): + results.append(json.loads(f.read_text())) + + metrics = { + "dataset": dataset, + "total": len(results), + "resolved": sum(1 for r in results if r["resolved"]), + "resolved_rate": f"{sum(1 for r in results if r['resolved']) / len(results) * 100:.1f}%", + "avg_locate_time_s": f"{statistics.mean(r['first_correct_file_time_s'] for r in results):.1f}", + "median_locate_time_s": f"{statistics.median(r['first_correct_file_time_s'] for r in results):.1f}", + "avg_explore_calls": f"{statistics.mean(r['explore_calls'] for r in results):.1f}", + "avg_input_tokens": f"{statistics.mean(r['total_input_tokens'] for r in results):.0f}", + } + return metrics +``` + +--- + +## 7. 预期结果与填表示例 + +### 7.1 按实验组预期的量化幅度 + +| 指标 | 预期变化 | 原理 | 风险(低于预期怎么办) | +|------|:-------:|------|------| +| M1 定位耗时 | **↓ 40%–60%** | 符号索引替代逐文件搜索 | 若下降 < 30%,说明索引不够细粒度或召回噪声过大 | +| M2 探索调用 | **↓ 50%–65%** | Repo map 直接导航 | 若下降 < 30%,需要增加跨文件依赖索引 | +| M3 Token 消耗 | **↓ 30%–45%** | Context Pack 压缩 + 探索减少 | 若下降 < 15%,Context Pack 本身可能过大 | +| M4 成功率 | **+5–10pp** | 精确定位 + 上下文质量提升 | 若持平或下降,需排查知识召回是否引入噪声 | +| M5 记忆误用率 | **↓ 78% 以上** | Git-aware 失效检测 | 若误用率 > 10%,需收紧 stale 判定阈值 | +| M6 关键路径 | **↓ 30%–50%** | DAG 并行调度 | 若下降 < 20%,说明任务间依赖太强、可并行度低 | + +### 7.2 简历填表示例(完成评测后填入实际数据) + +| 指标 | Baseline | Candidate | 变化 | 解释 | +|------|:--------:|:---------:|:----:|------| +| 首次定位正确文件耗时(中位数) | 42.3s | 18.7s | **↓ 56%** | 知识层的符号索引使 Agent 定向跳转,不再逐文件 grep | +| Grep / ReadFile 调用数(均值) | 8.4 次 | 3.1 次 | **↓ 63%** | Repo map 通过模块依赖图直接关联目标,避免全仓库搜索 | +| 输入 Token 消耗(均值) | 18.2K | 11.5K | **↓ 37%** | Context Pack 用压缩符号摘要替代原始文件,探索减少连带降低工具结果 Token | +| 最终任务成功率(Resolved Rate) | 28.3% | 35.7% | **+7.4pp** | 更准的定位 + 更少的噪声上下文,提升 patch 生成的正确率 | +| 过期记忆误用率 | 22.5% | 4.8% | **↓ 79%** | Git blob hash + 符号存在性双重校验,stale 记忆自动降级 | +| Team 关键路径耗时 | 187.0s | 114.0s | **↓ 39%** | explorer / coder / reviewer 按 DAG 依赖并行执行,消除等待 | + +> ⚠️ 表中 Baseline/Candidate 数值为**占位示例**,必须在真实实验后替换。所有百分比从原始数据计算得出,保留原始实验日志备查。 + +--- + +## 8. 风险与应对 + +| 风险 | 影响 | 应对策略 | +|------|------|----------| +| **SWE-bench 评测耗时长** | 300 实例 × 2 组 = 600 次运行,单次平均 3–5 分钟,总计 30–50 小时 | 先跑 SWE-bench Lite 的随机 50 实例做快速试跑,确认方向正确再全量 | +| **指标达不到预期幅度** | 简历数据缺乏竞争力 | 分析是哪个环节不足:索引不够细→增加 AST 遍历深度;召回噪声→提高 precision 阈值;并行度低→调整 Task DAG 拆解策略 | +| **自建记忆测试集构造复杂** | 手选 Issue 对耗时且容易遗漏场景 | 先写脚本从 SWE-bench 自动匹配同仓库的连续 Issue,人工审核后入库 | +| **DevBench 任务不适合 Kimi CLI 的工具集** | Agent 无法完成 DevBench 的设计或测试任务 | 降级为自定义多步骤任务(设计→编码→审查),内容可以手动拟定 | +| **模型版本变动** | 模型升级后基线数据过期 | 冻结一个模型版本号,评测报告标注模型版本;换模型时注明不可直接对比 | + +--- + +## 附录 A:SWE-bench Lite 仓库分布参考 + +| 仓库 | 实例数 | 示例 Issue | +|------|:------:|------------| +| django/django | ~100 | 修复 ORM 查询集排序、模板引擎边界条件 | +| scikit-learn/scikit-learn | ~40 | 修复交叉验证、特征提取的参数验证 | +| matplotlib/matplotlib | ~35 | 修复图表渲染、坐标轴标签位置 | +| sympy/sympy | ~40 | 修复符号计算的化简、积分表达式 | +| psf/requests | ~15 | 修复 HTTP 重定向、SSL 验证逻辑 | +| pydata/xarray | ~15 | 修复多维数组索引、NetCDF 读写 | +| pylint-dev/pylint | ~10 | 修复静态分析规则的误报 | +| pytest-dev/pytest | ~15 | 修复测试框架的 fixture 作用域 | +| sphinx-doc/sphinx | ~15 | 修复文档构建的交叉引用 | +| astropy/astropy | ~10 | 修复天文计算的单位转换 | + +## 附录 B:评测产出的文件清单 + +``` +results/ +├── 20250808_swe_lite_baseline/ +│ ├── django__django-11099.json +│ ├── django__django-11099.log +│ ├── ... +│ └── summary.json +├── 20250808_swe_lite_candidate/ +│ ├── ... +│ └── summary.json +├── 20250808_memory_failure/ +│ ├── scenario_01__function_deleted.json +│ ├── ... +│ └── summary.json +├── 20250808_agent_teams/ +│ ├── devbench_repo1_baseline.json +│ ├── devbench_repo1_teams.json +│ ├── ... +│ └── summary.json +└── report.md # 最终汇总报告 +``` diff --git "a/docs/mk/\346\272\220\347\240\201\346\236\266\346\236\204\344\270\216\350\260\203\350\257\225\346\214\207\345\215\227.md" "b/docs/mk/\346\272\220\347\240\201\346\236\266\346\236\204\344\270\216\350\260\203\350\257\225\346\214\207\345\215\227.md" new file mode 100644 index 0000000000..52823b6077 --- /dev/null +++ "b/docs/mk/\346\272\220\347\240\201\346\236\266\346\236\204\344\270\216\350\260\203\350\257\225\346\214\207\345\215\227.md" @@ -0,0 +1,135 @@ +# Kimi Code CLI:源码架构、阅读路线与调试指南 + +本文面向需要修改或排查 Kimi Code CLI 的开发者。仓库是以 Python 为核心、以 `uv` 管理的 monorepo;根包提供 CLI 与运行时,`packages/` 提供可复用底层库,`web/`、`vis/` 提供浏览器界面。 + +## 架构总览 + +```text +命令行:src/kimi_cli/__main__.py → cli/__init__.py + ↓ 创建会话并选择运行模式 +应用装配:app.py(KimiCLI.create) + ├─ config.py / llm.py:读取配置、建立模型提供商 + ├─ session.py:管理 context.jsonl、wire.jsonl 与状态 + ├─ soul/agent.py:加载 Agent YAML、系统提示词、Skills 和工具 + └─ soul/kimisoul.py:Agent 主循环 + ↓ + soul/toolset.py:执行内置、插件和 MCP 工具调用 + ↓ + wire/:将回合、步骤、工具与审批事件发送给 UI + ↓ + ui/shell、ui/print、ui/acp,或 Wire 服务 +``` + +- `src/kimi_cli/agents/`:Agent YAML 与提示词;`agentspec.py` 处理继承和工具声明。 +- `src/kimi_cli/tools/`:文件、Shell、子 Agent、计划、MCP 等内置工具;工具由 `KimiToolset` 注入运行时依赖。 +- `src/kimi_cli/soul/`:上下文、压缩、审批、斜杠命令和主循环,是行为变更的核心区域。 +- `src/kimi_cli/wire/`:UI 与核心之间的事件协议;`ui/`、`acp/` 是不同消费者。 +- `packages/kosong/`、`packages/kaos/`:LLM 抽象与本地/远程 OS 抽象;`tests/`、`tests_e2e/`、`tests_ai/` 分别覆盖单元、端到端和 AI 场景。 + +## 从 Agent 学习视角阅读源码 + +不要按目录从上到下通读。更有效的办法是选定一个最小场景,例如 `uv run kimi --print -p "列出当前目录"`,并持续追问:Agent 收到了什么?它看到了什么?它能做什么?它如何决定下一步?结果怎样回到用户?下面的顺序正好对应这五个问题。 + +### 1. 用户输入:CLI 如何发起一次任务 + +先读 `src/kimi_cli/__main__.py`,确认它只将执行权转交给 `cli/__init__.py`。随后重点读 Typer 的 `kimi()` 回调:它解析 `--prompt`、`--print`、`--session`、`--model` 等选项,创建或恢复 `Session`,再选择 Shell、Print、ACP 或 Wire 模式。 + +建议先只使用 Print 模式,因为它没有 TUI 输入循环,单条指令结束后进程会退出,调用链最短: + +```sh +uv run kimi --print -p "只读取并列出 src/kimi_cli/soul 下的 Python 文件" +``` + +此阶段不必理解每个 CLI 选项。完成标准是能指出:用户提示词和会话 ID 在哪里产生,以及为什么 CLI 不直接调用模型。 + +### 2. 运行时装配:Agent 的依赖从哪里来 + +接着读 `src/kimi_cli/app.py` 的 `KimiCLI.create()`,再读 `KimiCLI.run()`。前者是装配根:依次加载 `Config`,用 `llm.py` 创建 LLM,调用 `Runtime.create()` 扫描工作目录,加载 Agent、恢复 `Context`,最终构造 `KimiSoul`。后者通过 `run_soul()` 将核心循环连接到 UI。 + +这一步要建立四个对象的边界: + +- `Config` 是静态运行配置,如 provider、模型、循环上限和工具行为。 +- `Session` 是一次会话的文件与状态容器;其目录保存 `context.jsonl`、`wire.jsonl`、`state.json` 等可追溯数据。 +- `Runtime` 是共享依赖集合,包括 LLM、审批、Skills、子 Agent 注册表、后台任务和环境信息。 +- `KimiSoul` 是真正处理用户回合的 Agent;它持有 `Agent` 与 `Context`,但不关心具体终端界面。 + +完成标准是可以从 `KimiCLI.create()` 画出 `Config → Runtime → Agent + Context → KimiSoul` 的构造关系,并理解 `app.py` 是编排层而非推理层。 + +### 3. 角色与能力:系统提示词、规则和工具如何进入模型 + +然后阅读 `src/kimi_cli/agents/default/agent.yaml`、`system.md`,接着阅读 `agentspec.py` 与 `soul/agent.py` 的 `load_agent()`。YAML 定义系统提示词、工具 import path 和内建子 Agent;`agentspec.py` 解析并处理 `extend`;`load_agent()` 渲染提示词、注册子 Agent 并建立 `KimiToolset`。 + +重点追踪 `Runtime.create()` 中的三类动态输入: + +- `AGENTS.md`:从项目根到当前工作目录合并后,以 `KIMI_AGENTS_MD` 传入系统提示词。 +- Skills:`skill.py` 发现用户、项目和额外目录中的 Skill,并将摘要放入 `KIMI_SKILLS`。 +- 工作区信息:当前目录、目录列表、操作系统和 Shell 等被填入 `BuiltinSystemPromptArgs`。 + +这解释了 Agent 为什么既有通用指令,又能遵守仓库局部规则。完成标准是能从一个 YAML 工具路径定位到其 Python 实现,并说清“工具是否可用”与“模型是否选择调用”是两件不同的事。 + +### 4. 记忆:消息如何持久化并再次成为上下文 + +阅读 `session.py`、`soul/context.py`,再回到 `app.py` 中的 `Context.restore()`。`Session` 把一个工作目录映射到会话目录;`Context` 负责读取、追加和检查点化消息历史,而不是简单在内存中维护列表。系统提示词、User/Assistant/Tool 消息与 token 用量记录均写入 `context.jsonl`。 + +需要特别理解三个动作:`append_message()` 把新消息写入历史,`checkpoint()` 在每个关键阶段留下回退点,`revert_to()` 在需要撤回时轮转文件并恢复到指定检查点。长上下文时,`kimisoul.py` 通过 `compaction.py` 做自动压缩。 + +完成标准是能回答:重启后 Agent 为什么记得此前对话;一次工具返回为什么会影响下一次模型输出;上下文过长时为什么不一定立刻报错。 + +### 5. 决策循环:一次回合怎样变成多步行动 + +现在阅读核心文件 `src/kimi_cli/soul/kimisoul.py`,顺序是 `KimiSoul.run()` → `_turn()` → `_agent_loop()` → `_step()`。先看 `run()`:它触发 `UserPromptSubmit` hook,发送 `TurnBegin`,分流 slash command 或普通提示词,并在结束时发送 `TurnEnd`。 + +普通提示词进入 `_turn()` 后会先写入 `Context`。`_agent_loop()` 是多步循环:检查步数上限、发送 `StepBegin`、在必要时压缩上下文、创建 checkpoint,并调用 `_step()`。`_step()` 将当前消息、系统提示词和可用工具提交给 LLM;若模型返回工具调用,工具结果会追加为 Tool 消息,循环进入下一步;若没有工具调用,回合结束并把最终文本交给 UI。 + +将下图与代码中的 Wire 消息对照阅读: + +```text +User prompt → Context + → StepBegin → LLM response + ├─ final text ─────────────→ TurnEnd + └─ tool call → ToolResult → Context → 下一 Step +``` + +同时留意取消、最大步数、LLM 错误、重复工具调用和上下文压缩这些分支;它们才是生产环境中 Agent 行为与理想流程不同的主要原因。完成标准是能解释同一回合为何可能发出多个 `StepBegin`,以及最终回答为何可能在数次工具调用之后才生成。 + +### 6. 行动与约束:工具调用、审批和扩展 + +随后读 `soul/toolset.py` 的 `load_tools()`、`handle()`,再选择一个具体工具阅读,例如 `tools/shell/` 或 `tools/file/`。前者从 Agent 规格给出的 import path 加载工具并注入 `Runtime` 等依赖;后者解析模型提供的 JSON 参数、检查工具存在性、处理同一步并发与重复调用,并收集 `ToolResult`。 + +再读 `soul/approval.py` 与 `approval_runtime/`。它们把高风险操作的“是否允许”从工具实现中抽离:工具请求审批,UI 或运行模式给出回应,再由运行时恢复等待的调用。理解完内置工具后,扩展到 MCP:配置由 `mcp.py` 管理,MCP 工具在 `load_agent()` / `KimiToolset` 中接入;插件工具也在这里合并。 + +完成标准是能沿 `tool call → KimiToolset.handle() → 具体工具 → ToolResult` 跟踪一次调用,并能定位“模型没调用工具”“工具被拒绝”“工具异常”分别应查看的位置。 + +### 7. 反馈层:事件如何变为终端、IDE 或可视化结果 + +最后读 `soul/__init__.py` 的 `run_soul()`、`wire/`,再读 `ui/print/`。`run_soul()` 创建 `Wire`,并行运行 Soul 与 UI 循环,把 `wire_send()` 产生的消息持久化到 `wire.jsonl` 并交给 UI。Print UI 最适合首先理解:它把同一批事件转成文本或 JSONL;Shell、ACP 和 Wire Server 则是不同协议消费者。 + +这一层体现了关键解耦:核心循环不直接 `print`,也不了解 IDE;它只发布 `TurnBegin`、`StepBegin`、文本、工具结果和审批请求等 Wire 消息。完成标准是能从 `wire_send(TextPart(...))` 找到 Print 输出位置,或在 `kimi vis` 中找到相同事件的时间线记录。 + +### 建议的学习节奏 + +第一天完成第 1–2 步,并能用断点走通 `KimiCLI.create()`。第二天完成第 3–5 步,使用一个只读提示词在 `KimiSoul.run()`、`_agent_loop()`、`_step()` 处观察对象变化。第三天完成第 6–7 步,选择一个工具写或修改一个测试,再用 `kimi vis` 对比 `context.jsonl` 与 `wire.jsonl`。完成这一轮后,再进入 `tools/agent/`、`subagents/`、`background/`、hooks 与 MCP;此时阅读多 Agent 和异步扩展时才不会失去主线。 + +## 调试方案一:真实回合 + 可视化追踪 + +适合排查「模型为何调用某工具、上下文何时变化、Wire 事件是否完整」。先使用只读提示词,避免自动审批带来工作区修改: + +```sh +uv run kimi --debug --print -p "只读取并概述 src/kimi_cli/app.py 的职责;不要修改文件" +uv run kimi vis --no-open +``` + +第一条命令以本地源码启动真实运行时;`--debug` 会将诊断信息写入 `~/.kimi/logs/kimi.log`。第二条命令启动 Tracing Visualizer(默认 `http://127.0.0.1:5495`),从会话列表打开刚才的会话,按时间线查看 `Turn`、`Step`、工具调用和返回值,并在上下文视图核对消息。原始可复现证据也保存在会话目录的 `context.jsonl`、`wire.jsonl` 和 `state.json`;可用 `kimi export ` 打包共享。 + +## 调试方案二:定点测试 + 断点/输出 + +适合已定位到函数或回归场景,例如修改工具调度、Agent 规格或某个 Wire 消息后验证结果。先找最近的测试,再仅运行目标用例: + +```sh +rg -n "KimiToolset|目标函数名" tests tests_e2e +uv run pytest tests/test_目标模块.py -k "目标行为" -vv -s +``` + +在待验证分支临时放置 `breakpoint()`,重新运行第二条命令即可进入 `pdb`,检查输入、`Runtime` 状态和返回的 `ToolResult`;移除断点后再次运行。若不需要交互调试,可把断言所需的中间值写入测试失败信息,或运行 `uv run pytest ... --log-cli-level=DEBUG -vv -s` 查看标准日志。完成后至少执行对应测试;跨模块改动再运行 `make check` 与 `make test`。 + +> 不要提交临时 `breakpoint()`、调试输出、真实 API 密钥或会话日志。涉及写操作的端到端验证应使用临时工作目录或明确的测试夹具。 diff --git "a/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI.md" "b/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI.md" new file mode 100644 index 0000000000..d51e6d8a2a --- /dev/null +++ "b/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI.md" @@ -0,0 +1,140 @@ +# Kimi Code CLI — 秋招项目简历 + +> **项目名称**:Kimi Code CLI:具备持久化项目知识与多 Agent Teams 编排能力的开源 Coding Agent +> **角色**:核心开发者 +> **时间**:2025.06 — 至今 +> **关键词**:`Python` `asyncio` `SQLite/FTS5` `Tree-sitter` `Pydantic` `Typer` `Git` `pytest` `Multi-Agent` `Event-Driven` + +--- + +## S — 背景 Situation + +Kimi Code CLI 是一个终端里的 AI Coding Agent,具备异步主循环、会话持久化、子 Agent、后台任务、审批和事件驱动 UI 等能力。但在大型仓库长期开发场景中暴露出两个瓶颈: + +- **知识断层**:每个新会话都要重新搜索代码,历史架构决策、工程约束和踩坑经验无法跨会话复用。 +- **协作缺失**:虽能并发启动子 Agent,但缺乏任务依赖、消息通信、共享工件和失败恢复机制,多 Agent 各自为战。 + +> 📐 **代码规模背景**:运行时 Python ~6.6 万行,测试 ~6.4 万行(来自仓库调研快照,非个人产出) + +--- + +## T — 任务 Task + +在不另建旁路 Agent 系统的前提下,基于现有 Runtime 和 Session 基础设施,完成两项核心改造: + +| 知识平面 | 协作平面 | +|----------|----------| +| 设计并实现统一项目知识层,让 Agent 能够增量索引代码结构、持久化工程记忆,并在推理前注入经过时效性校验的 Context Pack | 在现有子 Agent 和后台任务之上构建 Agent Teams 协作运行时,实现任务 DAG 编排、成员消息、共享工件、预算调度和崩溃恢复 | + +--- + +## A — 行动 Action + +### Phase 1:代码级调研与架构设计 + +- 阅读并梳理全部运行时代码(CLI 入口 → Agent 主循环 → Session 持久化 → 子 Agent → 后台任务 → 审批 → Wire 事件链路),建立完整调用链认知。 +- 识别现有系统的边界与复用点:`KimiSoul` 主循环、`SubagentStore` 持久化、`BackgroundTaskManager` 调度、`ApprovalRuntime` 审批、`Wire` 事件流。 +- 输出 **"Repo Intelligence + Persistent Memory + Agent Teams"** 方案设计,包含数据模型、接入点、状态机、测试矩阵、评测指标和分阶段开发计划。 + +### Phase 2:知识层原型实现 + +- 使用 **Tree-sitter** 构建 Python 符号、引用与模块依赖的增量索引,支持按 commit 跟踪代码变更。 +- 以 **SQLite + FTS5** 持久化四类工程记忆:架构决策、工程约束、编码偏好、验证经验,每条记忆绑定来源 commit、blob hash 和符号引用。 +- 实现 **Knowledge Orchestrator**:在 Agent 推理前并行召回代码事实和历史记忆,去重、冲突标记、来源追踪,按 Token 预算合成临时 Context Pack(不永久写入会话历史)。 +- 基于 Git blob 和符号存在性实现记忆失效检测:召回时自动标记 `valid` / `possibly_stale` / `stale` / `conflicted`。 + +### Phase 3:Agent Teams 协作运行时 + +- 建立 **Team / Member / Task DAG** 领域模型与持久化状态机,复用现有 Subagent、BackgroundTaskManager 和 ApprovalRuntime 基础设施。 +- 实现 **Mailbox** 消息系统,支持成员间结构化消息传递与事件驱动通知。 +- 设计 **Artifact Index**,仅保存工件元数据与引用,避免冗余存储。 +- 实现 **预算传播与取消级联**:超限或异常时沿任务依赖图向上传播取消信号。 +- 采用**单写者策略**避免多 Agent 文件写冲突,后续预留 worktree 隔离扩展点。 + +### Phase 4:测试与质量保障 + +- 覆盖分支切换、代码删除、过期记忆、Agent 异常退出、依赖失败、并发写冲突等场景的单元测试和端到端测试。 +- 建设可复现的效果评测脚本,固定仓库、固定任务、固定模型和固定预算下对比 baseline 和 candidate 的表现。 + +--- + +## R — 成果 Result + +| 指标 | Baseline | Candidate | 解释 | +|------|:--------:|:---------:|------| +| 首次定位正确文件耗时 | — | — | 知识层是否减少盲目探索 | +| Grep / ReadFile 调用数 | — | — | Repo map 是否减少重复读取 | +| 输入 Token 消耗 | — | — | Context Pack 是否节省上下文 | +| 最终任务成功率 | — | — | 优化不能只降低成本 | +| 过期记忆误用率 | — | — | 失效机制是否可靠 | +| Team 关键路径耗时 | — | — | 并行是否真正缩短交付时间 | + +> ⚠️ 评测指标需来自固定任务集的实际实验,不可用预期值代替。秋招前应至少完成知识层或 Agent Teams 闭环之一。 + +--- + +## 技术栈 + +| 类别 | 技术 | +|------|------| +| 语言 | Python、asyncio | +| 数据 | SQLite / FTS5 | +| 解析 | Tree-sitter | +| 框架 | Pydantic、Typer | +| 工具 | Git、pytest | +| 架构 | Event-Driven、Multi-Agent、DAG Scheduling、Token Budget | + +--- + +## 关键代码位置 + +| 模块 | 路径 | +|------|------| +| Agent 主循环 | `src/kimi_cli/soul/kimisoul.py` | +| 会话持久化 | `src/kimi_cli/session.py` | +| 子 Agent 持久化 | `src/kimi_cli/subagents/store.py` | +| 多 Agent 执行 | `src/kimi_cli/tools/agent/` | +| 后台任务管理 | `src/kimi_cli/background/` | +| 安全审批 | `src/kimi_cli/approval_runtime/` | +| 事件驱动 UI | `src/kimi_cli/wire/` | + +--- + +## 面试核心表达 + +> 我没有简单加入向量数据库或批量启动多个 Agent,而是先划清事实、记忆、编排和执行的边界。代码索引只保存可验证的当前事实,长期记忆保存决策和经验,Team Scheduler 只处理任务状态与资源约束,LLM 负责语义判断但不充当唯一状态机。 + +--- + +## 高频追问速答 + +**Q: 为什么不用向量数据库?** + +MVP 的决策和符号名称对关键词检索较友好,SQLite/FTS5 部署零成本、可离线、易调试。先建立召回基线,语义召回不足时再引入 embedding。 + +**Q: 知识为什么不直接写进 Context?** + +Repo 快照随 HEAD 变化,永久追加会导致旧快照重复加载且难以判废。正确做法是临时合成 effective history,原始会话只存真实交互。 + +**Q: Agent Teams 和并发子 Agent 有什么区别?** + +并发只解决同时运行;Teams 还要解决任务分配、依赖就绪、结果路由、失败传播、工件共享、写权限、预算和恢复。本质上是 LLM 驱动 + 确定性状态机约束的协作运行时。 + +**Q: 多 Agent 写冲突怎么办?** + +MVP 单写者策略:探索/评审可并行,仅一个 coder 持有写租约。后续引入 worktree 隔离 + 集成合并。 + +--- + +## 真实性检查清单 + +提交简历前逐项确认: + +- [ ] "实现""优化""提升"等动词均有代码、测试或实验结果支撑 +- [ ] 规划中的模块没有写成已经上线 +- [ ] 百分比使用固定任务集重复实验得到,并保留原始结果 +- [ ] 能指出每条简历 bullet 对应的源码路径、测试和提交 +- [ ] 能解释至少一个失败案例和一次设计取舍 +- [ ] 没有把整个开源仓库的代码量或 star 数当作个人贡献 +- [ ] 没有把应用层审批描述为 OS 级沙箱 +- [ ] 没有把现有独立后台 Agent 描述成已经具备完整 Teams 编排 diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index a2a422bc91..c51a022965 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -2,6 +2,24 @@ Kimi Code CLI 支持通过环境变量覆盖配置或控制运行行为。本页列出所有支持的环境变量。 +## 项目本地 `.env` + +启动 CLI 时,若工作目录中存在 `.env`,Kimi Code CLI 会读取其中的变量作为本次运行的 LLM +配置覆盖。它不会修改进程环境变量、`~/.kimi/config.toml` 或其他项目;`.env` 中的值优先于 +启动 CLI 的环境变量。也可通过 `--env-file PATH` 指定其他文件。 + +在 `.env` 中设置 `KIMI_CONFIG_FILE` 可让项目默认使用指定的 TOML/JSON 配置文件,无需每次 +传入 `--config-file`。相对路径以工作目录为基准;显式传入的 `--config` 或 `--config-file` +优先于该设置。 + +```sh +# 项目根目录/.env(应保持在 .gitignore 中) +KIMI_API_KEY=sk-xxx +KIMI_BASE_URL=https://api.moonshot.cn/v1 +KIMI_MODEL_NAME=kimi-k2-thinking-turbo +KIMI_CONFIG_FILE=dev/kimi.toml +``` + 关于环境变量如何覆盖配置文件的详细说明,请参阅 [配置覆盖](./overrides.md)。 ## Kimi 环境变量 diff --git a/docs/zh/reference/kimi-command.md b/docs/zh/reference/kimi-command.md index 7de8bdd866..fc997f11ce 100644 --- a/docs/zh/reference/kimi-command.md +++ b/docs/zh/reference/kimi-command.md @@ -30,8 +30,9 @@ kimi [OPTIONS] COMMAND [ARGS] |------|------| | `--config STRING` | 加载 TOML/JSON 配置字符串 | | `--config-file PATH` | 加载配置文件(默认 `~/.kimi/config.toml`) | +| `--env-file PATH` | 加载仅用于本次运行 LLM 配置的 dotenv 文件;默认自动读取工作目录的 `.env`(如果存在) | -`--config` 和 `--config-file` 互斥。配置字符串和文件均支持 TOML 和 JSON 格式。详见 [配置文件](../configuration/config-files.md)。 +`--config` 和 `--config-file` 互斥。配置字符串和文件均支持 TOML 和 JSON 格式。详见 [配置文件](../configuration/config-files.md)。项目根目录 `.env` 中的 `KIMI_CONFIG_FILE` 可提供未显式指定时的默认配置文件。 ## 模型选择 diff --git a/pyproject.toml b/pyproject.toml index ddea4e11ef..7a7a0870cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "tenacity==9.1.2", "fastmcp==3.2.4", "pydantic==2.12.5", + "python-dotenv==1.2.1", "httpx[socks]==0.28.1", "pykaos==0.9.0", "batrachian-toad==0.5.23; python_version >= \"3.14\"", diff --git a/src/kimi_cli/__main__.py b/src/kimi_cli/__main__.py index 2d8aded024..9157f608ed 100644 --- a/src/kimi_cli/__main__.py +++ b/src/kimi_cli/__main__.py @@ -1,3 +1,13 @@ +"""Kimi CLI 的命令行进程入口。 + +这个模块非常薄,不包含任何业务逻辑,只负责三件事: +1. 在程序最开始安装崩溃处理器(crash handler)并规范化代理环境变量; +2. 单独处理 ``kimi --version`` / ``kimi -V`` 这种极简命令; +3. 把剩余参数原样交给 Typer 的 ``cli``(真正的 CLI 定义在 + ``kimi_cli/cli/__init__.py``),并把 ``SystemExit`` / 已知异常 + 统一翻译成进程退出码。 +""" + from __future__ import annotations import sys @@ -6,19 +16,27 @@ def _prog_name() -> str: + # 取 argv[0] 的文件名(如 "kimi" 或 "kimi-cli")作为程序名, + # 用于 Typer/Click 的帮助文本与错误提示。 return Path(sys.argv[0]).name or "kimi" def main(argv: Sequence[str] | None = None) -> int | str | None: + # 延迟导入:把重量级模块推迟到真正需要时,加快启动速度。 from kimi_cli.telemetry.crash import install_crash_handlers, set_phase from kimi_cli.utils.proxy import normalize_proxy_env - # Install excepthook before anything else so startup-phase crashes are captured. + # 第一步:在任何其它初始化之前安装 excepthook, + # 确保启动阶段的崩溃也能被捕获、记录到 telemetry 崩溃日志。 install_crash_handlers() + # 把 HTTP(S)_PROXY / NO_PROXY 等代理环境变量规范化(处理小写、空值等), + # 让 aiohttp/httpx 等库能正确读取代理配置。 normalize_proxy_env() + # 支持以列表形式传入参数(便于测试);默认取 sys.argv。 args = list(sys.argv[1:] if argv is None else argv) + # `kimi --version` / `kimi -V` 单独短路,避免启动整个 Typer 应用。 if len(args) == 1 and args[0] in {"--version", "-V"}: from kimi_cli.constant import get_version @@ -29,15 +47,21 @@ def main(argv: Sequence[str] | None = None) -> int | str | None: from kimi_cli.utils.environment import GitBashNotFoundError try: + # 把控制权交给 Typer 的 cli:参数解析、子命令分发、主命令回调都在那里。 return cli(args=args, prog_name=_prog_name()) except SystemExit as exc: + # Typer/Click 通过 SystemExit 表达退出码,这里原样返回(而非抛出), + # 方便上层(如 PyInstaller 打包后的入口)拿到退出码。 return exc.code except GitBashNotFoundError as exc: + # Windows 下执行 shell 工具需要 git-bash,缺失时给出明确提示。 print(f"Error: {exc}", file=sys.stderr) return 1 finally: + # 无论正常退出还是异常,都把 telemetry 阶段标记为 shutdown。 set_phase("shutdown") if __name__ == "__main__": + # 让返回值成为进程退出码。 raise SystemExit(main()) diff --git a/src/kimi_cli/app.py b/src/kimi_cli/app.py index a3d4d0a87a..41127aae98 100644 --- a/src/kimi_cli/app.py +++ b/src/kimi_cli/app.py @@ -1,3 +1,14 @@ +"""应用层装配与运行入口。 + +本模块把 CLI 层解析好的参数真正“组装”成一个可运行的 agent: +- ``KimiCLI.create`` 是核心装配流程(配置 → OAuth → LLM → Runtime → Agent → + Context → KimiSoul → hooks → telemetry),几乎所有模块都在这里接线; +- ``KimiCLI.run`` 把 soul 的 Wire 输出桥接到外部调用方(UI 层 / 测试),并负责 + 把审批请求等“轮外”消息从 RootWireHub 投影到当前 wire; +- ``run_shell`` / ``run_print`` / ``run_acp`` / ``run_wire_stdio`` 是四种前端 + 入口,供 ``cli/__init__.py`` 的 match ui 分发调用。 +""" + from __future__ import annotations import asyncio @@ -6,7 +17,7 @@ import sys import time import warnings -from collections.abc import AsyncGenerator, Callable +from collections.abc import AsyncGenerator, Callable, Mapping from pathlib import Path from typing import TYPE_CHECKING, Any @@ -29,6 +40,7 @@ from kimi_cli.soul.kimisoul import KimiSoul from kimi_cli.soul.toolset import KimiToolset from kimi_cli.utils.aioqueue import QueueShutDown +from kimi_cli.utils.dotenv import load_llm_env from kimi_cli.utils.envvar import get_env_bool from kimi_cli.utils.logging import logger, open_original_stderr, redirect_stderr_to_logger from kimi_cli.utils.path import shorten_home @@ -50,6 +62,8 @@ def _patch_session_id(record: dict[str, Any]) -> None: record["extra"].setdefault("sid", "") +# 配置 loguru 全局日志:写入 ~/.kimi/logs/kimi.log(debug 时开 TRACE 并启用 kosong 日志), +# 可选地把进程 fd=2(stderr)重定向到日志文件。 def enable_logging(debug: bool = False, *, redirect_stderr: bool = True) -> None: # NOTE: stderr redirection is implemented by swapping the process-level fd=2 (dup2). # That can hide Click/Typer error output during CLI startup, so some entrypoints delay @@ -90,6 +104,7 @@ def _write_original_stderr(text: str) -> None: sys.stderr.write(text) +# 后台静默刷新“托管模型”列表(来自平台的模型目录),失败只告警、不影响启动。 async def _refresh_managed_models_silent(config: Config) -> None: from kimi_cli.auth.platforms import refresh_managed_models @@ -99,6 +114,7 @@ async def _refresh_managed_models_silent(config: Config) -> None: logger.warning("Background managed-model refresh failed: {error}", error=exc) +# 启动时清理上次异常退出遗留的“前台”子 agent 实例,把它们标记为 failed。 def _cleanup_stale_foreground_subagents(runtime: Runtime) -> None: subagent_store = getattr(runtime, "subagent_store", None) if subagent_store is None: @@ -118,12 +134,15 @@ def _cleanup_stale_foreground_subagents(runtime: Runtime) -> None: class KimiCLI: + """一次可运行的 agent 实例;由 CLI 层构造,UI 层持有并调用其 run_* 方法。""" + @staticmethod async def create( session: Session, *, # Basic configuration config: Config | Path | None = None, + env_file: Path | None = None, model_name: str | None = None, thinking: bool | None = None, # Run mode @@ -151,6 +170,8 @@ async def create( session (Session): A session created by `Session.create` or `Session.continue_`. config (Config | Path | None, optional): Configuration to use, or path to config file. Defaults to None. + env_file (Path | None, optional): Dotenv file used only for LLM settings. When unset, + use ``.env`` in the session work directory if it exists. model_name (str | None, optional): Name of the model to use. Defaults to None. thinking (bool | None, optional): Whether to enable thinking mode. Defaults to None. yolo (bool, optional): Approve all actions without confirmation. Defaults to False. @@ -186,12 +207,16 @@ async def create( MCPRuntimeError(KimiCLIException, RuntimeError): When any MCP server cannot be connected. """ + # ==== 阶段 1:加载配置 ==== + # _create_t0 / _phase_timings_ms 用于记录各阶段耗时,上报 startup_perf 事件。 _create_t0 = time.monotonic() _phase_timings_ms: dict[str, int] = {} if startup_progress is not None: startup_progress("Loading configuration...") + # 加载配置(CLI 层可能只给了一个路径);把命令行 --max-steps-per-turn + # 等循环控制参数覆盖回写进 config.loop_control。 _phase_t = time.monotonic() config = config if isinstance(config, Config) else load_config(config) _phase_timings_ms["config_ms"] = int((time.monotonic() - _phase_t) * 1000) @@ -203,11 +228,15 @@ async def create( config.loop_control.max_ralph_iterations = max_ralph_iterations logger.info("Loaded config: {config}", config=config) + # ==== 阶段 2:OAuth 认证与后台模型刷新 ==== _phase_t = time.monotonic() oauth = OAuthManager(config) bg_refresh_task = asyncio.create_task(_refresh_managed_models_silent(config)) + # ==== 阶段 3:解析模型与提供商 ==== + # 优先用命令行 --model;否则用 config.default_model;都没有则用空占位 + # (后续会提示用户 /login)。 model: LLMModel | None = None provider: LLMProvider | None = None @@ -225,11 +254,21 @@ async def create( model = LLMModel(provider="", model="", max_context_size=100_000) provider = LLMProvider(type="kimi", base_url="", api_key=SecretStr("")) + # ==== 阶段 4:环境变量覆盖 ==== + # 加载会话工作目录下的 .env(或 --env-file),再让 KIMI_BASE_URL / + # KIMI_API_KEY / KIMI_MODEL_NAME 等环境变量覆盖上面的配置。 # try overwrite with environment variables assert provider is not None assert model is not None - env_overrides = augment_provider_with_env_vars(provider, model) - + default_env_file = Path(str(session.work_dir)) / ".env" + selected_env_file = env_file or (default_env_file if default_env_file.is_file() else None) + llm_env: Mapping[str, str] = load_llm_env(selected_env_file) + if selected_env_file is not None: + logger.info("Loaded local LLM environment from: {file}", file=selected_env_file) + env_overrides = augment_provider_with_env_vars(provider, model, llm_env) + + # ==== 阶段 5:确定 thinking / yolo / plan mode ==== + # 命令行未显式指定时,回退到配置默认值。 # determine thinking mode thinking = config.default_thinking if thinking is None else thinking @@ -240,12 +279,16 @@ async def create( if not resumed: plan_mode = plan_mode if plan_mode else config.default_plan_mode + # ==== 阶段 6:创建 LLM ==== + # 按 provider 类型构造 kosong ChatProvider,包成 LLM(含能力集与上下文上限); + # 未配置 base_url/model 时返回 None(等待 /login)。 llm = create_llm( provider, model, thinking=thinking, session_id=session.id, oauth=oauth, + env=llm_env, ) if llm is not None: logger.info("Using LLM provider: {provider}", provider=provider) @@ -255,6 +298,10 @@ async def create( if startup_progress is not None: startup_progress("Scanning workspace...") + # ==== 阶段 7:构建 Runtime ==== + # Runtime 是本次运行的全部上下文:采集 KIMI_* 内置参数、发现 skills、 + # 恢复 additional dirs、装配 approval / notifications / background_tasks / + # subagent_store / approval_runtime / root_wire_hub。 runtime = await Runtime.create( config, oauth, @@ -272,6 +319,8 @@ async def create( _cleanup_stale_foreground_subagents(runtime) _phase_timings_ms["init_ms"] = int((time.monotonic() - _phase_t) * 1000) + # ==== 阶段 8:刷新插件配置 ==== + # 用最新凭据(如 OAuth token)刷新插件配置。 # Refresh plugin configs with fresh credentials (e.g. OAuth tokens) try: from kimi_cli.plugin.manager import ( @@ -286,6 +335,9 @@ async def create( except Exception: logger.debug("Failed to refresh plugin configs, skipping") + # ==== 阶段 9:加载 Agent ==== + # 解析 agent spec YAML → 渲染系统提示词 → 注册内置 subagent → 加载内置工具 + # → 追加 plugin 工具 → 加载 MCP 工具(可延迟到 shell 就绪,见 defer_mcp_loading)。 if agent_file is None: agent_file = DEFAULT_AGENT_FILE if startup_progress is not None: @@ -302,6 +354,8 @@ async def create( if startup_progress is not None: startup_progress("Restoring conversation...") + # ==== 阶段 10:恢复会话上下文 ==== + # 从 context.jsonl 恢复消息历史;历史里若已有系统提示词则沿用,否则写入新的。 context = Context(session.context_file) await context.restore() @@ -310,8 +364,10 @@ async def create( else: await context.write_system_prompt(agent.system_prompt) + # ==== 阶段 11:构造 KimiSoul(主循环) ==== soul = KimiSoul(agent, context=context) + # ==== 阶段 12:激活 plan mode(如请求) ==== # Activate plan mode if requested (for new sessions or --plan flag) if plan_mode and not soul.plan_mode: await soul.set_plan_mode_from_manual(True) @@ -319,6 +375,8 @@ async def create( # Already in plan mode from restored session, trigger activation reminder soul.schedule_plan_activation_reminder() + # ==== 阶段 13:Hook 引擎 ==== + # 从 config.hooks 构造引擎并注入 soul(拦截/放行用户输入、工具调用等事件)。 # Create and inject hook engine from kimi_cli.hooks.engine import HookEngine @@ -326,6 +384,9 @@ async def create( soul.set_hook_engine(hook_engine) runtime.hook_engine = hook_engine + # ==== 阶段 14:Telemetry ==== + # 可被 KIMI_DISABLE_TELEMETRY 或 config.telemetry 关闭;否则挂载事件 sink, + # 用 OAuth token 鉴权的异步传输器上报事件。 # --- Initialize telemetry --- from kimi_cli.telemetry import attach_sink, set_context from kimi_cli.telemetry import disable as disable_telemetry @@ -354,6 +415,8 @@ def _get_token() -> str | None: from kimi_cli.telemetry import track, track_session_started_once from kimi_cli.telemetry.crash import install_asyncio_handler, set_phase + # ==== 阶段 15:收尾 ==== + # 初始化完成,进入 runtime 阶段;hook asyncio 崩溃并上报 started / startup_perf。 # App init finished — enter runtime phase and hook asyncio crashes. install_asyncio_handler() set_phase("runtime") @@ -398,6 +461,7 @@ def session(self) -> Session: """Get the Session instance.""" return self._runtime.session + # 硬退出路径:取消后台模型刷新、关闭 MCP 连接,并按配置杀掉仍在跑的后台任务。 async def shutdown_background_tasks(self) -> None: """Kill active background tasks on exit, unless keep_alive_on_exit is configured. @@ -508,6 +572,7 @@ async def shutdown_background_tasks(self) -> None: except Exception: logger.warning("Error during background task shutdown; continuing exit", exc_info=True) + # 等待后台模型刷新任务被取消后退出(尽力而为,超时不阻塞)。 async def await_bg_tasks_shutdown(self, timeout: float = 2.0) -> None: """Await completion of the model-refresh background task after cancellation.""" task = self._bg_refresh_task @@ -517,6 +582,8 @@ async def await_bg_tasks_shutdown(self, timeout: float = 2.0) -> None: with contextlib.suppress(TimeoutError, asyncio.CancelledError, Exception): await asyncio.wait_for(asyncio.shield(task), timeout=timeout) + # 运行期环境上下文:把 kaos 的当前目录切到会话 work_dir,并在 OAuth token 的 + # 自动刷新(refreshing 上下文)下执行包体。 @contextlib.asynccontextmanager async def _env(self) -> AsyncGenerator[None]: original_cwd = KaosPath.cwd() @@ -553,6 +620,8 @@ async def run( MaxStepsReached: When the maximum number of steps is reached. RunCancelled: When the run is cancelled by the cancel event. """ + # 无 UI 的 run:产出 Wire 消息流。_ui_loop_fn 订阅 RootWireHub,把审批等 + # “轮外”消息桥接到当前 wire;soul 任务结束后关闭 wire 并等待 UI 循环退出。 async with self._env(): wire_future = asyncio.Future[WireUISide]() stop_ui_loop = asyncio.Event() @@ -694,6 +763,7 @@ async def _mirror_external_cancel() -> None: with contextlib.suppress(asyncio.CancelledError): await external_cancel_task + # 交互式 TUI 前端:构造欢迎信息(目录/会话/模型/更新提示)后启动 Shell。 async def run_shell( self, command: str | None = None, *, prefill_text: str | None = None ) -> bool: @@ -787,6 +857,7 @@ async def run_shell( shell = Shell(self._soul, welcome_info=welcome_info, prefill_text=prefill_text) return await shell.run(command) + # 非交互前端:输入/输出支持 text 或 stream-json,用于脚本化调用。 async def run_print( self, input_format: InputFormat, @@ -808,6 +879,7 @@ async def run_print( ) return await print_.run(command) + # ACP 服务器前端(IDE 集成,如 Zed / JetBrains)。 async def run_acp(self) -> None: """Run the Kimi Code CLI instance as ACP server.""" from kimi_cli.ui.acp import ACP @@ -816,6 +888,7 @@ async def run_acp(self) -> None: acp = ACP(self._soul) await acp.run() + # Wire 服务器前端:在 stdio 上承载 Wire 消息流(实验性)。 async def run_wire_stdio(self) -> None: """Run the Kimi Code CLI instance as Wire server over stdio.""" from kimi_cli.wire.server import WireServer diff --git a/src/kimi_cli/cli/__init__.py b/src/kimi_cli/cli/__init__.py index b9aafb7a87..e18884ff08 100644 --- a/src/kimi_cli/cli/__init__.py +++ b/src/kimi_cli/cli/__init__.py @@ -1,3 +1,15 @@ +"""Kimi CLI 的 Typer 命令行定义与主命令回调。 + +本模块是 ``kimi`` / ``kimi-cli`` 命令的“大脑”: +- 定义主命令 ``kimi()`` 的完整参数面(--session/--model/--yolo/--print 等); +- 负责解析这些参数、做互斥校验,解析出 work_dir / config / MCP 配置; +- 通过 ``_run()`` 定位会话、构造 ``KimiCLI``,再按 UI 模式分发到 + shell / print / acp / wire; +- 通过异常(Reload / SwitchToWeb / SwitchToVis)实现运行中的模式切换; +- 注册 login / logout / term / acp 等子命令(info/export/mcp/plugin/vis/web + 由 ``LazySubcommandGroup`` 懒加载,见 _lazy_group.py)。 +""" + from __future__ import annotations from pathlib import Path @@ -10,6 +22,13 @@ from ._lazy_group import LazySubcommandGroup +# ========================================================================== +# 运行中的“模式切换”采用异常控制流实现: +# 当用户在交互界面里输入 /reload、/model、/new,或请求切换到 web / vis 界面时, +# UI 层会抛出下面这些异常;_run() 的 match ui 分支与 _reload_loop 捕获后, +# 用新参数重建 KimiCLI 实例,从而在“进程不退出”的前提下完成切换。 +# ========================================================================== + class Reload(Exception): """Reload configuration.""" @@ -37,6 +56,8 @@ def __init__(self, session_id: str | None = None): self.session_id = session_id +# 主 Typer 应用。使用 LazySubcommandGroup:info/export/mcp/plugin/vis/web 这几个 +# 重量级子命令只在真正被调用时才 import(见 _lazy_group.py),加快 --help 与启动速度。 cli = typer.Typer( cls=LazySubcommandGroup, epilog="""\b\ @@ -47,6 +68,8 @@ def __init__(self, session_id: str | None = None): help="Kimi, your next CLI agent.", ) +# UI 模式:shell(交互式 TUI,默认)、print(非交互)、acp(IDE 协议服务器)、 +# wire(stdio 事件流,实验性)。决定 _run() 最后分发到哪个前端。 UIMode = Literal["shell", "print", "acp", "wire"] @@ -66,6 +89,7 @@ def _strip_session_id_suffix(title: str, session_id: str) -> str: return title.rsplit(suffix, 1)[0] if title.endswith(suffix) else title +# --version/-V 的回调。is_eager=True 表示参数一出现就执行、不再继续解析其它参数。 def _version_callback(value: bool) -> None: if value: from kimi_cli.constant import get_version @@ -74,6 +98,8 @@ def _version_callback(value: bool) -> None: raise typer.Exit() +# 主命令回调。invoke_without_command=True 意味着即使不带子命令也会执行它; +# 当带子命令时(如 `kimi mcp list`),函数开头就 return,把控制权让给子命令。 @cli.callback(invoke_without_command=True) def kimi( ctx: typer.Context, @@ -170,6 +196,17 @@ def kimi( help="Config TOML/JSON file to load. Default: ~/.kimi/config.toml.", ), ] = None, + env_file: Annotated[ + Path | None, + typer.Option( + "--env-file", + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + help="Dotenv file for local LLM settings. Default: /.env if present.", + ), + ] = None, model_name: Annotated[ str | None, typer.Option( @@ -364,6 +401,7 @@ def kimi( ] = None, ): """Kimi, your next CLI agent.""" + # ---- 从这一行往下是回调主体:校验参数 → 解析配置 → 交给 _reload_loop ---- import asyncio import contextlib import json @@ -387,6 +425,7 @@ def kimi( from kimi_cli.metadata import load_metadata, save_metadata from kimi_cli.session import Session from kimi_cli.ui.shell.startup import ShellStartupProgress + from kimi_cli.utils.dotenv import load_dotenv_values from kimi_cli.utils.logging import logger, open_original_stderr, redirect_stderr_to_logger from .mcp import get_global_mcp_config_file @@ -397,6 +436,8 @@ def kimi( # MCP server stderr noise is captured into logs from the start. enable_logging(debug, redirect_stderr=False) + # 把致命错误写到“原始 stderr”(即使 fd=2 已被重定向到日志文件), + # 保证用户在终端上一定能看到错误信息。 def _emit_fatal_error(message: str) -> None: # Prefer writing to the original stderr fd even if we later redirect fd=2. # This ensures fatal errors are visible to the user. @@ -417,6 +458,8 @@ def _emit_fatal_error(message: str) -> None: if session_id is None: _picker_mode = True + # --quiet 是 `--print --output-format text --final-message-only` 的快捷写法; + # 与 ACP/Wire UI 冲突时直接报错。 if quiet: if acp_mode or wire_mode: raise typer.BadParameter( @@ -432,6 +475,7 @@ def _emit_fatal_error(message: str) -> None: output_format = "text" final_message_only = True + # 互斥参数校验:下面每组内最多只能同时启用一个。 conflict_option_sets = [ { "--print": print_mode, @@ -459,6 +503,7 @@ def _emit_fatal_error(message: str) -> None: param_hint=active_options[0], ) + # --agent 是内置 spec 的快捷名,翻译成对应的 agent 文件路径。 if agent is not None: match agent: case "default": @@ -466,6 +511,7 @@ def _emit_fatal_error(message: str) -> None: case "okabe": agent_file = OKABE_AGENT_FILE + # 由命令行标志决定本次运行的 UI 模式(shell 为默认)。 ui: UIMode = "shell" if print_mode: ui = "print" @@ -500,6 +546,11 @@ def _emit_fatal_error(message: str) -> None: param_hint="--session", ) + # 解析工作目录:--work-dir 指定,否则取当前目录(KaosPath 是 kaos 库的路径抽象)。 + work_dir = KaosPath.unsafe_from_local_path(local_work_dir) if local_work_dir else KaosPath.cwd() + + # 解析配置:优先级 --config(内联字符串) > --config-file(路径) > + # 项目 .env 里的 KIMI_CONFIG_FILE > 默认 ~/.kimi/config.toml。 config: Config | Path | None = None if config_string is not None: config_string = config_string.strip() @@ -511,7 +562,22 @@ def _emit_fatal_error(message: str) -> None: raise typer.BadParameter(str(e), param_hint="--config") from e elif config_file is not None: config = config_file + else: + project_env_file = Path(str(work_dir)) / ".env" + project_env = load_dotenv_values(project_env_file if project_env_file.is_file() else None) + if project_config := project_env.get("KIMI_CONFIG_FILE"): + config_path = Path(project_config).expanduser() + if not config_path.is_absolute(): + config_path = Path(str(work_dir)) / config_path + config_path = config_path.resolve(strict=False) + if not config_path.is_file(): + raise typer.BadParameter( + f"Project config file not found: {config_path}", param_hint="KIMI_CONFIG_FILE" + ) + config = config_path + # 汇总 MCP 配置:来自 --mcp-config-file(文件)与 --mcp-config(内联 JSON); + # 未显式给出时回退到全局默认 MCP 配置文件。 file_configs = list(mcp_config_file or []) raw_mcp_config = list(mcp_config or []) @@ -535,8 +601,6 @@ def _emit_fatal_error(message: str) -> None: if local_skills_dir: skills_dirs = [KaosPath.unsafe_from_local_path(p) for p in local_skills_dir] - work_dir = KaosPath.unsafe_from_local_path(local_work_dir) if local_work_dir else KaosPath.cwd() - # Tracks the most recently created/loaded session so that _reload_loop's # exception handler can clean it up even when _run() fails before returning. _latest_created_session: Session | None = None @@ -548,6 +612,7 @@ async def _run(session_id: str | None, prefill_text: str | None = None) -> tuple Returns: The session and the exit code (0 = success, 1 = failure, 75 = retryable). """ + # 仅 shell UI 需要启动进度条;print/acp/wire 不需要。 startup_progress = ShellStartupProgress(enabled=ui == "shell") try: startup_progress.update("Preparing session...") @@ -555,6 +620,8 @@ async def _run(session_id: str | None, prefill_text: str | None = None) -> tuple # Track if we're resuming an existing session (vs creating new) resumed = False + # 定位会话:--session 恢复指定会话 / --continue 恢复上次会话 / + # 否则创建新会话。resumed 标志区分“恢复”与“全新启动”。 if session_id is not None: session = await Session.find(work_dir, session_id) if session is None: @@ -616,9 +683,12 @@ async def _run(session_id: str | None, prefill_text: str | None = None) -> tuple # the saved original stderr fd. redirect_stderr_to_logger() + # 核心装配:加载配置 → 建 LLM → 构建 Runtime → 加载 Agent → 恢复 Context + # → 构造 KimiSoul → 挂 hooks/telemetry。所有模块在这里接线(见 app.py)。 instance = await KimiCLI.create( session, config=config, + env_file=env_file, model_name=model_name, thinking=thinking, yolo=yolo, @@ -654,6 +724,8 @@ async def _run(session_id: str | None, prefill_text: str | None = None) -> tuple # stderr noise is captured into logs without hiding startup failures. redirect_stderr_to_logger() preserve_background_tasks = False + # 按 UI 模式分发到对应前端;前端可能抛 Reload / SwitchToWeb / + # SwitchToVis 异常来请求切换运行方式。 try: match ui: case "shell": @@ -808,6 +880,7 @@ async def _reload_loop(session_id: str | None) -> tuple[str | None, int]: await _delete_empty_session(_latest_created_session) raise + # --session 不带值 = 进入“会话选择器”模式,交互式挑选一个历史会话恢复。 if _picker_mode: from prompt_toolkit.shortcuts.choice_input import ChoiceInput from rich.console import Console @@ -845,6 +918,7 @@ async def _pick_session() -> str: session_id = asyncio.run(_pick_session()) + # 主循环:_reload_loop 会反复调用 _run(),直到进程真正结束,或切换到 web/vis。 try: switch_target, exit_code = asyncio.run(_reload_loop(session_id)) except (typer.BadParameter, typer.Exit): @@ -901,6 +975,10 @@ async def _pick_session() -> str: raise typer.Exit(code=exit_code) +# -------------------------------------------------------------------------- +# 子命令:登录 / 登出(OAuth 设备码流程) +# -------------------------------------------------------------------------- + @cli.command() def login( json: bool = typer.Option( @@ -1004,6 +1082,7 @@ async def _run() -> bool: raise typer.Exit(code=1) +# 启动 Toad TUI(一个基于 Kimi CLI ACP server 的交互式终端界面)。 @cli.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) def term( ctx: typer.Context, @@ -1014,6 +1093,7 @@ def term( run_term(ctx) +# 以 ACP server 模式运行,供 IDE(如 Zed / JetBrains)集成;等价于主命令 --acp。 @cli.command() def acp(): """Run Kimi Code CLI ACP server.""" @@ -1022,6 +1102,7 @@ def acp(): acp_main() +# 内部隐藏命令:后台任务 worker 子进程的入口(由 BackgroundTaskManager spawn)。 @cli.command(name="__background-task-worker", hidden=True) def background_task_worker( task_dir: Annotated[Path, typer.Option("--task-dir")], @@ -1050,6 +1131,7 @@ def background_task_worker( ) +# 内部隐藏命令:web worker 子进程的入口(由 web 后端为每个会话 spawn)。 @cli.command(name="__web-worker", hidden=True) def web_worker(session_id: str) -> None: """Run web worker subprocess (internal).""" @@ -1072,6 +1154,7 @@ def web_worker(session_id: str) -> None: asyncio.run(run_worker(parsed_session_id)) +# 便于 `python -m kimi_cli.cli` 直接运行调试。 if __name__ == "__main__": import sys diff --git a/src/kimi_cli/llm.py b/src/kimi_cli/llm.py index 4b65fa227c..e31606cf9f 100644 --- a/src/kimi_cli/llm.py +++ b/src/kimi_cli/llm.py @@ -273,29 +273,32 @@ def model_display_name(model_name: str | None, model: LLMModel | None = None) -> return model_name -def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel) -> dict[str, str]: +def augment_provider_with_env_vars( + provider: LLMProvider, model: LLMModel, env: Mapping[str, str] | None = None +) -> dict[str, str]: """Override provider/model settings from environment variables. Returns: Mapping of environment variables that were applied. """ applied: dict[str, str] = {} + env = os.environ if env is None else env match provider.type: case "kimi": - if base_url := os.getenv("KIMI_BASE_URL"): + if base_url := env.get("KIMI_BASE_URL"): provider.base_url = base_url applied["KIMI_BASE_URL"] = base_url - if api_key := os.getenv("KIMI_API_KEY"): + if api_key := env.get("KIMI_API_KEY"): provider.api_key = SecretStr(api_key) applied["KIMI_API_KEY"] = "******" - if model_name := os.getenv("KIMI_MODEL_NAME"): + if model_name := env.get("KIMI_MODEL_NAME"): model.model = model_name applied["KIMI_MODEL_NAME"] = model_name - if max_context_size := os.getenv("KIMI_MODEL_MAX_CONTEXT_SIZE"): + if max_context_size := env.get("KIMI_MODEL_MAX_CONTEXT_SIZE"): model.max_context_size = int(max_context_size) applied["KIMI_MODEL_MAX_CONTEXT_SIZE"] = max_context_size - if capabilities := os.getenv("KIMI_MODEL_CAPABILITIES"): + if capabilities := env.get("KIMI_MODEL_CAPABILITIES"): caps_lower = (cap.strip().lower() for cap in capabilities.split(",") if cap.strip()) model.capabilities = set( cast(ModelCapability, cap) @@ -304,9 +307,9 @@ def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel) -> di ) applied["KIMI_MODEL_CAPABILITIES"] = capabilities case "openai_legacy" | "openai_responses": - if base_url := os.getenv("OPENAI_BASE_URL"): + if base_url := env.get("OPENAI_BASE_URL"): provider.base_url = base_url - if api_key := os.getenv("OPENAI_API_KEY"): + if api_key := env.get("OPENAI_API_KEY"): provider.api_key = SecretStr(api_key) case _: pass @@ -330,7 +333,9 @@ def create_llm( thinking: bool | None = None, session_id: str | None = None, oauth: OAuthManager | None = None, + env: Mapping[str, str] | None = None, ) -> LLM | None: + env = os.environ if env is None else env if provider.type not in {"_echo", "_scripted_echo"} and ( not provider.base_url or not model.model ): @@ -360,15 +365,15 @@ def create_llm( gen_kwargs: Kimi.GenerationKwargs = {} if session_id: gen_kwargs["prompt_cache_key"] = session_id - if temperature := os.getenv("KIMI_MODEL_TEMPERATURE"): + if temperature := env.get("KIMI_MODEL_TEMPERATURE"): gen_kwargs["temperature"] = float(temperature) - if top_p := os.getenv("KIMI_MODEL_TOP_P"): + if top_p := env.get("KIMI_MODEL_TOP_P"): gen_kwargs["top_p"] = float(top_p) for env_name in ( "KIMI_MODEL_MAX_COMPLETION_TOKENS", "KIMI_MODEL_MAX_TOKENS", ): - raw_max_completion_tokens = os.getenv(env_name) + raw_max_completion_tokens = env.get(env_name) if not raw_max_completion_tokens: continue try: @@ -488,7 +493,7 @@ def create_llm( from kosong.chat_provider.kimi import Kimi if isinstance(chat_provider, Kimi) and ( - thinking_keep := os.getenv("KIMI_MODEL_THINKING_KEEP") + thinking_keep := env.get("KIMI_MODEL_THINKING_KEEP") ): chat_provider = chat_provider.with_extra_body({"thinking": {"keep": thinking_keep}}) diff --git a/src/kimi_cli/utils/dotenv.py b/src/kimi_cli/utils/dotenv.py new file mode 100644 index 0000000000..64c9539d25 --- /dev/null +++ b/src/kimi_cli/utils/dotenv.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import os +from collections.abc import Mapping +from pathlib import Path + +from dotenv import dotenv_values + + +def load_dotenv_values(env_file: Path | None) -> dict[str, str]: + """Read dotenv values without modifying the process environment.""" + if env_file is None: + return {} + return {key: value for key, value in dotenv_values(env_file).items() if value is not None} + + +def load_llm_env(env_file: Path | None) -> Mapping[str, str]: + """Return process environment with values from a local dotenv file overlaid. + + This deliberately does not call ``load_dotenv``: loading a project file must + not mutate the CLI process environment or leak its values to other code. + """ + env = dict(os.environ) + env.update(load_dotenv_values(env_file)) + return env diff --git a/tests/core/test_create_llm.py b/tests/core/test_create_llm.py index dbbf0d09e0..0ef222c9a0 100644 --- a/tests/core/test_create_llm.py +++ b/tests/core/test_create_llm.py @@ -21,12 +21,7 @@ def test_augment_provider_with_env_vars_kimi(monkeypatch): base_url="https://original.test/v1", api_key=SecretStr("orig-key"), ) - model = LLMModel( - provider="kimi", - model="kimi-base", - max_context_size=4096, - capabilities=None, - ) + model = LLMModel(provider="kimi", model="kimi-base", max_context_size=4096) monkeypatch.setenv("KIMI_BASE_URL", "https://env.test/v1") monkeypatch.setenv("KIMI_API_KEY", "env-key") @@ -53,6 +48,23 @@ def test_augment_provider_with_env_vars_kimi(monkeypatch): ) +def test_augment_provider_with_env_vars_uses_explicit_env_without_process_mutation(monkeypatch): + provider = LLMProvider( + type="kimi", base_url="https://original.test/v1", api_key=SecretStr("orig-key") + ) + model = LLMModel(provider="kimi", model="kimi-base", max_context_size=4096) + monkeypatch.setenv("KIMI_API_KEY", "process-key") + + augment_provider_with_env_vars( + provider, + model, + {"KIMI_API_KEY": "local-key", "KIMI_MODEL_NAME": "local-model"}, + ) + + assert provider.api_key.get_secret_value() == "local-key" + assert model.model == "local-model" + + def test_create_llm_kimi_model_parameters(monkeypatch): provider = LLMProvider( type="kimi", diff --git a/tests/utils/test_dotenv.py b/tests/utils/test_dotenv.py new file mode 100644 index 0000000000..ad45ef1910 --- /dev/null +++ b/tests/utils/test_dotenv.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import os + +from kimi_cli.utils.dotenv import load_dotenv_values, load_llm_env + + +def test_load_dotenv_values_does_not_mutate_process(tmp_path, monkeypatch): + env_file = tmp_path / ".env" + env_file.write_text("KIMI_CONFIG_FILE=dev/deepseek.toml\n") + monkeypatch.delenv("KIMI_CONFIG_FILE", raising=False) + + assert load_dotenv_values(env_file) == {"KIMI_CONFIG_FILE": "dev/deepseek.toml"} + assert "KIMI_CONFIG_FILE" not in os.environ + + +def test_load_llm_env_overlays_dotenv_without_mutating_process(tmp_path, monkeypatch): + env_file = tmp_path / ".env" + env_file.write_text("KIMI_API_KEY=local-key\nKIMI_MODEL_NAME=local-model\n") + monkeypatch.setenv("KIMI_API_KEY", "process-key") + + env = load_llm_env(env_file) + + assert env["KIMI_API_KEY"] == "local-key" + assert env["KIMI_MODEL_NAME"] == "local-model" + assert os.environ["KIMI_API_KEY"] == "process-key" diff --git a/uv.lock b/uv.lock index 48141ef8da..7118519b9c 100644 --- a/uv.lock +++ b/uv.lock @@ -1303,6 +1303,7 @@ dependencies = [ { name = "pydantic" }, { name = "pykaos" }, { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "python-dotenv" }, { name = "pyyaml" }, { name = "rich" }, { name = "ripgrepy" }, @@ -1347,6 +1348,7 @@ requires-dist = [ { name = "pydantic", specifier = "==2.12.5" }, { name = "pykaos", editable = "packages/kaos" }, { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'", specifier = ">=12.1" }, + { name = "python-dotenv", specifier = "==1.2.1" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "rich", specifier = "==14.2.0" }, { name = "ripgrepy", specifier = "==2.2.0" }, From f04777d7dd5dca654427cfcba1758dd5259b7e22 Mon Sep 17 00:00:00 2001 From: seal Date: Tue, 18 Aug 2026 12:18:37 +0800 Subject: [PATCH 2/2] dev_notes update --- ...00\345\217\221\350\256\241\345\210\222.md" | 323 ++++++++++++++++++ ...00\345\217\221\350\256\241\345\210\222.md" | 147 ++++++++ ...04\346\265\213\346\226\271\346\241\210.md" | 287 ++++++++++++++-- ...15\347\275\256\346\224\271\351\200\240.md" | 217 ++++++++++++ ...on\344\270\260\345\257\214\347\211\210.md" | 281 +++++++++++++++ src/kimi_cli/app.py | 6 +- src/kimi_cli/auth/oauth.py | 73 ++++ src/kimi_cli/soul/__init__.py | 18 + src/kimi_cli/soul/agent.py | 39 +++ src/kimi_cli/soul/approval.py | 26 ++ src/kimi_cli/soul/btw.py | 10 + src/kimi_cli/soul/compaction.py | 13 + src/kimi_cli/soul/context.py | 15 + src/kimi_cli/soul/denwarenji.py | 10 + src/kimi_cli/soul/dynamic_injection.py | 10 + .../soul/dynamic_injections/afk_mode.py | 9 + .../soul/dynamic_injections/plan_mode.py | 9 + src/kimi_cli/soul/kimisoul.py | 93 +++++ src/kimi_cli/soul/message.py | 9 + src/kimi_cli/soul/slash.py | 13 + src/kimi_cli/soul/toolset.py | 52 +++ 21 files changed, 1630 insertions(+), 30 deletions(-) create mode 100644 "docs/dev_note/G1\351\241\271\347\233\256\347\272\247\347\237\245\350\257\206\345\271\263\351\235\242\345\274\200\345\217\221\350\256\241\345\210\222.md" create mode 100644 "docs/dev_note/\351\241\271\347\233\256\347\237\245\350\257\206\344\270\216Agent-Teams\345\274\200\345\217\221\350\256\241\345\210\222.md" create mode 100644 "docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-CLI-\351\241\271\347\233\256\347\272\247\351\205\215\347\275\256\346\224\271\351\200\240.md" create mode 100644 "docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI-Action\344\270\260\345\257\214\347\211\210.md" diff --git "a/docs/dev_note/G1\351\241\271\347\233\256\347\272\247\347\237\245\350\257\206\345\271\263\351\235\242\345\274\200\345\217\221\350\256\241\345\210\222.md" "b/docs/dev_note/G1\351\241\271\347\233\256\347\272\247\347\237\245\350\257\206\345\271\263\351\235\242\345\274\200\345\217\221\350\256\241\345\210\222.md" new file mode 100644 index 0000000000..53801227b0 --- /dev/null +++ "b/docs/dev_note/G1\351\241\271\347\233\256\347\272\247\347\237\245\350\257\206\345\271\263\351\235\242\345\274\200\345\217\221\350\256\241\345\210\222.md" @@ -0,0 +1,323 @@ +# G1:项目级知识平面开发计划 + +> 状态:设计完成,尚未开始编码 +> 范围:Repo Intelligence、Persistent Memory、Knowledge Orchestrator 及请求级 Context Pack +> 原则:先完成可追溯、可失效的最小闭环,再考虑向量检索和多语言。 + +## 1. 目标与验收口径 + +G1 最终需要形成以下闭环: + +```text +用户任务 + → 检索当前代码事实与历史工程记忆 + → 校验版本、来源和时效状态 + → 生成受 Token 预算约束的临时 Context Pack + → Agent 执行并验证 + → 产生候选记忆,经确认后跨会话复用 +``` + +完成 G1 必须同时满足: + +- Python 代码索引支持增量刷新和符号级查询。 +- 新 Session 能召回同一项目的历史决策、约束、偏好和经验。 +- 代码变化后,相关记忆能被正确保留、降级或判废。 +- Context Pack 只存在于当前模型请求,不进入 `context.jsonl`。 +- Root 与 Subagent 共用知识服务,但保持各自对话上下文。 +- 关闭知识功能时,行为可作为 baseline,现有 Agent 流程不受影响。 + +## 2. 源码调研结论 + +| 现状 | 设计影响 | +| --- | --- | +| `KimiCLI.create()` 先创建 Runtime,再恢复 Context 和构造 KimiSoul | 知识服务适合在 Runtime 生命周期中装配 | +| `Runtime.copy_for_subagent()` 共享 Session、审批和 Root Wire Hub | 新增的 `KnowledgeService` 应以同一实例传给子 Agent | +| `Session`、`Context`、`SubagentStore` 都是 Session 级存储 | 项目知识必须放到独立的项目级目录,不能放进某个 Session | +| 当前动态注入处理会把 Provider 结果写入 `Context.append_message()` | Repo 快照不能直接复用该持久化注入路径 | +| `_step()` 会先构造 `effective_history`,再计算请求 Token 并调用模型 | 应在这里把 Context Pack 合并到历史副本中,不修改 Context | +| `collect_git_context()` 只提供分支、脏文件和近期提交 | 需要新增结构化 Git 快照、文件 hash、blob 和符号事实 | +| Hook 是面向用户配置的外部扩展机制,没有内部 TurnEnd 数据契约 | 记忆写入应使用内部生命周期接口,不把外部 Hook 当内部消息总线 | +| Tree-sitter 当前只是 Python 3.14 依赖链中的间接依赖 | 实现时必须显式声明 `tree-sitter` 和 `tree-sitter-python` | +| 当前运行环境的 SQLite 支持 FTS5 | 仍需在各平台及 PyInstaller 产物中增加 FTS5 冒烟验证 | + +## 3. 核心设计决策 + +### 3.1 项目标识与存储 + +- 优先通过 `git rev-parse --show-toplevel` 获取项目根目录;非 Git 目录退回当前工作目录。 +- `project_id = sha256(kaos_name + "\0" + canonical_project_root)`,保证: + - 同一仓库不同子目录启动时共享知识; + - 不同 KAOS、clone 或 worktree 相互隔离; + - remote URL 不作为主键,避免凭据泄露和同源工作区污染。 +- 项目级数据存放于: + +```text +~/.kimi/knowledge// +├── project.json +├── repo.sqlite3 # 可重建的当前代码事实 +└── memory.sqlite3 # 不可随意删除的长期工程记忆 +``` + +Repo 与 Memory 分库,原因是二者的恢复策略不同:Repo 数据损坏后可重建;Memory 数据损坏时必须保留原文件、停止写入并提示修复,不能自动清空。 + +### 3.2 异步与一致性 + +- 使用标准库 `sqlite3`,通过 `asyncio.to_thread()` 执行阻塞事务,不额外引入数据库运行时。 +- 开启 WAL、foreign keys 和 busy timeout;写操作使用进程内锁与短事务。 +- 文件读取和 AST 解析在事务外完成,单个文件的旧事实删除与新事实写入在同一事务完成。 +- 索引刷新期间允许读取上一个已完成快照;若快照与当前工作区不匹配,则不注入旧 Repo 事实。 +- 所有知识异常均 fail-open:记录降级原因,继续运行原有 Agent。 + +### 3.3 记忆可信度 + +- `Git diff` 只能证明代码发生变化,不能单独证明一条结论正确。 +- `preference`、`decision` 优先由用户明确表达或确认后升级。 +- `constraint`、`lesson` 需要测试证据、用户确认或其他明确验证结果。 +- `conflicted` 只在存在确定性证据时产生,例如新记忆显式取代旧记忆、结构化约束值相反或用户确认冲突;不让 LLM 仅凭自然语言猜测冲突。 + +## 4. 模块边界 + +建议新增以下模块,首期避免拆分过细: + +```text +src/kimi_cli/knowledge/ +├── models.py # 公共模型、枚举和查询结果 +├── project.py # project root、project_id 和存储路径 +├── database.py # SQLite 连接、迁移和事务封装 +├── python_parser.py # Tree-sitter Python 结构提取 +├── repo_index.py # 清单、增量刷新和代码事实查询 +├── memory_store.py # 候选/长期记忆、FTS5 和状态管理 +├── memory_validator.py # 代码引用与当前 Repo 快照交叉校验 +├── memory_writer.py # TurnEvidence → 候选记忆 +├── context_pack.py # 排序、裁剪、格式化和临时历史合并 +├── orchestrator.py # 查询规划、并行召回和冲突处理 +└── service.py # 生命周期、后台刷新和统一入口 +``` + +`KnowledgeService` 由 Runtime 持有,聚合 `RepoIndex`、`MemoryStore` 和 `KnowledgeOrchestrator`;KimiSoul 只依赖统一入口,不直接操作数据库。 + +## 5. 数据模型 + +### 5.1 Repo 数据 + +| 表 | 关键字段 | 用途 | +| --- | --- | --- | +| `schema_migrations` | version、applied_at | Schema 迁移 | +| `index_runs` | revision、workspace_state、status、duration | 记录完整刷新结果 | +| `files` | path、language、content_hash、blob_hash、parse_status | 当前文件事实 | +| `symbols` | qualified_name、kind、signature、range、fingerprint | 类、函数、方法等定义 | +| `imports` | module、name、alias、level、target_file | 模块关系 | +| `references` | text、kind、location、target_symbol、confidence | 引用和候选调用关系 | + +约束:只存结构、签名、范围、hash 和短摘要,不复制整份源码。所有路径使用相对项目根目录的 POSIX 形式。 + +### 5.2 Memory 数据 + +```text +MemoryRecord +├── id / project_id / kind +├── content / confidence +├── source_session_id / source_agent_id +├── validation_kind / validation_detail +├── status / supersedes_id +└── created_at / updated_at / last_checked_at + +CodeReference +├── path / symbol / symbol_kind +├── git_commit / blob_hash / content_hash +├── symbol_fingerprint / source_range +└── snapshot_id +``` + +记忆分为 `decision`、`constraint`、`preference`、`lesson`。候选记忆单独保存 `pending/accepted/rejected` 状态,只有 accepted 记录进入正常召回。 + +### 5.3 失效规则 + +| 当前证据 | 结果 | +| --- | --- | +| 文件及引用符号 fingerprint 未变化 | `valid` | +| 文件变化,符号仍存在但 fingerprint 变化 | `possibly_stale` | +| 文件或符号已删除 | `stale` | +| 存在明确的替代、相反约束或用户确认 | `conflicted` | +| RepoIndex 尚未完成或验证失败 | 保持原状态,但标记本次 `unverified` | + +非代码偏好不因分支切换自动失效,只能被用户撤销或由新记录取代。 + +## 6. Repo Intelligence 设计 + +### 6.1 文件清单 + +- Git 仓库使用 `git ls-files` 获取 tracked 与未忽略的 untracked 文件;非 Git 仓库使用带忽略规则的目录遍历。 +- 首期只处理 `.py`,跳过敏感文件、符号链接、二进制文件和超大文件。 +- 同时采集 HEAD、分支、工作区状态及 HEAD blob;以当前文件内容的 SHA-256 作为增量刷新主依据。 + +### 6.2 Python 解析 + +- 提取 module、class、function、async function、method、signature、docstring 首行和源码范围。 +- 通过父节点栈生成稳定的 `qualified_name`。 +- import 关系尽量解析到项目内文件。 +- name、attribute、call 先保存为候选引用;只有可确定的本地定义或 import alias 才绑定 `target_symbol`。 +- 动态分发、反射和 monkey patch 不包装成确定调用图,查询结果必须带 confidence。 + +### 6.3 最小查询接口 + +```python +await repo_index.search_symbol(query, *, kinds=None, limit=20) +await repo_index.find_references(symbol_id, *, limit=50) +await repo_index.module_summary(path) +await repo_index.related_files(path_or_symbol, *, limit=20) +await repo_index.refresh_if_needed() +``` + +首期由 Orchestrator 调用这些接口;是否额外暴露成 Agent 工具,在基线评测后决定。 + +## 7. Persistent Memory 设计 + +### 7.1 写入链路 + +```text +Turn 完成 + → 生成最小 TurnEvidence + → 过滤敏感路径、凭据模式和大段工具输出 + → 结构化提取候选记忆 + → 去重并保存为 pending + → 用户确认或验证门槛通过 + → accepted,进入长期召回 +``` + +`TurnEvidence` 只包含用户任务、最终答复、变更路径、测试命令及退出状态、使用过的知识 ID 和 Git 前后快照,不保存思维链,也不复制完整 Shell 输出。 + +自动提取放在知识闭环稳定之后实现;初期先支持测试夹具和显式 API 写入,避免同时调试索引、召回和 LLM 提取质量。 + +### 7.2 召回与排序 + +- FTS5 索引正文、标签、路径和符号名称。 +- 基础分数由 BM25、路径/符号命中和记忆类型组成。 +- 最终分数综合相关性、confidence 和状态权重。 +- `stale` 默认不注入;`possibly_stale`、`conflicted` 必须携带醒目标记和当前证据。 +- 相同规范化内容或 supersedes 链只保留最新有效记录。 + +## 8. Knowledge Orchestrator 与 Context Pack + +### 8.1 查询流程 + +1. 使用规则判断代码定位、历史决策或混合任务,不增加额外 LLM 调用。 +2. 并行执行 Repo 与 Memory 查询。 +3. 校验当前 Repo 快照和记忆引用。 +4. 按来源键去重,标记过期、冲突和无法验证的结果。 +5. 默认按 50% 代码事实、30% 决策/约束、20% 经验分配预算,空余预算可流转。 +6. 使用现有 `estimate_message_tokens()` 裁剪,输出带来源的 Context Pack。 + +### 8.2 请求级临时注入 + +- 每个 `_turn()` 根据当前用户消息构建一次 Pack;同一 Turn 的后续 Step 复用。 +- steer 追加新任务时使 Pack 失效,并在下一 Step 重建。 +- 在 `_step()` 中复制 `normalize_history()` 的结果,把 Pack 合并到本轮最新 user 消息的副本。 +- `Context.history`、`Context.token_count` 和 `context.jsonl` 均不写入 Pack。 +- `_compute_completion_overrides()` 使用合并后的 `effective_history`,确保 Pack 被计入请求 Token。 +- 当前 DynamicInjection 保持原语义,知识注入走独立的 request-context 路径。 + +## 9. 生命周期与接入点 + +| 位置 | 计划改动 | +| --- | --- | +| `config.py` | 增加 `KnowledgeConfig` 和 baseline/candidate 开关 | +| `Runtime.create()` | 解析项目身份、打开数据库、启动非阻塞索引刷新 | +| `Runtime.copy_for_subagent()` | 共享同一 `KnowledgeService` 实例 | +| `KimiSoul._turn()` | 建立/清理 Turn 级 Pack,并提交 TurnEvidence | +| `KimiSoul._step()` | 将 Pack 临时并入 `effective_history` | +| `KimiCLI` shutdown | 取消索引任务并限时刷新候选写入 | +| `wire/types.py` | 增加查询开始/结束事件,不记录正文和用户查询 | +| Slash commands | 增加 status、reindex、候选确认、forget 等管理入口 | + +建议初始配置:`enabled=false`、`auto_extract=false`、`context_token_budget=4000`、查询超时 1.5 秒。完成评测后再决定默认开启策略。 + +## 10. 分阶段开发计划 + +### P0:契约与基线 + +- [ ] 固化公共模型、项目身份、失效状态和 Context Pack 格式。 +- [ ] 建立小型 Python 仓库夹具、记忆 Golden Set 和 baseline 指标采集。 +- [ ] 写出“Pack 不落盘”“错误必须降级”的契约测试骨架。 + +完成标准:后续每阶段都有独立输入、输出和可重复验收方式。 + +### P1:项目基础设施 + +- [ ] 实现 ProjectIdentity、双 SQLite 数据库、迁移和 KnowledgeConfig。 +- [ ] 实现 WAL、并发锁、损坏检测和 Repo/Memory 不同恢复策略。 +- [ ] 将可选 KnowledgeService 装配进 Runtime,并正确共享给 Subagent。 + +完成标准:启用或关闭知识层均不改变现有 Turn 行为,两个 Session 可访问同一项目存储。 + +### P2:Repo Intelligence 纵向闭环 + +- [ ] 显式加入 Tree-sitter 依赖和 PyInstaller 打包验证。 +- [ ] 完成文件清单、Python 解析、事务化增量刷新和删除清理。 +- [ ] 完成四个最小查询接口及来源、confidence 返回值。 + +完成标准:首次索引、单文件修改、删除、重命名、脏工作区和分支切换均通过测试。 + +### P3:Persistent Memory 纵向闭环 + +- [ ] 完成候选/长期记忆 Schema、FTS5、去重和管理 API。 +- [ ] 完成 CodeReference 校验与四级状态转换。 +- [ ] 先使用人工夹具和显式确认完成跨 Session 召回。 + +完成标准:记忆可创建、确认、召回、撤销和判废,Memory 数据不会随 Repo 重建丢失。 + +### P4:Orchestrator 与临时注入 + +- [ ] 完成规则查询规划、并行召回、排序、裁剪和 Pack 格式化。 +- [ ] 完成 request-context 合并,不复用持久化 DynamicInjection。 +- [ ] 增加 Wire 指标事件和超时、索引未就绪、数据库异常降级。 + +完成标准:模型请求中能看到 Pack,而内存 Context、磁盘历史、恢复会话和压缩结果均不包含 Pack。 + +### P5:候选记忆自动提取 + +- [ ] 采集不含思维链和大段输出的 TurnEvidence。 +- [ ] 使用严格结构化输出生成候选,并实现敏感信息过滤。 +- [ ] 增加候选查看、接受、拒绝、forget 和 supersedes 流程。 + +完成标准:自动提取结果默认 pending;任何未验证内容都不会直接成为强约束。 + +### P6:硬化与评测 + +- [ ] 覆盖并发查询/刷新、多 CLI 进程、数据库损坏、remote KAOS 和二进制产物。 +- [ ] 完成沉淀质量、召回精度、复用效果和失效检测四组评测。 +- [ ] 根据实测调整预算、状态权重和是否引入 embedding。 + +完成标准:固定任务集可复现,原始 Trace 和失败样本保留,简历数据只引用实测结果。 + +## 11. 必测场景 + +- 同一项目不同 Session 共享记忆,不同项目严格隔离。 +- 从仓库子目录启动仍得到同一 project_id;不同 worktree 不混用索引。 +- 文件新增、修改、删除、重命名、未跟踪、脏工作区和分支切换。 +- 符号删除、符号内容变化、同名符号、动态引用和解析错误。 +- Pack 在模型请求中仅出现一次,且不会进入 Context、压缩摘要或恢复历史。 +- Root/Subagent 并发查询,索引刷新与查询并发,多进程 SQLite 竞争。 +- Repo DB 损坏可重建;Memory DB 损坏保留现场并禁用写入。 +- FTS5、Tree-sitter 在 Python 3.12–3.14、主流平台和 PyInstaller 中可用。 +- 敏感文件、密钥形态文本和超大工具输出不会进入索引摘要或记忆。 + +## 12. 暂不实施 + +- 多语言解析和编译器级精确调用图。 +- Embedding、向量数据库和云端同步。 +- 自动保存全部对话或源码正文。 +- 跨 clone 自动合并知识、团队共享知识服务和复杂权限模型。 +- 在 G1 尚未独立验收前接入 Agent Teams 调度。 + +## 13. 开发顺序约束 + +```text + ┌→ P2 Repo ───┐ +P0 → P1 ─┤ ├→ P4 → P5 → P6 + └→ P3 Memory ─┘ +``` + +Repo 与 Memory 可在 P1 完成后分支开发,但 P4 必须等待两者接口稳定。 + +任何阶段未达到完成标准时,不提前引入下一阶段的可选复杂度。特别是 FTS5 基线未证明不足前,不加入 embedding。 diff --git "a/docs/dev_note/\351\241\271\347\233\256\347\237\245\350\257\206\344\270\216Agent-Teams\345\274\200\345\217\221\350\256\241\345\210\222.md" "b/docs/dev_note/\351\241\271\347\233\256\347\237\245\350\257\206\344\270\216Agent-Teams\345\274\200\345\217\221\350\256\241\345\210\222.md" new file mode 100644 index 0000000000..bf263f6e11 --- /dev/null +++ "b/docs/dev_note/\351\241\271\347\233\256\347\237\245\350\257\206\344\270\216Agent-Teams\345\274\200\345\217\221\350\256\241\345\210\222.md" @@ -0,0 +1,147 @@ +# 项目知识与 Agent Teams 开发计划 + +> 状态:规划中 +> 目标:在复用现有 Runtime、Session、Subagent、Background、Approval 和 Wire 的前提下,完成项目级知识平面与确定性多 Agent 编排平面。 +> 使用方式:开发前确认当前里程碑,开发中勾选任务,完成后补充验证结果与关键链接。 + +## 1. 总体目标 + +### G1:项目级知识平面 + +- 增量索引 Python 文件、符号、引用和模块关系。 +- 跨会话保存决策、约束、偏好和已验证经验。 +- 检索时校验 Git、blob、文件和符号状态,识别过期或冲突记忆。 +- 将有来源、受 Token 预算约束的 Context Pack 临时注入模型请求。 + +### G2:Agent Teams 编排平面 + +- 使用确定性状态机管理 Team、Member、Task DAG 和依赖传播。 +- 复用现有子 Agent 与后台任务执行能力。 +- 支持 Mailbox、Artifact、预算、取消、写租约和进程恢复。 + +### G3:工程质量与效果证明 + +- 覆盖索引、记忆失效、DAG 调度、故障恢复和并发冲突等测试。 +- 使用固定任务集对比 baseline/candidate,所有效果数据均来自实测。 + +## 2. 范围与原则 + +- 首期只支持 Python、SQLite/FTS5 和单工作区单写者。 +- 代码事实、历史记忆、协作状态分别存储;LLM 不承担数据库或状态机职责。 +- Repo 快照只进入当前请求,不写入原始 `context.jsonl`。 +- 所有检索结果必须可追溯;未经验证的候选记忆不得直接长期保存。 +- 首期不做多语言、向量数据库、云端知识服务和多 worktree 并行写入。 + +## 3. 里程碑总览 + +| 里程碑 | 目标 | 主要交付物 | 状态 | +| --- | --- | --- | --- | +| M0 | 固化边界与评测基线 | 接入点说明、基线任务集、指标采集脚本 | 待开始 | +| M1 | 建立知识层基础设施 | 项目标识、SQLite Schema、统一模型与生命周期 | 待开始 | +| M2 | 完成 Repo Intelligence | Python 增量索引与最小查询 API | 待开始 | +| M3 | 完成 Persistent Memory | 四类记忆、FTS5、候选写入与失效校验 | 待开始 | +| M4 | 打通知识闭环 | Orchestrator、Context Pack、Wire 事件 | 待开始 | +| M5 | 完成 Teams 调度核心 | Team/Task DAG、状态机、任务派发 | 待开始 | +| M6 | 补齐协作与资源控制 | Mailbox、Artifact、预算、租约、取消传播 | 待开始 | +| M7 | 完成恢复与验收 | 状态恢复、完整测试矩阵、效果报告 | 待开始 | + +## 4. 分步计划 + +### M0:边界与基线 + +- [ ] 复核 Runtime、KimiSoul、Context、Subagent、Background、Approval、Wire 的调用链和所有权。 +- [ ] 明确新增模块接口、存储位置、配置项和错误降级策略。 +- [ ] 选定固定仓库与任务集,记录当前定位耗时、工具调用、Token 和成功率。 + +完成标准:形成可复现 baseline;每个新增能力都有明确接入点,不复制现有执行链路。 + +### M1:知识层基础设施 + +- [ ] 建立 `knowledge/` 模块、稳定的 `project_id` 和统一查询结果模型。 +- [ ] 设计 RepoIndex、MemoryStore 的 SQLite Schema、版本迁移和事务边界。 +- [ ] 在 Runtime 中装配共享知识服务,并保证 Root/Subagent 共享服务但隔离对话上下文。 +- [ ] 增加数据库损坏、服务不可用时的安全降级和基础单元测试。 + +完成标准:知识服务可创建、关闭、迁移和降级,不影响未启用该能力时的现有流程。 + +### M2:Repo Intelligence + +- [ ] 使用 Tree-sitter 提取 Python 文件、类、函数、方法、签名、import、定义和引用。 +- [ ] 基于内容 hash、commit、blob 和脏状态实现新增、修改、删除的增量刷新。 +- [ ] 提供 `search_symbol`、`find_references`、`module_summary`、`related_files` 查询。 +- [ ] 覆盖首次索引、单文件修改、删除、重命名、分支切换和并发刷新测试。 + +完成标准:索引结果带路径、范围和版本来源;变化文件能局部更新,查询失败不阻断 Agent。 + +### M3:Persistent Memory + +- [ ] 实现 `decision`、`constraint`、`preference`、`lesson` 四类记忆及代码引用模型。 +- [ ] 使用 FTS5 完成可解释检索,并过滤敏感信息、源码副本和低价值工具输出。 +- [ ] 回合结束只生成候选记忆,通过测试、Git diff 或用户确认后再持久化。 +- [ ] 根据文件、符号、commit 和 blob 标注 `valid`、`possibly_stale`、`stale`、`conflicted`。 +- [ ] 覆盖跨会话召回、重复写入、过期、冲突和删除引用测试。 + +完成标准:记忆有来源、有验证状态、可失效;新会话能检索项目级历史知识。 + +### M4:Knowledge Orchestrator + +- [ ] 根据任务类型规划 RepoIndex、MemoryStore 或双路查询,并行执行召回。 +- [ ] 实现去重、时效校验、冲突标记、相关性排序和 Token 预算裁剪。 +- [ ] 生成带来源的 Context Pack,仅加入本轮 `effective_history`。 +- [ ] 通过 Wire 记录检索来源、状态、Token、耗时和降级原因。 +- [ ] 对比 baseline,验证定位效率、探索调用和过期记忆误用情况。 + +完成标准:完成“召回—校验—临时注入—验证—候选记忆”的知识闭环,且不污染会话历史。 + +### M5:Agent Teams 调度核心 + +- [ ] 定义 Team、Member、TeamTask、Dependency 模型和合法状态迁移。 +- [ ] 实现 DAG 环检测、就绪计算、依赖失败传播、并发限制和重试规则。 +- [ ] 将就绪任务映射到现有 SubagentStore 与 BackgroundTaskManager。 +- [ ] 扩展 Approval 来源和 Wire 事件,使任务可追踪到 team/task/agent。 +- [ ] 覆盖环检测、就绪顺序、失败阻塞、超时、重复通知和审批取消测试。 + +完成标准:Scheduler 只负责编排,Agent 执行、审批和 UI 继续复用现有实现。 + +### M6:协作与资源控制 + +- [ ] 实现持久化 Mailbox,支持成员间结构化消息和消费状态。 +- [ ] 实现 Artifact Index,记录工件引用、摘要、生产者和验证状态。 +- [ ] 实现 Team/Task 分层预算的预留、结算和超限停止派发。 +- [ ] 实现单写者租约、心跳、过期回收以及 Team 取消级联。 +- [ ] 覆盖预算耗尽、成员丢失、租约回收和并发写冲突测试。 + +完成标准:成员能交换结果而无需全部回灌 Root 上下文;资源上限和写冲突由代码控制。 + +### M7:恢复、评测与交付 + +- [ ] 持久化 Team、DAG、Mailbox、Artifact、预算账本和租约。 +- [ ] 实现重启后的 `load → reconcile → schedule`,处理失联任务和过期租约。 +- [ ] 运行完整测试、格式化、静态检查及端到端冒烟测试。 +- [ ] 使用固定任务集重复对比 baseline/candidate,保存原始 Trace 和失败样本。 +- [ ] 更新用户文档、架构说明和简历材料,只陈述有代码与数据支撑的成果。 + +完成标准:异常退出后可安全恢复;核心指标有可复现实测结果;文档与仓库实现一致。 + +## 5. 验收指标 + +| 类别 | 指标 | +| --- | --- | +| 知识效率 | 首次定位正确文件耗时、`Grep`/`ReadFile` 调用数、输入 Token | +| 知识质量 | 召回准确率、过期记忆误用率、增量索引延迟 | +| 任务效果 | 最终任务成功率、测试通过率 | +| Teams 效率 | 关键路径耗时、重复工作率、预算使用 | +| Teams 安全 | 并发写冲突率、取消成功率、重启恢复成功率 | + +## 6. 开发记录模板 + +每完成一个里程碑,在对应章节更新勾选状态,并追加一条简短记录: + +```text +日期:YYYY-MM-DD +里程碑:M? +结果:完成 / 部分完成 / 阻塞 +验证:测试命令、报告或 Trace 路径 +关键决定:仅记录会影响后续开发的取舍 +下一步:一个明确动作 +``` diff --git "a/docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" "b/docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" index a40d1c0ed3..f3d477cbdd 100644 --- "a/docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" +++ "b/docs/mk/\346\225\210\346\236\234\350\257\204\346\265\213\346\226\271\346\241\210.md" @@ -9,7 +9,7 @@ - [1. 评测目标与指标总览](#1-评测目标与指标总览) - [2. 数据集选型](#2-数据集选型) - [3. 逐指标评测方案](#3-逐指标评测方案) -- [4. 自建测试集:记忆失效检测](#4-自建测试集记忆失效检测) +- [4. 自建测试集:跨会话知识复用验证](#4-自建测试集跨会话知识复用验证) - [5. Agent Teams 专项评测](#5-agent-teams-专项评测) - [6. 实验执行流程](#6-实验执行流程) - [7. 预期结果与填表示例](#7-预期结果与填表示例) @@ -205,33 +205,246 @@ SWE-bench 官方评测脚本逻辑: --- -## 4. 自建测试集:记忆失效检测 +## 4. 自建测试集:跨会话知识复用验证 -这是整个评测方案中**最具原创性**的部分,专门验证 Persistent Memory 的时效性管理。 +### 待验证的核心命题 -### 4.1 测试集构造方法 +SWE-bench 等公开数据集每次以一个**独立、干净**的会话跑一个 Issue。它们能衡量单次任务的完成能力,但无法回答知识层的核心价值: -从 SWE-bench 中利用 Git 历史自然构造"代码变更-记忆过期"场景: +> **历史会话中产生的架构决策、工程约束和踩坑经验,能否在新会话中被有效沉淀、精准召回、正确复用,从而减少重复探索、加速任务完成?** + +换句话说,公开数据集测的是"第一次见到这个仓库的 Agent 有多能打",知识层要测的是"在这个仓库上干过活的 Agent,第二次、第三次是不是越来越快"。 + +### 验证链路总览 + +一条完整的跨会话知识复用链路包含四个环节,本章分别验证每个环节是否生效: + +``` + Session 1 Persistent Session 2 + (完成 Issue X) Memory (完成关联 Issue Y) + │ │ │ + ├── 工具调用日志 ──→ 维度1:知识沉淀 ──→ 记忆库条目 │ + │ "能否正确提取" │ + │ │ │ + │ ├──────── 维度2:知识召回 ──→ 判断相关性、排序 + │ │ "能否精准召回" │ + │ │ │ + │ └──────── 维度3:知识复用 ──→ 减少探索、加速定位 + │ "召回了有没有用" │ + │ │ + └─────────────────── 代码随 commit 变更 ──→ 维度4:记忆失效 + "过期的能否安全降级" +``` + +四个维度构成一条**效果链**:沉淀不出 → 召不回 → 召回再多也没用;召回不准 → 复用噪声反而帮倒忙;复用有效但无法判废 → 代码一变更就误导。只有四个维度全部通过,知识层才算真正解决了"跨会话知识断层"。 + +--- + +### 维度 1:知识沉淀质量 + +> **验证问题**:一次会话结束后,系统能否从 Agent 的工具调用日志和对话中**正确提取**出结构化的工程知识? + +如果沉淀阶段就漏掉了关键信息或提取了错误信息,后续召回和复用无从谈起。这是全链路的基础。 + +#### 1.1 测试方法 ``` -构造流程: - 仓库时间线 +方法:人工标注 → 自动沉淀 → 对比差异 + +步骤: + 1. 选取 6–8 个 SWE-bench 实例,在 Baseline 模式下运行 Agent 并完整记录 + 所有工具调用、模型推理和最终 patch + 2. 人工审阅每个实例的完整日志,按以下四类标注出"应该被沉淀的知识条目", + 生成 golden set + 3. 用知识层的沉淀模块处理相同的日志,自动生成沉淀条目 + 4. 对比 golden set 与自动沉淀结果 +``` + +#### 1.2 四类工程记忆的 Golden Set 标注规范 + +| 记忆类型 | 定义 | 标注示例 | +|----------|------|----------| +| **架构决策** | Agent 发现的模块边界、设计模式、数据流方向、关键抽象 | "`QuerySet._chain()` 是所有 QuerySet 方法返回新实例的统一入口,修改查询行为应在此处拦截而非逐个方法覆写" | +| **工程约束** | 不可违反的规则、已知的坑、版本兼容要求 | "django `Field.default` 不能设为可变对象(如 `{}` 或 `[]`),必须在 `__init__` 中判断 `callable` 后延迟赋值" | +| **编码偏好** | 该仓库的命名习惯、文件组织方式、测试风格 | "测试类统一继承 `SimpleTestCase` 而非 `TestCase`,避免不必要的数据库事务开销" | +| **验证经验** | 上次修改后如何确认正确性、跑哪些测试、关注哪些边界 | "修改 `Field.__init__` 后需运行 `tests/model_fields/` 和 `tests/forms/` 全部用例,特别关注 `test_deconstruct` 序列化往返" | + +#### 1.3 指标 + +| 指标 | 公式 | 目标值 | 说明 | +|------|------|:------:|------| +| **沉淀召回率** | `提取出的正确知识 / Golden Set 总条目` | ≥ 85% | 关键知识是否遗漏 | +| **沉淀精确率** | `提取出的正确知识 / 自动沉淀总条目` | ≥ 75% | 是否混入噪声(幻觉/碎片) | +| **类型覆盖率** | `四类记忆至少各沉淀 1 条 的实例 / 总实例` | 100% | 是否偏科——只提代码事实不提决策 | +| **可验证性** | `绑定具体文件/符号/commit 的记忆 / 总记忆` | ≥ 90% | 每条记忆是否能在代码中找到对应证据 | + +#### 1.4 测试规模 + +| 维度 | 数量 | +|------|:----:| +| SWE-bench 实例 | 6–8 个(覆盖 3–4 个仓库) | +| Golden Set 总条目 | 预计 40–60 条(每实例 5–10 条) | +| 实例选取要求 | 包含至少 1 个涉及跨模块修改的复杂 Issue | + +--- + +### 维度 2:知识召回精度 + +> **验证问题**:新会话发起任务时,系统能否从记忆库中**精准召回**与该任务相关的历史知识? + +即使沉淀质量好,如果召回策略不准——漏掉关键记忆或带回大量不相关记忆——后续要么没用,要么引入噪声。 + +#### 2.1 测试方法 + +``` +方法:离线召回基准测试(不依赖完整 Agent 运行,可快速迭代) + +步骤: + 1. 选定 3 个仓库,每个仓库向记忆库预先写入 20–30 条人工编写的记忆 + (混合架构决策、工程约束、编码偏好、验证经验) + 2. 为每个仓库准备 10 个查询(Query),对应 SWE-bench Issue 描述或 + 人工构造的任务描述 + 3. 每个 Query 由人工标注相关记忆集合(relevant set) + 4. 用知识层的 Knowledge Orchestrator 对每个 Query 执行召回 + 5. 计算 Precision@k / Recall@k / NDCG@k / MRR +``` + +#### 2.2 查询类型覆盖 + +召回不能只测"关键词完全匹配"的场景。真实的跨会话任务关联是多样的: + +| 查询类型 | 说明 | 示例 | +|----------|------|------| +| **直接关联** | 新任务涉及与历史记忆完全相同的模块/函数 | "Fix `QuerySet.count()` behavior" ← 记忆:`QuerySet` 的链式调用机制 | +| **间接关联** | 新任务涉及同一模块的其他函数,或调用相同底层依赖 | "Fix `QuerySet.iterator()` chunk size" ← 记忆:`QuerySet._chain()` 的返回语义 | +| **约束关联** | 新任务不涉及相同代码,但受同一工程约束 | "Add a new Field type `PointField`" ← 记忆:`Field.default` 不能设可变对象 | +| **不相关** | 新任务与历史记忆无关 | "Fix `django.conf.settings` lazy loading" ← 记忆:ORM Field 相关(应不召回) | + +#### 2.3 指标 + +| 指标 | 公式/说明 | 目标值 | +|------|-----------|:------:| +| **Precision@5** | 召回 Top-5 中相关记忆的比例 | ≥ 0.70 | +| **Recall@5** | Top-5 覆盖了多少应召回的相关记忆 | ≥ 0.80 | +| **NDCG@5** | 考虑排序质量的归一化折损累积增益 | ≥ 0.75 | +| **MRR** | 第一条相关记忆的平均倒数排名 | ≥ 0.80 | +| **无关召回率** | 不相关 Query 的 Top-5 中混入记忆的比例 | ≤ 0.10 | + +#### 2.4 测试规模 + +| 维度 | 数量 | +|------|:----:| +| 仓库 | 3 个(django、flask、requests) | +| 每仓库预写记忆 | 20–30 条 | +| 每仓库查询 | 10 个(覆盖 4 种关联类型) | +| **总查询** | **30 个** | + +--- + +### 维度 3:知识复用效果 + +> **验证问题**:召回的知识**实际帮到了 Agent 吗**?探索行为减少了吗?任务完成得更快/更好了吗? + +维度 2 测的是"搜不搜得到",维度 3 测的是"搜到了有没有用"。这是全链路中最关键的效果证据。 + +#### 3.1 测试方法 + +``` +方法:同仓库连续任务实验(A/B 对比) + +核心设计: + 同一个仓库中,先跑 Issue X(沉淀知识),再跑关联 Issue Y(复用知识)。 + 比较:Issue Y 在"无知识辅助" vs "有 Issue X 沉淀的知识辅助"两种模式下的表现差异。 + +步骤: + 1. 从 SWE-bench Lite 中按仓库分组,选出同一仓库的 Issue 对 (X, Y), + 要求 X 和 Y 存在代码层面关联(共享修改过的模块或函数) + 2. 实验组 A(Baseline):用干净的新会话独立跑 Issue Y,不注入任何历史记忆 + 3. 实验组 B(Candidate): + a. 用独立会话跑 Issue X → 沉淀知识(维度 1 已验证的沉淀流程) + b. 用全新会话跑 Issue Y,推理前由 Knowledge Orchestrator 召回并注入 + 与 Issue Y 相关的记忆 + 4. 对比 A 和 B 在 Issue Y 上的表现差异 +``` + +#### 3.2 Issue 对选取标准 + +| 标准 | 要求 | 说明 | +|------|------|------| +| 同仓库 | X 和 Y 属于同一个 SWE-bench 仓库 | 否则谈不上知识跨会话复用 | +| 代码关联 | X 修改的文件与 Y 修改的文件有一定交集,或同属一个模块 | 无关联的 Issue 对无法体现知识价值 | +| 非相同文件 | X 和 Y 不是修改完全相同的文件 | 排除"完全一样的定位"(太 trivial) | +| 难度梯度 | 混合简单和复杂 Issue | 验证知识对不同难度任务的边际价值 | + +``` +示例 Issue 对(django 仓库): + Issue X: "Fix QuerySet.count() with custom select" (修改 django/db/models/query.py) + ↓ 沉淀知识:QuerySet 的方法链机制、count() 与 _fetch_all() 的交互 + Issue Y: "Fix QuerySet.iterator() chunk size on PostgreSQL" (同样修改 query.py) + ↑ 关联:Agent 需要理解 QuerySet 的内部执行流程 +``` + +#### 3.3 指标(对应总表中的 M1–M4) + +| 指标 | 对应 | 测量方式 | 期望方向 | +|------|:----:|----------|:--------:| +| **首次定位耗时变化** | M1 | T(Y_B) vs T(Y_A) 的首次打开正确文件时间差 | ↓ 40%–60% | +| **探索调用减少比例** | M2 | grep/glob/read_file 调用次数的减少比例 | ↓ 50%–65% | +| **输入 Token 节省比例** | M3 | 全任务 input tokens 的减少比例 | ↓ 30%–45% | +| **成功率变化** | M4 | Y_B 和 Y_A 的 Resolved Rate 对比 | +5–10pp | +| **知识贡献率** | 新增 | 被 Agent 实际引用的记忆 / 召回的记忆总数 | ≥ 50% | + +> **知识贡献率**是新维度专属指标:召回的记忆被 Agent 在推理中引用(如"根据之前的经验,这里应该...")才算真正产生了价值。高召回 + 低贡献率 = 噪声。 + +#### 3.4 定性分析 + +除定量指标外,每对 Issue 还需做定性判断: + +``` +Agent 行为日志的定性分析(选做,用于发现定量指标无法捕捉的问题): + - Agent 是否因为错误记忆走上了错误的探索路径? + - Agent 是否忽视了正确记忆,仍然进行了不必要的搜索? + - 记忆是否提供了仅靠代码索引得不到的"为什么这样设计"的解释? + - Agent 是否将记忆中的约束正确转化为代码修改? +``` + +#### 3.5 测试规模 + +| 维度 | 数量 | +|------|:----:| +| 仓库 | 3–4 个(django、scikit-learn、flask、requests) | +| 每仓库 Issue 对 | 4–6 组 | +| **总实验组** | **15–20 组 Issue 对,每组跑 Baseline + Candidate 各 1 次** | + +--- + +### 维度 4:记忆失效检测 + +> **验证问题**:当代码随 commit 变更后,系统能否**自动识别**哪些记忆已过时,并将其安全降级而不误导 Agent? + +维度 3 保证"好的记忆被复用",维度 4 保证"坏(过时)的记忆被拦截"。二者共同构成知识层的**正向效果 + 负向兜底**。 + +#### 4.1 测试方法 + +``` +方法:利用 Git 历史自然构造"代码变更记忆过期"场景 + + 仓库时间线: commit A (v1) ────────── commit B (v2) ──────────→ main │ │ - │ Issue X (较老) │ Issue Y (较新) - │ 在 v1 上解决 │ 在 v2 上解决 - │ │ - 步骤 1:在 v1 上跑 Issue X │ - 步骤 2:沉淀记忆 M │ - (记录修改了哪些函数、为什么这样改) │ + Issue X (较老) Issue Y (较新) + 在 v1 上解决 在 v2 上解决 │ │ - │ 步骤 3:切换到 v2(commit A→commit B) - │ 步骤 4:在 v2 上跑 Issue Y - │ 步骤 5:Agent 自动召回记忆 M - │ 步骤 6:检查 Agent 是否正确判断 M 的时效性 + 步骤 1:在 v1 上跑 Issue X → 沉淀记忆 M │ + │ 步骤 2:切换到 v2(commit A→commit B) + │ 步骤 3:在 v2 上跑 Issue Y + │ 步骤 4:Agent 召回记忆 M + │ 步骤 5:检查 Agent 是否正确判断 M 的时效性 ``` -### 4.2 四类记忆失效场景 +> 维度 4 与维度 3 的区别:维度 3 的 Issue 对中,记忆**仍然有效**(仓库代码未变更,或变更未涉及被记忆引用的部分),测试的是复用效果;维度 4 刻意选取了**代码已变更**的 Issue 对,测试的是判废是否安全。 + +#### 4.2 四类记忆失效场景 | 场景编号 | 场景 | 构造方式 | 期望行为 | |:--------:|------|----------|----------| @@ -240,32 +453,52 @@ SWE-bench 官方评测脚本逻辑: | F3 | **代码仍有效** | 记忆 M 引用的符号在两个 commit 间未变化,blob hash 一致 | Agent 标记 M 为 `valid`,正常使用 | | F4 | **验证经验冲突** | 记忆 M 中的约束(如"禁止直接调用 X")在新版本中与代码实际情况矛盾 | Agent 标记 M 为 `conflicted`,触发人工确认 | -### 4.3 指标计算 +#### 4.3 指标(对应总表中的 M5) ``` 过期记忆误用率 = 误用次数 / 总召回次数 定义: - - 总召回次数:Agent 在实验中从记忆库召回的记忆条目总数 + - 总召回次数:Agent 在维度 4 实验中从记忆库召回的记忆条目总数 - 误用次数:Agent 基于已失效/冲突的记忆做出了错误决策的次数 -细分: - - valid 召回准确率 = valid 判定且确实有效 / valid 判定总数 - - stale 召回率 = stale 判定且确实失效 / 实际失效总数 - - 冲突检出率 = 检出的冲突 / 实际存在的冲突 +细分指标: + - valid 召回准确率 = 判定 valid 且确实有效 / 判定 valid 总数 + - stale 召回率 = 判定 stale 且确实失效 / 实际失效总数 + - 冲突检出率 = 检出的冲突 / 实际存在的冲突 + - 误降级率 = 仍有效但被错误标记为 stale 的记忆 / 实际有效总数 ``` -### 4.4 测试规模 +| 指标 | 目标值 | 说明 | +|------|:------:|------| +| 过期记忆误用率 | **≤ 5%** | 一条过期记忆都不应该被当作正确依据 | +| stale 召回率 | ≥ 90% | 失效的记忆要尽可能找出来 | +| 误降级率 | ≤ 5% | 仍然有效的记忆不被错误标记——过度降级会削弱维度 3 的复用效果 | + +#### 4.4 测试规模 | 组合维度 | 数量 | |----------|:----:| -| 仓库 | 3–4 个(从 SWE-bench 选活跃仓库如 django、flask、requests) | -| 每仓库 Issue 对 | 6–8 组 | +| 仓库 | 3–4 个(从 SWE-bench 选活跃仓库) | +| 每仓库 Issue 对(跨 commit) | 6–8 组 | | 每 Issue 对沉淀记忆数 | 2–3 条 | | **总测试用例** | **20–30 组,含 60–80 条记忆的失效判断** | --- +### 4.5 四维度汇总 + +| 维度 | 验证问题 | 指标 | 对应简历指标 | 测试规模 | +|------|----------|------|:---:|:--------:| +| ① 沉淀质量 | 能否正确提取知识? | 召回率 ≥ 85%,精确率 ≥ 75% | — | 6–8 实例 | +| ② 召回精度 | 能否精准检索知识? | Precision@5 ≥ 0.70,Recall@5 ≥ 0.80 | — | 30 查询 | +| ③ 复用效果 | 知识是否实际加速任务? | 定位耗时 ↓ / 探索 ↓ / Token ↓ / 成功率 ↑ | **M1–M4** | 15–20 组 | +| ④ 失效检测 | 过期知识能否安全降级? | 误用率 ≤ 5%,stale 召回率 ≥ 90% | **M5** | 20–30 组 | + +> **四个维度的关系**:维度 ①→②→③ 串联成效果链——沉淀不出就召不回,召回不准就没用,复用有效才证明知识层有价值。维度 ④ 是整条链的安全网——代码会变,不变的安全降级会让前三个维度的价值被过时记忆侵蚀。**四个维度全部通过才算解决了"跨会话知识断层"。** + +--- + ## 5. Agent Teams 专项评测 ### 5.1 数据集 diff --git "a/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-CLI-\351\241\271\347\233\256\347\272\247\351\205\215\347\275\256\346\224\271\351\200\240.md" "b/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-CLI-\351\241\271\347\233\256\347\272\247\351\205\215\347\275\256\346\224\271\351\200\240.md" new file mode 100644 index 0000000000..31bed02068 --- /dev/null +++ "b/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-CLI-\351\241\271\347\233\256\347\272\247\351\205\215\347\275\256\346\224\271\351\200\240.md" @@ -0,0 +1,217 @@ +# Kimi CLI — 秋招项目简历(项目级多模型配置改造) + +> **项目名称**:Kimi CLI:面向多项目、多模型场景的配置隔离与启动链路改造 +> **项目性质**:开源项目二次开发 +> **角色**:核心功能开发者 +> **时间**:2026.08 +> **关键词**:`Python` `Typer` `asyncio` `Pydantic` `python-dotenv` `Dependency Injection` `pytest` `LLM Provider` + +--- + +## S — 背景 Situation + +Kimi CLI 是一个运行在终端中的开源 Coding Agent,已具备 Shell、Print、ACP、Wire 等多种前端,会话恢复、工具调用、MCP、子 Agent、审批和事件流等完整运行时能力。原有模型配置主要来自全局配置文件 `~/.kimi/config.toml` 和进程环境变量,适合单一用户环境,但在同时维护多个项目、切换 Kimi 与 OpenAI-compatible 模型提供商时存在三个问题: + +- 不同项目共用全局配置,切换模型、API 地址和密钥时需要反复修改配置或传递命令行参数; +- 直接 `export` 环境变量会污染整个终端进程及其子进程,项目之间容易串用模型或凭据; +- `llm.py` 直接读取 `os.environ`,配置来源与 LLM 构造耦合,难以注入独立环境并验证优先级和隔离行为。 + +因此,本次改造不重写已有 Agent 运行时,而是在现有 `CLI → Config → LLM → Runtime` 启动链路中增加一个项目级、可覆盖、无全局副作用的配置层。 + +--- + +## T — 任务 Task + +在保持原有配置文件、环境变量和各类 UI 启动方式兼容的前提下,完成项目级 LLM 配置能力: + +| 目标 | 约束 | +|------|------| +| 每个工作目录可通过 `.env` 选择配置文件并覆盖模型参数 | 不修改 `os.environ`,不影响其他项目和后续模块 | +| 支持 `--env-file` 显式指定 dotenv 文件 | 未传参数时仍兼容原有启动方式,并自动发现项目 `.env` | +| 让 Kimi 与 OpenAI-compatible Provider 共用同一注入机制 | 复用原有 `Config`、`LLMProvider`、`LLMModel` 和 `create_llm()` | +| 明确配置优先级与相对路径语义 | 无效配置路径应在启动早期给出可诊断错误 | +| 补齐测试、命令帮助和用户文档 | 不把上游已有 Agent、Session、MCP、Wire 能力描述为个人实现 | + +--- + +## A — 行动 Action + +### Phase 1:基于 Git 历史还原启动架构,确定最小改造面 + +- 结合模块提交历史,从 `src/kimi_cli/__main__.py` 追踪 Typer 主回调、Session 创建、`KimiCLI.create()` 装配、`llm.py` Provider 构造、Runtime 初始化和 UI 分发,确认配置改造的接入点应位于 **工作目录确定之后、LLM 创建之前**。 +- 将原有能力按职责拆分:CLI 层负责参数和路径解析,`config.py` 负责 TOML/JSON 模型,`llm.py` 负责 Provider 适配,`app.py` 负责把 Config、LLM、Runtime、Agent、Context 和 KimiSoul 接线。 +- 没有另建一套配置框架,而是保留现有 Config 与 Provider 抽象,仅把“配置从哪里读取”从“如何创建 LLM”中解耦,控制改动范围并保持旧调用方兼容。 + +### Phase 2:设计双层配置模型与确定性优先级 + +- 将项目配置拆成两个互补层次: + - **结构化配置层**:继续使用已有 TOML/JSON,保存 Provider 类型、模型别名、上下文长度和 thinking 能力等稳定结构; + - **项目环境层**:新增 `.env`,保存 API Key、Base URL、实际模型名和生成参数等项目私有覆盖项。 +- 为配置文件选择建立优先级:`--config` → `--config-file` → 工作目录 `.env` 中的 `KIMI_CONFIG_FILE` → 原有默认配置;其中相对 `KIMI_CONFIG_FILE` 路径统一按工作目录解析,不存在时在 CLI 启动阶段直接报错。 +- 为 LLM 环境覆盖建立优先级:`--env-file` 指定文件优先于工作目录 `.env`;选定文件中的值覆盖当前进程环境,再覆盖已加载的 Provider/Model 配置。 + +### Phase 3:实现无副作用的 dotenv 读取与显式依赖注入 + +- 新增 `utils/dotenv.py`: + - `load_dotenv_values()` 使用 `python-dotenv` 解析文件,但不调用 `load_dotenv()`,避免直接写入全局进程环境; + - `load_llm_env()` 先复制 `os.environ`,再在副本上叠加项目 `.env`,形成只读 `Mapping[str, str]` 供当前 Kimi CLI 实例使用。 +- 将 `augment_provider_with_env_vars()` 和 `create_llm()` 从内部硬编码 `os.getenv()` 改为接收可选 `Mapping`;未传入时仍回退到 `os.environ`,从而兼容原有调用方,同时让项目环境可以被单元测试和其他前端显式注入。 +- 复用原有 Provider 分支和参数校验逻辑,将同一份环境映射贯穿 Provider/Model 覆盖及 `temperature`、`top_p`、`max_tokens`、`thinking_keep` 等生成参数解析,避免配置在不同阶段读取到不一致的值。 + +### Phase 4:接入 CLI 与应用装配链路 + +- 在 Typer 主命令新增 `--env-file` 参数,利用 Typer/Click 自带的存在性、文件类型和可读性校验,避免重复实现路径验证。 +- 将 `work_dir` 的解析前移,使 CLI 能在加载 Config 前定位项目 `.env`;从中解析 `KIMI_CONFIG_FILE`,并处理 `~` 展开、相对路径拼接和文件存在性校验。 +- 在 `KimiCLI.create()` 中复用 `Session.work_dir` 自动发现默认 `.env`,构造当前实例专属的 `llm_env`,依次传入 Provider 覆盖和 `create_llm()`;之后继续沿用原有 OAuth、Runtime、Agent Spec、Context 恢复、KimiSoul、Hooks、Telemetry 和 UI 流程。 +- 增加 `dev/deepseek.toml` 作为 OpenAI-compatible Provider 示例,将 Provider/模型结构保留在 TOML,将密钥和端点覆盖留在项目 `.env`,验证该方案不依赖单一模型厂商。 + +### Phase 5:测试、文档与回归验证 + +- 新增 3 个针对性单元测试,覆盖 dotenv 解析不修改进程环境、项目值覆盖进程值、LLM 层使用显式环境映射而非偷读全局变量。 +- 在中文环境变量文档和命令参考中补充 `.env`、`KIMI_CONFIG_FILE`、`--env-file` 的使用方式与优先级,并在 CLI `--help` 中暴露新参数。 +- 对 `dotenv.py`、`llm.py`、`app.py`、CLI 与相关测试执行 Ruff 检查;运行 dotenv 与 LLM 构造相关测试集,结果为 **34 passed**。 + +--- + +## 复用了什么,新增了什么 + +| 层次 | 复用的上游能力 | 本次新增或修改 | +|------|----------------|----------------| +| CLI | Typer 主回调、`--config` / `--config-file`、参数校验、UI 模式分发 | `--env-file`;工作目录 `.env` 发现;`KIMI_CONFIG_FILE` 路径解析与错误处理 | +| 配置 | `Config`、`load_config()`、TOML/JSON、Provider/Model 数据模型 | 项目环境覆盖层;结构配置与敏感覆盖项分离;明确优先级 | +| LLM | Kimi、OpenAI Legacy/Responses Provider 分支;Kosong LLM 抽象;OAuth | 将全局 `os.getenv()` 重构为可注入 `Mapping`,统一 Provider 与生成参数的配置来源 | +| 运行时 | `Session.work_dir`、`KimiCLI.create()`、Runtime、Agent、Context、KimiSoul | 在 LLM 构造前注入实例级 `llm_env`,后续运行时保持不变 | +| 工程化 | pytest、Ruff、现有中文文档结构 | dotenv 工具模块、3 个新增测试、DeepSeek 示例配置、用户文档与 CLI Help | + +> 核心取舍:复用成熟的 Agent 执行框架,只新增“项目配置解析与注入”这一薄层;通过显式环境映射代替全局状态,使改造可以独立测试,也不会把项目密钥传播到不相关模块。 + +--- + +## 修改后的整体框架流程 + +```mermaid +flowchart TD + A["用户执行 kimi"] --> B["Typer 解析参数"] + B --> C["确定 work_dir"] + + C --> D{"是否显式提供配置?"} + D -->|"--config / --config-file"| E["复用 load_config 加载 Config"] + D -->|"否"| F["读取 work_dir/.env 中的 KIMI_CONFIG_FILE"] + F --> E + + C --> G{"是否传入 --env-file?"} + G -->|"是"| H["读取指定 dotenv"] + G -->|"否"| I["自动读取 work_dir/.env"] + H --> J["复制 os.environ 并叠加项目变量"] + I --> J + + E --> K["KimiCLI.create 选取 Provider / Model"] + J --> L["显式 llm_env Mapping"] + K --> M["augment_provider_with_env_vars"] + L --> M + M --> N["create_llm 读取统一生成参数"] + + N --> O["复用 Runtime.create"] + O --> P["加载 Agent Spec / Tools / MCP"] + P --> Q["恢复 Context 并构造 KimiSoul"] + Q --> R["Shell / Print / ACP / Wire"] +``` + +新的主流程可以概括为: + +```text +CLI 参数 + 项目工作目录 + → 选择结构化 Config + → 选择项目 dotenv + → 构造不污染进程的 llm_env + → 覆盖 Provider / Model / 生成参数 + → 创建 LLM + → 复用原有 Runtime → Agent → Context → KimiSoul → UI +``` + +--- + +## R — 成果 Result + +- 将原有“全局配置 + 全局环境变量”扩展为“全局默认 + 项目配置 + 显式 env 文件”的多层配置体系,使不同仓库可独立选择模型、API 地址和生成参数,无需反复修改用户级配置。 +- 通过复制并注入 `Mapping` 的方式隔离项目环境;测试验证 `.env` 可以覆盖当前进程中的同名值,但不会反向修改 `os.environ`。 +- 保持旧 API 兼容:`augment_provider_with_env_vars()` 与 `create_llm()` 未接收显式环境时仍使用原有进程环境,原有 Provider 与 UI 调用链无需重写。 +- 完成代码、示例配置、CLI Help 和中文文档闭环;相关测试集 **34 项全部通过**,目标文件 Ruff 检查通过。 + +> **结果边界**:当前证据能够支撑“完成项目级配置改造并通过相关回归测试”;尚无线上用户量、启动耗时或故障率数据,因此简历中不写未经实验验证的性能提升比例。 + +--- + +## 可直接投递的简历版本 + +### 项目名称 + +**Kimi CLI:面向多项目、多模型场景的配置隔离与启动链路改造**|开源项目二次开发|2026.08 + +### 技术栈 + +`Python`、`Typer`、`Pydantic`、`python-dotenv`、`Kosong`、`pytest`、`Ruff` + +### 项目描述 + +Kimi CLI 是支持会话恢复、工具调用、MCP、子 Agent 及 Shell/ACP 等多前端的终端 Coding Agent。本项目针对多仓库共用全局模型配置、环境变量易串用的问题,在不重写现有 Agent Runtime 的前提下,为启动链路增加项目级、无全局副作用的 LLM 配置层。 + +### 个人工作 + +- 基于 Git 历史和源码调用链,梳理 `CLI → Config → LLM → Runtime → Agent → KimiSoul → UI` 架构,定位配置接入点并复用现有 Typer、Config、Session、Provider 与 Runtime 抽象,避免侵入核心 Agent 循环。 +- 新增项目 `.env` 自动发现和 `--env-file` 参数,支持通过 `KIMI_CONFIG_FILE` 选择项目 TOML/JSON;实现显式配置、项目配置与全局默认的优先级、相对路径解析及启动期错误校验。 +- 将 LLM 层对 `os.getenv()` 的隐式依赖重构为可注入的 `Mapping[str, str]`,以“复制进程环境 + 叠加项目值”实现实例级配置隔离,并统一驱动 Provider、模型及生成参数构造,兼容 Kimi 与 OpenAI-compatible 接口。 +- 补充 DeepSeek Provider 示例、CLI/中文文档和 3 个隔离性测试;dotenv 与 LLM 相关回归测试 **34 项通过**,目标模块 Ruff 检查通过。 + +--- + +## 面试核心表达 + +> 我没有重做 Kimi CLI 已有的配置和 Agent 框架,而是先通过 Git 历史和调用链确定边界:Typer 负责参数,Config 负责结构,llm.py 负责 Provider,app.py 负责装配。真正的问题是 LLM 构造直接读取全局环境,所以我增加项目 dotenv 层,并把环境从全局状态改成显式依赖。现在每次启动先根据工作目录选择 Config 和 dotenv,再生成当前实例独享的环境映射,覆盖 Provider、模型和生成参数,之后继续走原有 Runtime、Agent、Context、KimiSoul 与 UI 流程。这样既实现了多项目隔离,也保持了旧调用方兼容。 + +--- + +## 高频追问速答 + +**Q:为什么不直接调用 `load_dotenv()`?** + +`load_dotenv()` 会修改 `os.environ`,使项目配置变成进程级全局状态,后续创建的 Provider、插件或子进程都可能读到不属于自己的值。我选择解析后生成环境副本,并显式传给 LLM 构造链路,副作用边界更清晰,也更容易测试。 + +**Q:为什么同时保留 TOML 和 `.env`?** + +两者职责不同:TOML 适合保存 Provider 类型、模型别名、能力集等可版本化结构;`.env` 适合保存密钥、私有端点和项目覆盖。让 `.env` 通过 `KIMI_CONFIG_FILE` 选择 TOML,可以在不复制整份结构配置的情况下实现项目级切换。 + +**Q:这次改造复用了哪些关键模块?** + +复用了 Typer 参数系统、`Config/load_config`、`Session.work_dir`、Pydantic Provider/Model、Kosong Provider 适配、OAuth、`KimiCLI.create()` 以及后续 Runtime/Agent/KimiSoul/Wire 链路。本次新增集中在配置发现、环境合并和显式注入,没有把上游已有功能包装成个人产出。 + +**Q:如何保证兼容性?** + +新增的 `env` 参数都是可选参数;调用方不传时仍回退到 `os.environ`。没有项目 `.env` 时也继续使用原配置文件和进程环境,因此旧命令和各 UI 模式保持原行为。 + +**Q:当前测试还缺什么?** + +现有测试已覆盖 dotenv 的无副作用读取和 LLM 显式注入;后续应补充 CLI 集成测试,完整覆盖 `--config`、`--config-file`、`KIMI_CONFIG_FILE`、`--env-file` 的优先级,以及配置文件不存在、相对路径和 Shell/Print/ACP 多入口的一致性。 + +--- + +## 关键代码位置 + +| 模块 | 路径 | 作用 | +|------|------|------| +| CLI 参数与配置选择 | `src/kimi_cli/cli/__init__.py` | `--env-file`、work_dir、`KIMI_CONFIG_FILE` 和优先级 | +| 应用装配 | `src/kimi_cli/app.py` | 自动发现 `.env`,生成 `llm_env` 并注入 LLM 链路 | +| dotenv 工具 | `src/kimi_cli/utils/dotenv.py` | 无副作用解析与环境副本合并 | +| LLM 适配 | `src/kimi_cli/llm.py` | Provider/Model 覆盖及生成参数显式注入 | +| 示例配置 | `dev/deepseek.toml` | OpenAI-compatible Provider 与模型定义 | +| 单元测试 | `tests/utils/test_dotenv.py`、`tests/core/test_create_llm.py` | 环境隔离、覆盖语义和兼容性验证 | + +--- + +## 真实性检查清单 + +- [x] 将 Session、Runtime、Agent、MCP、子 Agent、Wire 等标为上游复用能力 +- [x] 将个人实现范围限定为项目级配置发现、隔离、注入、测试与文档 +- [x] 测试结果来自本地实际执行:34 passed;Ruff 检查通过 +- [x] 未虚构性能提升、线上使用量、PR 合并或生产部署结果 +- [ ] 投递前补充 CLI 配置优先级集成测试,可进一步增强结果证据 diff --git "a/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI-Action\344\270\260\345\257\214\347\211\210.md" "b/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI-Action\344\270\260\345\257\214\347\211\210.md" new file mode 100644 index 0000000000..6a2b738846 --- /dev/null +++ "b/docs/mk/\347\247\213\346\213\233\351\241\271\347\233\256\347\256\200\345\216\206-Kimi-Code-CLI-Action\344\270\260\345\257\214\347\211\210.md" @@ -0,0 +1,281 @@ +# Kimi Code CLI — 秋招项目简历(Action 丰富版) + +> **项目名称**:Kimi Code CLI:具备持久化项目知识与多 Agent Teams 编排能力的开源 Coding Agent +> **角色**:核心开发者 +> **时间**:2025.06 — 至今 +> **关键词**:`Python` `asyncio` `SQLite/FTS5` `Tree-sitter` `Pydantic` `Typer` `Git` `pytest` `Multi-Agent` `Event-Driven` + +> **真实性说明**:当前仓库已经具备 Agent 主循环、单会话持久化、子 Agent、后台任务、审批和 Wire 事件系统;Repo Intelligence、Persistent Memory 和完整 Agent Teams 仍属于新增项目范围。未完成相应代码、测试和评测前,应将下文中的“实现”改为“设计”,不要把规划写成已经上线的能力。 + +--- + +## S — 背景 Situation + +Kimi Code CLI 是一个运行在终端中的开源 Coding Agent,已具备异步主循环、会话恢复、子 Agent、后台任务、审批和事件驱动 UI。但在大型仓库的长期开发中,现有架构仍有两个瓶颈: + +- **知识断层**:`Session` 和 `Context` 能恢复指定会话,`SubagentStore` 也能保存子 Agent 的独立上下文,但新会话不会主动检索其他会话中的架构决策、工程约束和验证经验;Agent 仍需通过 `Glob`、`Grep`、`ReadFile` 重复探索仓库。 +- **协作缺失**:Root Agent 可以并发启动多个后台子 Agent,`BackgroundTaskManager` 也能管理任务状态、输出、超时和终止,但现有 `TaskSpec` 没有依赖边、成员消息、共享工件和统一预算,多个 Agent 本质上仍是彼此独立的后台任务。 + +> **代码规模背景**:运行时 Python 约 6.6 万行,测试约 6.4 万行(来自仓库调研快照,仅用于说明系统复杂度,不作为个人产出)。 + +--- + +## T — 任务 Task + +在不重写 Agent 主循环、不另建旁路执行系统的前提下,基于现有 Runtime 和 Session 基础设施完成两项改造: + +| 知识平面 | 协作平面 | +| --- | --- | +| 构建统一项目知识层,增量索引当前代码事实,持久化跨会话工程记忆,并在每次模型推理前注入经过来源追踪、时效校验和 Token 裁剪的 Context Pack | 在现有子 Agent 和后台任务之上增加 Agent Teams 编排层,以确定性状态机管理 Team、Member、Task DAG、Mailbox、Artifact、预算、取消和恢复 | + +同时满足以下工程约束: + +- 当前代码事实与历史决策分开存储,避免用不可验证的记忆替代源码; +- 动态 Repo 快照不永久写入原始会话历史,避免恢复会话时重复注入过期内容; +- LLM 只负责语义拆解和总结,任务就绪、状态迁移、预算扣减和失败传播由代码状态机保证; +- 首期限制团队规模并采用单写者策略,避免开放无限递归和多写者后造成资源失控或文件冲突; +- 所有“提升”必须通过固定仓库、任务、模型和预算的 baseline/candidate 实验验证。 + +--- + +## A — 行动 Action + +### Phase 1:从源码调用链划分边界,确定最小侵入式接入点 + +- 沿 `CLI → KimiCLI.create() → Runtime.create() → Agent → Context → KimiSoul.run() → Toolset → Wire/UI` 还原完整启动和执行链路,并继续追踪子 Agent 创建、后台任务、审批投影与会话落盘过程,明确系统中“执行”“持久化”“协作”“展示”的责任边界。 +- 核对现有能力后,将可复用基础拆为五层: + - `Session`、`Context` 和 `SubagentStore` 已能保存会话、消息历史、子 Agent 元数据、独立 `context.jsonl` 与 `wire.jsonl`; + - `Runtime.copy_for_subagent()` 已能让子 Agent 共享配置、审批、通知和 Root Wire Hub,同时保持独立上下文; + - `BackgroundTaskManager` 已能异步启动 Agent、限制并发、记录运行状态、输出、心跳、超时和终止信号; + - `ApprovalRuntime` 已能按执行来源创建、等待、解析和取消审批,并把请求投影到 Root Wire; + - `Wire` 已提供 Soul 到 UI 的单生产者多消费者事件通道和 JSONL 回放能力。 +- 识别出两个关键缺口:现有持久化边界是“单个 Session”,不能提供项目级跨会话检索;现有后台任务是“独立 Task 列表”,不能表达任务依赖和成员协作。 +- 基于上述边界把改造拆成两个正交平面:知识平面只负责“给 Agent 什么可信上下文”,协作平面只负责“哪些 Agent 在什么条件下执行什么任务”,两者继续调用同一套 Agent Runtime,不复制 LLM、工具、审批或 UI 框架。 + +### Phase 2:实现 Repo Intelligence 增量代码事实层 + +- 新增项目级 `RepoIndex`,首期仅解析 Python:通过 Tree-sitter 提取文件、类、函数、方法、签名、源码范围、import、定义、引用和可静态确定的模块依赖,形成可查询的符号图,而不是保存整份源码副本。 +- 复用现有工作目录和 Git 上下文作为项目作用域,通过当前 `HEAD`、文件 blob hash、工作区脏状态和内容 hash 判断变化范围: + 1. 启动或后台刷新时读取当前 revision 与文件清单; + 2. 将文件 hash 与索引快照对比,只解析新增或变化文件; + 3. 在单个事务中删除旧符号/引用并写入新结果; + 4. 对已删除文件级联清理关联记录; + 5. 保存本轮 revision 和索引时间,供查询与失效判断使用。 +- 在 SQLite 中建立 `files`、`symbols`、`references`、`imports` 和 `index_snapshots` 等事实表,并提供 `search_symbol`、`find_references`、`module_summary`、`related_files` 等最小查询接口;继续保留 `Glob`、`Grep`、`ReadFile` 作为最终源码核验手段。 +- 将 Tree-sitter 限定为结构提取器,不把静态分析结果包装成绝对正确的调用图;无法静态解析的动态调用只返回候选关系,并附带文件、行号、commit 和 blob 来源。 + +### Phase 3:实现 Persistent Memory 与 Git-aware 失效机制 + +- 在同一个项目级 SQLite 数据库中新增记忆表,但与 RepoIndex 事实表保持逻辑隔离;使用 Pydantic 定义 `MemoryRecord` 和 `CodeReference`,将记忆限定为四类: + - `decision`:架构决策、采用原因和替代方案; + - `constraint`:兼容性、安全和工程约束; + - `preference`:用户或团队的稳定编码偏好; + - `lesson`:经过验证的成功方案和失败经验。 +- 复用 Session ID、Turn/Hook 事件和 Git diff 作为来源信息。每轮结束只生成“候选记忆”,只有通过测试结果、代码 diff 或用户确认后才升级为长期记忆;过滤临时猜测、大段工具输出、可由 AST 恢复的源码和疑似敏感信息。 +- 使用 SQLite FTS5 为记忆正文、标签、符号名和路径建立关键词索引,先以可离线、可解释、易调试的召回方式建立基线;只有评测证明语义召回不足时再增加 embedding,避免首期引入额外服务和双索引一致性问题。 +- 为每条代码相关记忆绑定 `source_session_id`、`git_commit`、`blob_hash`、文件路径、符号限定名、验证方式和置信度;召回时与当前 RepoIndex 交叉校验: + - 文件、blob 与符号均未变化,标为 `valid`; + - 文件发生变化但符号仍存在,标为 `possibly_stale`; + - 文件或符号已经删除,标为 `stale`; + - 当前代码事实与历史结论相反,标为 `conflicted`。 +- 不静默丢弃过期或冲突记忆,而是降低排序权重并把“历史结论—当前证据—状态”一起交给 Agent,使其能回到源码继续核验。 + +### Phase 4:实现 Knowledge Orchestrator 与临时 Context Pack + +- 新增 `KnowledgeOrchestrator` 作为 RepoIndex 与 MemoryStore 的唯一查询入口,根据请求类型选择数据源:代码定位以 RepoIndex 为主,偏好或历史决策以 MemoryStore 为主,设计和修改任务同时查询两者。 +- 使用 `asyncio.gather()` 并行召回代码事实与历史记忆,再执行统一的结果管线: + 1. 按路径、符号和规范化文本去重; + 2. 使用当前 Git/blob/符号信息校验记忆时效; + 3. 对当前事实与历史决策进行冲突标记; + 4. 综合相关性、来源可信度、时效状态和任务类型排序; + 5. 按 Token 预算裁剪并生成带来源引用的 Context Pack。 +- 复用 `KimiSoul` 已有的动态注入扩展点和 `effective_history` 组装逻辑,但调整注入语义:Context Pack 只加入本次 LLM 请求使用的历史副本,不调用 `Context.append_message()` 写入原始 `context.jsonl`。原始 Context 仍只保存用户、Assistant 和 Tool 的真实交互,避免恢复或压缩后重复加载旧 Repo 快照。 +- 为 Context Pack 设置分区预算,例如当前代码事实占 50%、架构决策与约束占 30%、历史验证经验占 20%;单条结果只注入摘要和来源位置,Agent 需要细节时继续调用文件工具读取原文。 +- 通过 Wire 新增知识查询开始/结束、来源数量、失效状态、注入 Token 和检索耗时事件,使 Shell、Print、ACP 等前端可以选择展示或忽略,而无需改变核心查询逻辑。 + +### Phase 5:在现有子 Agent 之上增加 Agent Teams 确定性编排层 + +- 新增 `Team`、`Member`、`TeamTask` 和 `Dependency` 领域模型:Team 记录目标与总预算,Member 绑定既有 `agent_id` 和角色,TeamTask 记录输入、依赖、负责人、尝试次数、预算、状态、结果与失败原因。 +- 将任务状态机限定为显式迁移,例如 `pending → ready → running → completed/failed/cancelled/blocked`;创建或更新 DAG 时执行环检测,只有全部前置任务成功且预算、并发槽位、写租约均满足时,Scheduler 才能把任务从 `pending` 推进到 `ready`。 +- 复用 `SubagentStore` 创建和恢复成员实例,复用 `BackgroundTaskManager.create_agent_task()` 执行已就绪任务,复用既有超时、心跳、输出、kill 和完成通知;Team Scheduler 不重新实现 Agent Runner,只负责把 DAG 中的逻辑任务映射到已有后台 Agent Task,并记录二者 ID 的对应关系。 +- 复用 `ApprovalRuntime` 的来源模型,将审批绑定到具体 `team_id`、`team_task_id` 和 `agent_id`;当任务取消、成员退出或 Team 终止时,沿用按来源取消待处理审批的机制,防止孤儿审批继续阻塞运行时。 +- 复用 Root Wire Hub 汇总子 Agent 事件,新增 Team/Task/Member 状态变化事件;前端只消费事件,不参与调度决策,从而保持 Shell、Print、ACP、Wire 多入口行为一致。 + +### Phase 6:补齐 Mailbox、Artifact、预算和写冲突控制 + +- 新增持久化 Mailbox,为每条消息记录发送者、接收者、关联任务、消息类型、正文、创建时间和消费状态;成员完成探索或发现阻塞时写入结构化消息,Scheduler 通过事件唤醒目标成员或 Root,而不是把所有协作信息都回灌到 Root Agent 的上下文。 +- 新增 Artifact Index,只记录工件类型、生产者、关联任务、文件路径或内容 hash、摘要和验证状态;代码、测试报告和设计文档仍保存在工作区或现有输出文件中,索引层不重复存储大块内容。 +- 建立分层预算控制:Team 保存总 Token/时间/重试预算,Task 获得子预算,Member 每次执行实时回报消耗;创建任务和重试前做预算预留,完成后结算,超限时停止派发新任务并触发取消传播。 +- 实现失败与取消传播规则:前置任务失败后,下游强依赖任务转为 `blocked`;成员超时或丢失时先回收租约,再根据重试策略重新排队或标记失败;用户取消 Team 时,由 Scheduler 终止运行任务、取消关联审批并阻止新的 Task 进入 `ready`。 +- MVP 采用单写者租约:`explore` 和 `reviewer` 可并行只读,只有一个 `coder` 能获得工作区写租约;租约包含 owner、过期时间和心跳,成员异常退出后可回收。后续再扩展为独立 git worktree 并行修改与集成任务合并。 + +### Phase 7:实现持久化恢复、测试矩阵与效果评测 + +- 将 Team、Task DAG、Mailbox、Artifact、预算账本和租约写入 Session 目录下的独立状态存储;所有状态更新采用 SQLite 事务或项目已有的原子写模式,避免进程中断后出现半写状态。 +- 会话恢复时执行 `load → reconcile → schedule`:读取持久化 Team 状态,对照 `SubagentStore`、后台 Task 心跳和终态修正 `running` 任务;回收失效租约;把不可恢复任务标为 `lost` 或重新排队;最后重新计算 DAG 就绪集合,而不是盲目重跑全部任务。 +- 为知识平面覆盖首次索引、单文件修改、文件删除、分支切换、脏工作区、符号重命名、FTS5 召回、过期记忆、冲突记忆、数据库损坏与并发刷新。 +- 为协作平面覆盖 DAG 环检测、依赖就绪、并发上限、成员异常退出、依赖失败、预算耗尽、审批取消、重复通知、租约回收、并发写冲突和重启恢复。 +- 建设固定仓库、固定任务、固定模型、固定预算和固定重复次数的 baseline/candidate 脚本,采集首次定位正确文件耗时、`Grep`/`ReadFile` 调用数、输入 Token、成功率、过期记忆误用率、Team 关键路径耗时、重复工作率和冲突率;保留原始 Trace 和失败样本,不用预期值代替实测结果。 + +--- + +## 复用了什么,新增了什么 + +| 层次 | 复用的现有能力 | 本项目新增内容 | +| --- | --- | --- | +| 应用装配 | `KimiCLI.create()`、`Runtime.create()`、工作目录和配置 | 项目级 `RepoIndex`、`MemoryStore`、`KnowledgeOrchestrator`、`TeamScheduler` 的生命周期装配 | +| 会话与上下文 | `Session`、`Context`、`context.jsonl`、`SessionState` | 跨会话项目知识库;请求级 Context Pack;Team/DAG/Mailbox/Artifact 持久化 | +| 代码理解 | `Glob`、`Grep`、`ReadFile`、现有 Git 上下文 | Tree-sitter 符号/引用/依赖索引;按 commit、blob 和内容 hash 增量更新 | +| 子 Agent | `LaborMarket`、`AgentTool`、`SubagentStore`、前后台 Runner、恢复能力 | Team/Member 领域模型;Task 到 Agent 实例的绑定;成员协作协议 | +| 后台执行 | `BackgroundTaskManager`、Task 状态、输出、心跳、超时、kill、通知 | DAG 就绪调度、重试、依赖失败传播、分层预算和崩溃恢复协调 | +| 安全审批 | `ApprovalRuntime`、来源绑定、Root Wire 投影、按来源取消 | 审批来源扩展到 team/task/member;Team 取消时级联清理 | +| 可观测性 | `Wire`、Root Wire Hub、JSONL 记录、Shell/Print/ACP 消费端 | Knowledge 与 Team 事件、召回来源、预算、关键路径和恢复 Trace | +| 数据与一致性 | Pydantic 模型、原子 JSON 写入模式 | SQLite/FTS5 Schema、事务化索引刷新、记忆失效状态、写租约 | + +> **核心取舍**:复用成熟的 Agent 执行、审批和 UI 管道,只新增知识与编排控制面;代码事实、历史记忆、任务状态分别由确定性数据结构维护,LLM 不作为唯一数据库或状态机。 + +--- + +## 改造后的整体框架流程 + +```mermaid +flowchart TD + U["用户请求"] --> KS["KimiSoul 接收本轮输入"] + + KS --> KO["Knowledge Orchestrator"] + KO --> RI["Repo Intelligence"] + KO --> PM["Persistent Memory"] + RI --> RV["Git/blob/符号校验"] + PM --> RV + RV --> CP["去重、冲突标记、排序、Token 裁剪"] + CP --> EH["临时 Context Pack + effective_history"] + + EH --> LEAD["Root Agent / Team Lead"] + LEAD --> TS["Team Scheduler"] + TS --> DAG["计算 Task DAG 就绪集合"] + DAG --> BC["预算、并发槽位、写租约检查"] + BC --> SA["复用 SubagentStore 创建或恢复成员"] + SA --> BG["复用 BackgroundTaskManager 执行"] + + BG --> MB["Mailbox 消息"] + BG --> AI["Artifact Index"] + BG --> AP["ApprovalRuntime"] + BG --> WE["Wire / Root Wire Hub"] + + MB --> TS + AI --> TS + AP --> BG + WE --> UI["Shell / Print / ACP / Wire UI"] + + BG --> RC{"任务结果"} + RC -->|"成功"| NEXT["释放下游依赖并继续调度"] + RC -->|"失败/超限/取消"| FAIL["重试或失败/取消级联"] + NEXT --> TS + FAIL --> TS + + RC --> MW["候选 Memory Writer"] + MW --> VERIFY["测试 / Git diff / 用户确认"] + VERIFY --> PM +``` + +新的端到端流程可以概括为: + +```text +用户请求 + → 并行召回当前代码事实与历史工程记忆 + → 用 Git/blob/符号校验时效,完成去重、冲突标记和 Token 裁剪 + → 将 Context Pack 临时加入本次模型请求 + → Root Agent 生成或更新任务 DAG + → Scheduler 根据依赖、预算、并发槽位和写租约派发任务 + → 复用现有子 Agent、后台任务、审批与 Wire 完成执行 + → 通过 Mailbox 传递结构化结果,通过 Artifact Index 共享工件引用 + → 成功时释放下游依赖;失败时重试、阻塞或级联取消 + → 经测试、Git diff 或用户确认后沉淀新的项目记忆 + → 会话重启时从持久化状态 reconcile 后继续调度 +``` + +--- + +## R — 成果 Result + +### 可在闭环完成后陈述的工程结果 + +- 将原有“单会话历史 + 临时文件搜索”扩展为项目级知识平面,使当前代码结构与跨会话工程决策能够统一召回,同时保留来源、版本和失效状态。 +- 将原有“Root Agent 并发启动独立子 Agent”扩展为可持久化的协作编排平面,使复杂目标能够通过 Task DAG、Mailbox、Artifact、预算和写租约受控执行。 +- 通过复用 Runtime、Subagent、Background、Approval 和 Wire,避免维护第二套 Agent 生命周期与 UI 协议,将新增复杂度集中在知识一致性和协作状态机。 +- 建立覆盖分支切换、代码删除、记忆过期、Agent 异常退出、依赖失败、预算耗尽和并发写冲突的测试矩阵,并形成可复现的 baseline/candidate 评测流程。 + +### 必须用实测填写的效果指标 + +| 指标 | Baseline | Candidate | 说明 | +| --- | ---: | ---: | --- | +| 首次定位正确文件耗时 | `[待测]` | `[待测]` | RepoIndex 是否减少盲目探索 | +| `Grep` / `ReadFile` 调用数 | `[待测]` | `[待测]` | 符号索引是否减少重复读取 | +| 输入 Token 消耗 | `[待测]` | `[待测]` | Context Pack 是否降低总上下文成本 | +| 最终任务成功率 | `[待测]` | `[待测]` | 成本优化不能牺牲正确率 | +| 过期记忆误用率 | `[待测]` | `[待测]` | Git-aware 失效机制是否有效 | +| Team 关键路径耗时 | `[待测]` | `[待测]` | 并行调度是否真正缩短交付时间 | +| 重复工作率 | `[待测]` | `[待测]` | Mailbox 与任务分配是否有效 | +| 并发写冲突率 | `[待测]` | `[待测]` | 单写者租约是否可靠 | + +> 在得到固定任务集的真实结果前,简历中只写“实现闭环并建立评测”,不写未经验证的百分比。 + +--- + +## 可直接投递的简历版本 + +### 项目描述 + +Kimi Code CLI 是支持会话恢复、工具调用、MCP、子 Agent、后台任务和多前端的开源终端 Coding Agent。本项目针对大型仓库中新会话重复探索、历史决策无法复用以及多个子 Agent 缺少任务依赖和协作状态的问题,在不重写现有 Agent Runtime 的前提下,增加项目级知识平面与 Agent Teams 编排平面。 + +### 个人工作 + +- 梳理 `CLI → Runtime → KimiSoul → Context/Toolset → Wire/UI` 及子 Agent、后台任务、审批调用链,复用现有 Session、SubagentStore、BackgroundTaskManager、ApprovalRuntime 与 Wire,确定在 Runtime 生命周期和模型请求前后增加知识与编排控制面。 +- 使用 Tree-sitter 与 SQLite/FTS5 构建 Python 文件、符号、引用和模块依赖的增量索引,以 Git commit、blob hash 和符号存在性校验架构决策、工程约束、编码偏好和验证经验,支持 `valid`、`possibly_stale`、`stale`、`conflicted` 四级记忆状态。 +- 实现 Knowledge Orchestrator,并行召回代码事实与跨会话记忆,完成去重、冲突标记、来源追踪和 Token 预算裁剪;将 Context Pack 仅注入本次 `effective_history`,避免过期 Repo 快照污染原始会话历史。 +- 在现有子 Agent 与后台任务之上增加 Team/Member/Task DAG 状态机、Mailbox、Artifact Index、分层预算、取消传播和单写者租约,支持依赖就绪调度、成员异常恢复及审批来源追踪。 +- 建设覆盖分支切换、符号删除、过期记忆、DAG 环、依赖失败、预算耗尽、Agent 异常退出和并发写冲突的测试矩阵,并以固定任务集对比定位耗时、探索调用、Token、成功率和 Team 关键路径耗时。 + +> 如果目前只完成设计,应把上述动词统一改为“设计”,并删除尚无代码和测试支撑的实现细节;如果只完成知识层或 Teams 中的一条闭环,则只保留对应的两至三条个人工作。 + +--- + +## 面试核心表达 + +> 我没有重写 Kimi Code CLI 的 Agent 执行框架,而是先从源码划清边界:Session 解决单会话恢复,Subagent 和 Background 解决独立任务执行,Approval 与 Wire 解决安全交互和可观测性;缺少的是项目级知识和确定性协作状态机。因此我在 Runtime 中装配 RepoIndex、MemoryStore、KnowledgeOrchestrator 和 TeamScheduler。每轮先并行召回代码事实与历史记忆,用 Git/blob/符号做时效校验,再把受 Token 预算约束的 Context Pack 临时送入模型;Root Agent 生成 DAG 后,Scheduler 根据依赖、预算和写租约复用现有后台 Agent 执行,并通过 Mailbox、Artifact 和 Wire 收敛结果。这样新增的是知识与编排控制面,原有 LLM、工具、审批、会话和 UI 管道都继续复用。 + +--- + +## 关键代码与拟接入位置 + +| 模块 | 现有路径 | 在新架构中的作用 | +| --- | --- | --- | +| 应用装配 | `src/kimi_cli/app.py` | 创建 Session、Runtime、Agent、Context 和 KimiSoul;装配新增项目级依赖 | +| Runtime | `src/kimi_cli/soul/agent.py` | 持有共享知识服务和 Team Scheduler;复制子 Agent Runtime | +| 主循环 | `src/kimi_cli/soul/kimisoul.py` | 在 LLM 请求前组装临时 Context Pack,在回合后触发候选记忆 | +| 动态注入 | `src/kimi_cli/soul/dynamic_injection.py` | 复用 Provider 扩展方式,但将知识内容保持为请求级注入 | +| 会话持久化 | `src/kimi_cli/session.py`、`src/kimi_cli/soul/context.py` | 保持原始交互历史与 Session 状态职责不变 | +| 子 Agent 持久化 | `src/kimi_cli/subagents/store.py` | 创建、恢复和追踪 Team Member 对应的 Agent 实例 | +| 子 Agent 执行 | `src/kimi_cli/tools/agent/`、`src/kimi_cli/subagents/runner.py` | 执行 Scheduler 已派发的成员任务 | +| 后台任务 | `src/kimi_cli/background/` | 复用异步执行、状态、心跳、输出、超时、终止和通知 | +| 审批 | `src/kimi_cli/approval_runtime/` | 将高风险操作追踪到 Team、Task 和 Agent,并支持级联取消 | +| 事件与 UI | `src/kimi_cli/wire/`、`src/kimi_cli/ui/` | 记录和展示知识召回、Team 状态、预算与恢复事件 | +| Repo Intelligence | `拟新增:src/kimi_cli/knowledge/repo_index/` | Tree-sitter 增量索引、符号查询和 Git-aware 版本管理 | +| Persistent Memory | `拟新增:src/kimi_cli/knowledge/memory/` | SQLite/FTS5 记忆、来源、验证和失效状态 | +| Knowledge Orchestrator | `拟新增:src/kimi_cli/knowledge/orchestrator.py` | 查询规划、并行召回、冲突处理与 Context Pack | +| Agent Teams | `拟新增:src/kimi_cli/teams/` | Team/DAG、Mailbox、Artifact、预算、租约与恢复状态机 | + +--- + +## 真实性检查清单 + +- [ ] “实现”“优化”“提升”等动词均有代码、测试、提交或 PR 支撑 +- [ ] Repo Intelligence、Persistent Memory、Knowledge Orchestrator 和 Agent Teams 的完成状态与仓库一致 +- [ ] Runtime、Session、Subagent、Background、Approval、Wire 明确写为复用的上游能力 +- [ ] Context Pack 的实际实现不会永久写入 `context.jsonl` +- [ ] 百分比来自固定任务集的重复实验,并保留原始 Trace 与失败样本 +- [ ] 能指出每条简历 bullet 对应的源码、测试和提交 +- [ ] 能解释至少一个失效记忆、任务恢复或写冲突失败案例 +- [ ] 没有把应用层审批描述为 OS 级沙箱 +- [ ] 没有把现有独立后台 Agent 描述成已经具备完整 Teams 编排 +- [ ] 没有把整个开源仓库的代码量、测试量或 Star 数当作个人贡献 diff --git a/src/kimi_cli/app.py b/src/kimi_cli/app.py index 41127aae98..e996fc61a1 100644 --- a/src/kimi_cli/app.py +++ b/src/kimi_cli/app.py @@ -136,8 +136,8 @@ def _cleanup_stale_foreground_subagents(runtime: Runtime) -> None: class KimiCLI: """一次可运行的 agent 实例;由 CLI 层构造,UI 层持有并调用其 run_* 方法。""" - @staticmethod - async def create( + @staticmethod + async def create( # Agent启动的入口 session: Session, *, # Basic configuration @@ -365,7 +365,7 @@ async def create( await context.write_system_prompt(agent.system_prompt) # ==== 阶段 11:构造 KimiSoul(主循环) ==== - soul = KimiSoul(agent, context=context) + soul = KimiSoul(agent, context=context) # * 核心 # ==== 阶段 12:激活 plan mode(如请求) ==== # Activate plan mode if requested (for new sessions or --plan flag) diff --git a/src/kimi_cli/auth/oauth.py b/src/kimi_cli/auth/oauth.py index 5000b532c0..a56388d732 100644 --- a/src/kimi_cli/auth/oauth.py +++ b/src/kimi_cli/auth/oauth.py @@ -47,6 +47,19 @@ from kimi_cli.soul.agent import Runtime +# ───────────────────────────────────────────────────────────────────────────── +# 模块概述 +# ───────────────────────────────────────────────────────────────────────────── +# 本模块实现 Kimi Code CLI 的 OAuth 设备授权(Device Flow)登录与令牌管理。 +# 职责: +# 1. 设备授权登录(login_kimi_code)与登出(logout_kimi_code),异步生成器逐步下发事件。 +# 2. 令牌持久化:文件(推荐)与 keyring(弃用,自动迁移),原子写 + 600 权限。 +# 3. 令牌刷新(OAuthManager.ensure_fresh / _refresh_tokens):临近过期自动刷新, +# 用进程内 asyncio.Lock + 跨进程文件锁协调多实例并发,避免重复刷新。 +# 4. 401 处理与拒绝记忆(_REJECTED_REFRESH_TOKENS tombstone),避免死循环重试。 +# ───────────────────────────────────────────────────────────────────────────── + + KIMI_CODE_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098" KIMI_CODE_OAUTH_KEY = "oauth/kimi-code" DEFAULT_OAUTH_HOST = "https://auth.kimi.com" @@ -59,6 +72,7 @@ _RETRYABLE_REFRESH_STATUSES = {429, 500, 502, 503, 504} +# 动态刷新阈值(秒):取 max(300, expires_in * 0.5),避免过早刷新。 def _refresh_threshold(expires_in: float) -> float: """Return the dynamic refresh threshold in seconds.""" if expires_in > 0: @@ -66,18 +80,22 @@ def _refresh_threshold(expires_in: float) -> float: return MIN_REFRESH_THRESHOLD_SECONDS +# OAuth 流程错误基类。 class OAuthError(RuntimeError): """OAuth flow error.""" +# OAuth 凭据被拒绝。 class OAuthUnauthorized(OAuthError): """OAuth credentials rejected.""" +# 刷新时遇到瞬态 HTTP 错误(5xx / 429)。 class _RetryableRefreshError(OAuthError): """Transient HTTP error during token refresh (5xx / 429).""" +# 设备授权过期。 class OAuthDeviceExpired(OAuthError): """Device authorization expired.""" @@ -85,6 +103,7 @@ class OAuthDeviceExpired(OAuthError): OAuthEventKind = Literal["info", "error", "waiting", "verification_url", "success"] +# 登录/登出流程中下发给 UI 的事件(类型 + 消息 + 可选数据)。 @dataclass(slots=True, frozen=True) class OAuthEvent: type: OAuthEventKind @@ -102,6 +121,7 @@ def json(self) -> str: return json.dumps(payload, ensure_ascii=False) +# OAuth 令牌:access/refresh token、过期时间、scope、token 类型等。 @dataclass(slots=True) class OAuthToken: access_token: str @@ -111,6 +131,7 @@ class OAuthToken: token_type: str expires_in: float = 0.0 + # 从 token 响应 JSON 构造(expires_at = 当前时间 + expires_in)。 @classmethod def from_response(cls, payload: dict[str, Any]) -> OAuthToken: expires_in = float(payload["expires_in"]) @@ -133,6 +154,7 @@ def to_dict(self) -> dict[str, Any]: "expires_in": self.expires_in, } + # 从持久化 JSON 构造(字段缺失时容错为默认值)。 @classmethod def from_dict(cls, payload: dict[str, Any]) -> OAuthToken: expires_at_value = payload.get("expires_at") @@ -146,6 +168,7 @@ def from_dict(cls, payload: dict[str, Any]) -> OAuthToken: ) +# 被服务器拒绝的 refresh token 状态(含可重试时间点)。 @dataclass(slots=True) class _RejectedRefreshState: refresh_token: str @@ -165,9 +188,11 @@ class _RejectedRefreshState: # independently on its first attempt, and the tombstone auto-clears when the # on-disk refresh_token differs from the rejected one (i.e. another process # successfully rotated, or /login atomically rewrote the file). +# 进程级"拒绝记忆":记录最近被服务器拒绝的 refresh token,避免反复用同一失效 token 重试。 _REJECTED_REFRESH_TOKENS: dict[str, _RejectedRefreshState] = {} +# 设备授权流程响应(user_code / device_code / 验证 URI / 过期与轮询间隔)。 @dataclass(slots=True) class DeviceAuthorization: user_code: str @@ -178,19 +203,23 @@ class DeviceAuthorization: interval: int +# 返回 OAuth 服务地址(环境变量优先,默认 DEFAULT_OAUTH_HOST)。 def _oauth_host() -> str: return os.getenv("KIMI_CODE_OAUTH_HOST") or os.getenv("KIMI_OAUTH_HOST") or DEFAULT_OAUTH_HOST +# 设备 id 文件路径。 def _device_id_path() -> Path: return get_share_dir() / "device_id" +# 将文件权限设为 600(仅属主可读写)。 def _ensure_private_file(path: Path) -> None: with suppress(OSError): os.chmod(path, 0o600) +# 生成设备型号字符串(macOS / Windows / 其他,含版本与架构)。 def _device_model() -> str: system = platform.system() arch = platform.machine() or "" @@ -225,6 +254,7 @@ def _device_model() -> str: return "Unknown" +# 读取或生成持久化设备 id;首次生成时埋点 first_launch。 def get_device_id() -> str: path = _device_id_path() if path.exists(): @@ -238,6 +268,7 @@ def get_device_id() -> str: return device_id +# 把 header 值转 ASCII(无法编码时忽略非 ASCII 字符,空则回退 fallback)。 def _ascii_header_value(value: str, *, fallback: str = "unknown") -> str: try: value.encode("ascii") @@ -247,6 +278,7 @@ def _ascii_header_value(value: str, *, fallback: str = "unknown") -> str: return sanitized or fallback +# 构造公共请求头(平台/版本/设备名/型号/系统版本/设备 id)。 def _common_headers() -> dict[str, str]: device_name = platform.node() or socket.gethostname() device_model = _device_model() @@ -261,22 +293,26 @@ def _common_headers() -> dict[str, str]: return {key: _ascii_header_value(value) for key, value in headers.items()} +# 凭据目录(share/credentials)。 def _credentials_dir() -> Path: path = get_share_dir() / "credentials" path.mkdir(parents=True, exist_ok=True) return path +# 凭据文件路径(.json)。 def _credentials_path(key: str) -> Path: name = key.removeprefix("oauth/").split("/")[-1] or key return _credentials_dir() / f"{name}.json" +# 跨进程锁文件路径(.lock)。 def _credentials_lock_path(key: str) -> Path: name = key.removeprefix("oauth/").split("/")[-1] or key return _credentials_dir() / f"{name}.lock" +# 跨进程文件锁:Unix 用 fcntl.flock,Windows 用 msvcrt.locking,协调多个 kimi-cli 实例的刷新。 class _CrossProcessLock: """File-based lock that coordinates token refresh across kimi-cli processes. @@ -287,6 +323,7 @@ def __init__(self, key: str) -> None: self._path = _credentials_lock_path(key) self._fd: int | None = None + # 尝试获取锁:成功返回 True,竞争返回 False,无法打开锁文件抛 OSError。 def _acquire(self) -> bool: """Acquire the lock. @@ -313,6 +350,7 @@ def _acquire(self) -> bool: self._fd = None return False + # 释放锁。 def release(self) -> None: if self._fd is not None: try: @@ -327,6 +365,7 @@ def release(self) -> None: os.close(self._fd) self._fd = None + # 带重试地异步获取锁:多次尝试,间隔随机退避;打不开锁文件则永久失败回退为无锁刷新。 async def acquire_with_retry(self) -> bool: for _attempt in range(_CROSS_PROCESS_LOCK_RETRIES): try: @@ -343,6 +382,7 @@ async def acquire_with_retry(self) -> bool: except OSError: return False + # 异步上下文管理器入口:返回是否成功拿到锁。 async def __aenter__(self) -> bool: return await self.acquire_with_retry() @@ -350,6 +390,7 @@ async def __aexit__(self, *args: object) -> None: self.release() +# 从 keyring 读取 token(旧存储方式)。 def _load_from_keyring(key: str) -> OAuthToken | None: try: raw = keyring.get_password(KEYRING_SERVICE, key) @@ -368,6 +409,7 @@ def _load_from_keyring(key: str) -> OAuthToken | None: return OAuthToken.from_dict(payload) +# 从 keyring 删除 token。 def _delete_from_keyring(key: str) -> None: try: keyring.delete_password(KEYRING_SERVICE, key) @@ -375,6 +417,7 @@ def _delete_from_keyring(key: str) -> None: return +# 从文件读取 token(缺失/损坏返回 None)。 def _load_from_file(key: str) -> OAuthToken | None: path = _credentials_path(key) if not path.exists(): @@ -389,6 +432,7 @@ def _load_from_file(key: str) -> OAuthToken | None: return OAuthToken.from_dict(payload) +# 原子写 token 到文件:临时文件 + fsync + os.replace + chmod 600,失败清理临时文件。 def _save_to_file(key: str, token: OAuthToken) -> None: path = _credentials_path(key) fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp") @@ -412,12 +456,14 @@ def _save_to_file(key: str, token: OAuthToken) -> None: raise +# 删除 token 文件。 def _delete_from_file(key: str) -> None: path = _credentials_path(key) if path.exists(): path.unlink() +# 加载 token:优先文件;keyring 旧存储则读取后迁移到文件并删除 keyring 副本。 def load_tokens(ref: OAuthRef) -> OAuthToken | None: file_token = _load_from_file(ref.key) if file_token is not None: @@ -437,6 +483,7 @@ def load_tokens(ref: OAuthRef) -> OAuthToken | None: return token +# 保存 token(keyring 已弃用,统一存文件),返回可能修正后的 OAuthRef。 def save_tokens(ref: OAuthRef, token: OAuthToken) -> OAuthRef: if ref.storage == "keyring": logger.warning("Keyring storage is deprecated; saving OAuth tokens to file.") @@ -445,12 +492,14 @@ def save_tokens(ref: OAuthRef, token: OAuthToken) -> OAuthRef: return ref +# 删除 token(同时清理 keyring 与文件)。 def delete_tokens(ref: OAuthRef) -> None: if ref.storage == "keyring": _delete_from_keyring(ref.key) _delete_from_file(ref.key) +# 发起设备授权请求,返回 DeviceAuthorization。 async def request_device_authorization() -> DeviceAuthorization: async with ( new_client_session() as session, @@ -474,6 +523,7 @@ async def request_device_authorization() -> DeviceAuthorization: ) +# 轮询设备授权 token 端点,返回 (HTTP 状态, 响应数据);5xx 直接抛错。 async def _request_device_token(auth: DeviceAuthorization) -> tuple[int, dict[str, Any]]: try: async with ( @@ -500,6 +550,7 @@ async def _request_device_token(auth: DeviceAuthorization) -> tuple[int, dict[st return status, data +# 用 refresh_token 换新 token:最多重试 max_retries 次,401/403 抛 OAuthUnauthorized。 async def refresh_token(refresh_token: str, *, max_retries: int = 3) -> OAuthToken: last_exc: Exception | None = None for attempt in range(max_retries): @@ -546,6 +597,7 @@ async def refresh_token(refresh_token: str, *, max_retries: int = 3) -> OAuthTok raise OAuthError("Token refresh failed after retries.") from last_exc +# 选择默认模型与思考开关:取第一个模型,依据 capabilities 判断是否 thinking。 def _select_default_model_and_thinking(models: list[ModelInfo]) -> tuple[ModelInfo, bool] | None: if not models: return None @@ -555,6 +607,7 @@ def _select_default_model_and_thinking(models: list[ModelInfo]) -> tuple[ModelIn return selected_model, thinking +# 把登录结果写回 config:注册 provider、替换 kimi-code 模型、设置默认模型与思考、配置搜索/抓取服务。 def _apply_kimi_code_config( config: Config, *, @@ -607,6 +660,7 @@ def _apply_kimi_code_config( ) +# 设备授权登录流程:异步生成器,逐步 yield 事件(错误/验证链接/等待/成功)。 async def login_kimi_code( config: Config, *, open_browser: bool = True ) -> AsyncIterator[OAuthEvent]: @@ -713,6 +767,7 @@ async def login_kimi_code( return +# 登出流程:删除 token 与 config 中的 provider/model/服务配置。 async def logout_kimi_code(config: Config) -> AsyncIterator[OAuthEvent]: if not config.is_from_default_location: yield OAuthEvent( @@ -747,6 +802,7 @@ async def logout_kimi_code(config: Config) -> AsyncIterator[OAuthEvent]: return +# OAuth 管理器:缓存 access token,用进程内锁 + 跨进程锁协调刷新,并把 token 注入运行时 LLM client。 class OAuthManager: def __init__(self, config: Config) -> None: self._config = config @@ -756,6 +812,7 @@ def __init__(self, config: Config) -> None: self._migrate_oauth_storage() self._load_initial_tokens() + # 遍历配置中所有 OAuth 引用(provider + search/fetch 服务)。 def _iter_oauth_refs(self) -> list[OAuthRef]: refs: list[OAuthRef] = [] for provider in self._config.providers.values(): @@ -769,6 +826,7 @@ def _iter_oauth_refs(self) -> list[OAuthRef]: refs.append(service.oauth) return refs + # 把 keyring 存储迁移到文件,必要时保存 config。 def _migrate_oauth_storage(self) -> None: migrated_keys: set[str] = set() changed = False @@ -797,12 +855,14 @@ def _migrate_ref(ref: OAuthRef) -> OAuthRef: if changed and self._config.is_from_default_location: save_config(self._config) + # 启动时加载并缓存 token(被拒的跳过)。 def _load_initial_tokens(self) -> None: for ref in self._iter_oauth_refs(): token = load_tokens(ref) if token and not self._should_suppress_persisted_token(ref, token): self._cache_access_token(ref, token) + # 查询某 refresh token 是否在拒绝 tombstone 中(token 已轮换则清除记忆)。 def _rejected_refresh_state( self, ref: OAuthRef, refresh_token: str | None ) -> _RejectedRefreshState | None: @@ -814,13 +874,16 @@ def _rejected_refresh_state( return None return state + # 是否应抑制持久化 token(该 refresh token 在拒绝列表内)。 def _should_suppress_persisted_token(self, ref: OAuthRef, token: OAuthToken) -> bool: return self._rejected_refresh_state(ref, token.refresh_token) is not None + # 被拒 token 是否已过冷却期可重试。 def _can_retry_rejected_refresh_token(self, ref: OAuthRef, refresh_token: str | None) -> bool: state = self._rejected_refresh_state(ref, refresh_token) return state is None or time.time() >= state.retry_after + # 将 refresh token 记入拒绝 tombstone 并设冷却时间(默认 300s)。 def _mark_refresh_token_rejected(self, ref: OAuthRef, refresh_token: str) -> None: if not refresh_token: return @@ -829,22 +892,27 @@ def _mark_refresh_token_rejected(self, ref: OAuthRef, refresh_token: str) -> Non retry_after=time.time() + UNAUTHORIZED_REFRESH_RETRY_COOLDOWN_SECONDS, ) + # 清除某 key 的拒绝 tombstone。 def _clear_rejected_refresh_token(self, ref: OAuthRef) -> None: _REJECTED_REFRESH_TOKENS.pop(ref.key, None) + # 缓存 access token(空则清除缓存)。 def _cache_access_token(self, ref: OAuthRef, token: OAuthToken) -> None: if not token.access_token: self._access_tokens.pop(ref.key, None) return self._access_tokens[ref.key] = token.access_token + # 取缓存 access token(无则 None)。 def get_cached_access_token(self, key: str) -> str | None: """Get a cached access token by key, or None if not available.""" return self._access_tokens.get(key) + # 公共请求头。 def common_headers(self) -> dict[str, str]: return _common_headers() + # 解析 api key:优先 OAuth access token,否则回退配置的静态 api_key。 def resolve_api_key(self, api_key: SecretStr, oauth: OAuthRef | None) -> str: if oauth: token = self._access_tokens.get(oauth.key) @@ -862,6 +930,7 @@ def resolve_api_key(self, api_key: SecretStr, oauth: OAuthRef | None) -> str: ) return api_key.get_secret_value() + # 找到 kimi-code 的 OAuthRef(provider 或 search/fetch 服务)。 def _kimi_code_ref(self) -> OAuthRef | None: provider_key = managed_provider_key(KIMI_CODE_PLATFORM_ID) provider = self._config.providers.get(provider_key) @@ -875,6 +944,7 @@ def _kimi_code_ref(self) -> OAuthRef | None: return service.oauth return None + # 加载持久化 token、缓存、临近过期则刷新;runtime 传入时原地更新 LLM client 的 key。 async def ensure_fresh(self, runtime: Runtime | None = None, *, force: bool = False) -> None: """Load persisted tokens, cache them, and refresh if close to expiry. @@ -904,6 +974,7 @@ async def ensure_fresh(self, runtime: Runtime | None = None, *, force: bool = Fa self._apply_access_token(runtime, token.access_token) await self._refresh_tokens(ref, token, runtime, force=force) + # 异步上下文管理器:周期刷新 token(处理睡眠唤醒),退出时取消后台刷新任务。 @asynccontextmanager async def refreshing(self, runtime: Runtime) -> AsyncIterator[None]: stop_event = asyncio.Event() @@ -948,6 +1019,7 @@ async def _runner() -> None: with suppress(asyncio.CancelledError): await refresh_task + # 核心刷新逻辑:进程内锁 + 跨进程锁,多重检查后 refresh_token 换新,处理 401 与网络错误。 async def _refresh_tokens( self, ref: OAuthRef, @@ -1070,6 +1142,7 @@ async def _refresh_tokens( finally: xlock.release() + # 把 access token 应用到运行时 LLM client(Kimi provider;空则回退静态 api_key)。 def _apply_access_token(self, runtime: Runtime | None, access_token: str) -> None: if runtime is None: return diff --git a/src/kimi_cli/soul/__init__.py b/src/kimi_cli/soul/__init__.py index 5cde510dc5..9fff5a6f5f 100644 --- a/src/kimi_cli/soul/__init__.py +++ b/src/kimi_cli/soul/__init__.py @@ -20,6 +20,10 @@ from kimi_cli.utils.slashcmd import SlashCommand +# 本模块是 soul 包的对外门面:定义 Soul 协议、异常、状态快照, +# 并提供 run_soul 把 KimiSoul 连接到 UI 循环与 Wire 事件流上运行。 + +# LLM 未配置时抛出。 class LLMNotSet(Exception): """Raised when the LLM is not set.""" @@ -27,6 +31,7 @@ def __init__(self) -> None: super().__init__("LLM not set") +# LLM 缺少所需能力时抛出。 class LLMNotSupported(Exception): """Raised when the LLM does not have required capabilities.""" @@ -40,6 +45,7 @@ def __init__(self, llm: LLM, capabilities: list[ModelCapability]): ) +# 达到最大步数时抛出。 class MaxStepsReached(Exception): """Raised when the maximum number of steps is reached.""" @@ -51,6 +57,7 @@ def __init__(self, n_steps: int): self.n_steps = n_steps +# 把 token 数格式化为紧凑字符串,如 28.5k、128k、1.2m。 def format_token_count(n: int) -> str: """Format token count as compact string, e.g. 28.5k, 128k, 1.2m.""" suffix = "" @@ -68,6 +75,7 @@ def format_token_count(n: int) -> str: return f"{compact}{suffix}" +# 格式化上下文占用状态字符串,用于状态栏展示。 def format_context_status( context_usage: float, context_tokens: int = 0, @@ -82,6 +90,7 @@ def format_context_status( return f"context: {bounded:.1%}" +# soul 当前状态的不可变快照,供 UI 展示。 @dataclass(frozen=True, slots=True) class StatusSnapshot: context_usage: float @@ -100,6 +109,7 @@ class StatusSnapshot: """The current MCP startup snapshot, if MCP is configured.""" +# soul 的结构化协议:任何 agent 核心循环实现都应满足。 @runtime_checkable class Soul(Protocol): @property @@ -172,10 +182,12 @@ async def run( """A long-running async function to visualize the agent behavior.""" +# run 被取消事件取消时抛出。 class RunCancelled(Exception): """The run was cancelled by the cancel event.""" +# 运行 soul 的编排函数:用 Wire 把 soul.run 与 UI 循环连接起来。 async def run_soul( soul: Soul, user_input: str | list[ContentPart], @@ -211,6 +223,7 @@ async def run_soul( ) notification_task = asyncio.create_task(_pump_notifications_to_wire(runtime, wire)) + # 等待 soul 运行完成或取消事件触发,二者谁先完成谁返回。 cancel_event_task = asyncio.create_task(cancel_event.wait()) await asyncio.wait( [soul_task, cancel_event_task], @@ -232,6 +245,7 @@ async def run_soul( await cancel_event_task soul_task.result() # this will raise if any exception was raised in the run task finally: + # 收尾:停掉通知泵、冲刷一次通知、关闭 wire 并等待 UI 循环退出。 notification_task.cancel() with contextlib.suppress(asyncio.CancelledError): await notification_task @@ -257,6 +271,7 @@ async def run_soul( _current_wire = ContextVar[Wire | None]("current_wire", default=None) +# 获取当前 wire;agent 循环内调用时应非 None。 def get_wire_or_none() -> Wire | None: """ Get the current wire or None. @@ -265,6 +280,7 @@ def get_wire_or_none() -> Wire | None: return _current_wire.get() +# 向当前 wire 发送一条消息(soul 侧的「print」)。 def wire_send(msg: WireMessage) -> None: """ Send a wire message to the current wire. @@ -276,6 +292,7 @@ def wire_send(msg: WireMessage) -> None: wire.soul_side.send(msg) +# 周期性地把待发通知投递到 wire。 async def _pump_notifications_to_wire(runtime: Runtime | None, wire: Wire) -> None: while True: try: @@ -287,6 +304,7 @@ async def _pump_notifications_to_wire(runtime: Runtime | None, wire: Wire) -> No await asyncio.sleep(1.0) +# 投递一批待发通知到 wire(仅 root 角色)。 async def _deliver_notifications_to_wire_once(runtime: Runtime | None, wire: Wire) -> None: if runtime is None or runtime.role != "root": return diff --git a/src/kimi_cli/soul/agent.py b/src/kimi_cli/soul/agent.py index a208e08796..b18f657d0e 100644 --- a/src/kimi_cli/soul/agent.py +++ b/src/kimi_cli/soul/agent.py @@ -43,6 +43,20 @@ from fastmcp.mcp_config import MCPConfig +# ───────────────────────────────────────────────────────────────────────────── +# 模块概述 +# ───────────────────────────────────────────────────────────────────────────── +# 本模块定义 agent 的核心数据结构与装配流程。 +# 职责: +# 1. Runtime 数据类:持有一次会话共享的运行时组件(配置/LLM/审批/后台任务/技能/子代理等)。 +# 2. BuiltinSystemPromptArgs:注入系统提示词模板的内置变量(KIMI_NOW / KIMI_WORK_DIR 等)。 +# 3. load_agent:从 agent spec(YAML)装配出可运行的 Agent(系统提示词 + 工具集 + 运行时)。 +# 4. load_agents_md:从项目根到工作目录发现并合并 AGENTS.md(预算截断、叶优先)。 +# 5. _load_system_prompt:用 Jinja2 渲染系统提示词模板(${var} 语法)。 +# ───────────────────────────────────────────────────────────────────────────── + + +# 内置系统提示词参数:渲染系统提示词模板时可用的占位变量。 @dataclass(frozen=True, slots=True, kw_only=True) class BuiltinSystemPromptArgs: """Builtin system prompt arguments.""" @@ -65,9 +79,11 @@ class BuiltinSystemPromptArgs: """The shell executable used by the Shell tool, e.g. 'bash (`/bin/bash`)'.""" +# AGENTS.md 合并内容的最大字节数(32 KiB)。 _AGENTS_MD_MAX_BYTES = 32 * 1024 # 32 KiB +# 返回从 project_root 到 work_dir(含)的目录列表,方向为根→叶。 async def _dirs_root_to_leaf(work_dir: KaosPath, project_root: KaosPath) -> list[KaosPath]: """Return the list of directories from *project_root* down to *work_dir* (inclusive).""" dirs: list[KaosPath] = [] @@ -84,6 +100,9 @@ async def _dirs_root_to_leaf(work_dir: KaosPath, project_root: KaosPath) -> list return dirs +# 从项目根到 work_dir 发现并合并 AGENTS.md: +# 每级目录依次检查 .kimi/AGENTS.md(优先)与 AGENTS.md / agents.md(大小写互斥,大写优先), +# 根→叶拼接,总大小受 _AGENTS_MD_MAX_BYTES 限制,预算"叶优先"分配避免深层文件被截断。 async def load_agents_md(work_dir: KaosPath) -> str | None: """Discover and merge ``AGENTS.md`` files from the project root down to *work_dir*. @@ -168,6 +187,7 @@ async def load_agents_md(work_dir: KaosPath) -> str | None: return "\n\n".join(parts) if parts else None +# Agent 运行时:持有一次会话共享的所有组件,通过 Runtime.create 构建,子代理用 copy_for_subagent 克隆。 @dataclass(slots=True, kw_only=True) class Runtime: """Agent runtime.""" @@ -197,6 +217,7 @@ class Runtime: hook_engine: Any = None """HookEngine instance, set by KimiCLI after soul creation.""" + # 补齐可选组件(subagent_store / root_wire_hub / approval_runtime)并做相互绑定。 def __post_init__(self) -> None: if self.subagent_store is None: self.subagent_store = SubagentStore(self.session) @@ -208,6 +229,8 @@ def __post_init__(self) -> None: self.approval.set_runtime(self.approval_runtime) self.background_tasks.bind_runtime(self) + # 静态工厂:并发收集目录列表/AGENTS.md/环境 → 发现并格式化技能 → 恢复额外目录 → + # 合并审批状态 → 组装内置参数,最终返回根 Runtime。 @staticmethod async def create( config: Config, @@ -219,6 +242,7 @@ async def create( runtime_afk: bool = False, skills_dirs: list[KaosPath] | None = None, ) -> Runtime: + # 并发收集三类信息:工作目录列表、AGENTS.md、运行环境探测。 ls_output, agents_md, environment = await asyncio.gather( list_directory(session.work_dir), load_agents_md(session.work_dir), @@ -226,6 +250,7 @@ async def create( ) # Discover and format skills (grouped by scope for the system prompt). + # 解析技能根目录并按作用域分组发现技能。 scoped_roots = await resolve_skills_roots( session.work_dir, skills_dirs=skills_dirs, @@ -240,6 +265,7 @@ async def create( skills_formatted = format_skills_for_prompt(skills) # Restore additional directories from session state, pruning stale entries + # 从会话状态恢复额外目录,剔除已不存在的目录并回写状态。 additional_dirs: list[KaosPath] = [] pruned = False valid_dir_strs: list[str] = [] @@ -259,6 +285,7 @@ async def create( session.save_state() # Format additional dirs info for system prompt + # 把额外目录列表格式化进系统提示词。 additional_dirs_info = "" if additional_dirs: parts: list[str] = [] @@ -274,12 +301,14 @@ async def create( additional_dirs_info = "\n\n".join(parts) # Merge invocation flags with persisted session state. + # 合并命令行标志与持久化会话状态(yolo / afk / 自动批准动作)。 effective_yolo = yolo or session.state.approval.yolo if afk and not session.state.approval.afk: session.state.approval.afk = True session.save_state() saved_actions = set(session.state.approval.auto_approve_actions) + # 审批状态变化时回写会话状态。 def _on_approval_change() -> None: session.state.approval.yolo = approval_state.yolo session.state.approval.afk = approval_state.afk @@ -336,6 +365,7 @@ def _on_approval_change() -> None: role="root", ) + # 克隆运行时给子代理:独立 DenwaRenji,共享审批/通知/技能/额外目录等,角色设为 subagent。 def copy_for_subagent( self, *, @@ -369,6 +399,7 @@ def copy_for_subagent( ) +# 加载完成的 agent:名称 + 系统提示词 + 工具集 + 运行时。 @dataclass(frozen=True, slots=True, kw_only=True) class Agent: """The loaded agent.""" @@ -380,6 +411,8 @@ class Agent: """Each agent has its own runtime, which should be derived from its main agent.""" +# 从 agent spec 文件装配 Agent:加载 spec → 系统提示词 → 注册内置 subagent 类型 → +# 加载工具(含插件工具)→ 处理 MCP 配置(后台加载或延迟)。 async def load_agent( agent_file: Path, runtime: Runtime, @@ -402,6 +435,7 @@ async def load_agent( logger.info("Loading agent: {agent_file}", agent_file=agent_file) agent_spec = load_agent_spec(agent_file) + # 渲染系统提示词模板(内置参数 + spec 自定义参数)。 system_prompt = _load_system_prompt( agent_spec.system_prompt_path, agent_spec.system_prompt_args, @@ -410,6 +444,7 @@ async def load_agent( # Register built-in subagent types before loading tools because some tools render # descriptions from the labor market on initialization. + # 先注册内置 subagent 类型,因为部分工具初始化时会从 labor market 渲染描述。 for subagent_name, subagent_spec in agent_spec.subagents.items(): logger.debug( "Registering builtin subagent type: {subagent_name}", subagent_name=subagent_name @@ -431,6 +466,7 @@ async def load_agent( ) ) + # 构建工具集,并注入工具依赖(KimiToolset / Runtime / Config / Session 等)。 toolset = KimiToolset() tool_deps = { KimiToolset: toolset, @@ -451,6 +487,7 @@ async def load_agent( toolset.load_tools(tools, tool_deps) # Load plugin tools + # 加载插件工具,重名时跳过并告警。 from kimi_cli.plugin.manager import get_plugins_dir from kimi_cli.plugin.tool import load_plugin_tools @@ -464,6 +501,7 @@ async def load_agent( continue toolset.add(plugin_tool) + # 处理 MCP 配置:校验后按 start_mcp_loading 决定后台加载或延迟加载。 if mcp_configs: validated_mcp_configs: list[MCPConfig] = [] if mcp_configs: @@ -491,6 +529,7 @@ async def load_agent( ) +# 用 Jinja2 渲染系统提示词模板:${var} 语法、StrictUndefined 严格检查,错误统一转 SystemPromptTemplateError。 def _load_system_prompt( path: Path, args: dict[str, str], builtin_args: BuiltinSystemPromptArgs ) -> str: diff --git a/src/kimi_cli/soul/approval.py b/src/kimi_cli/soul/approval.py index 23c1b144d4..b2c1473eec 100644 --- a/src/kimi_cli/soul/approval.py +++ b/src/kimi_cli/soul/approval.py @@ -16,6 +16,9 @@ from kimi_cli.utils.logging import logger from kimi_cli.wire.types import DisplayBlock +# 本模块是审批系统的 soul 侧门面:面向工具提供 request 接口, +# 在自动批准(yolo/afk/会话缓存)与人工批准之间分流,并上报遥测。 + type Response = Literal["approve", "approve_for_session", "reject"] # Maps DisplayBlock.type to the TS approval_surface vocabulary. @@ -27,12 +30,14 @@ } +# 由展示块推导审批表面类型(用于遥测)。 def _approval_surface(display: list[DisplayBlock]) -> str: if not display: return "generic" return _SURFACE_BY_BLOCK_TYPE.get(display[0].type, "generic") +# 上报 permission_approval_result 遥测事件(与 TS permissionGateService 对齐)。 def _track_permission_result( *, step_no: int | None, @@ -67,6 +72,7 @@ def _track_permission_result( track("permission_approval_result", **kwargs) +# 审批结果:包含 approved 标志与可选反馈,行为上可当作 bool 使用。 class ApprovalResult: """Result of an approval request. Behaves as bool for backward compatibility.""" @@ -76,9 +82,11 @@ def __init__(self, approved: bool, feedback: str = ""): self.approved = approved self.feedback = feedback + # 使 `if not result:` 直接按 approved 判断。 def __bool__(self) -> bool: return self.approved + # 构造被拒绝时的 ToolRejectedError(含反馈或子 agent 专用提示)。 def rejection_error(self) -> ToolRejectedError: if self.feedback: return ToolRejectedError( @@ -101,6 +109,7 @@ def rejection_error(self) -> ToolRejectedError: return ToolRejectedError() +# 审批状态:yolo/afk/自动批准动作集合等可变状态。 class ApprovalState: def __init__( self, @@ -122,11 +131,13 @@ def __init__( """Set of action names that should automatically be approved.""" self._on_change = on_change + # 触发状态变更回调(如刷新 UI 状态栏)。 def notify_change(self) -> None: if self._on_change is not None: self._on_change() +# 审批门面:持有审批状态与运行时队列,向工具提供 request 接口。 class Approval: def __init__( self, @@ -138,6 +149,7 @@ def __init__( self._state = state or ApprovalState(yolo=yolo) self._runtime = runtime or ApprovalRuntime() + # 创建一个共享同一审批状态与运行时的子审批实例。 def share(self) -> Approval: """Create a new approval queue that shares approval state.""" return Approval(state=self._state, runtime=self._runtime) @@ -149,10 +161,12 @@ def set_runtime(self, runtime: ApprovalRuntime) -> None: def runtime(self) -> ApprovalRuntime: return self._runtime + # 设置 yolo 标志并通知变更。 def set_yolo(self, yolo: bool) -> None: self._state.yolo = yolo self._state.notify_change() + # 切换持久化 afk 模式;关闭时同时清除本次调用级 afk 覆盖。 def set_afk(self, afk: bool) -> None: """Toggle persisted afk (away-from-keyboard) mode. @@ -165,10 +179,12 @@ def set_afk(self, afk: bool) -> None: self._state.runtime_afk = False self._state.notify_change() + # 切换本次调用级 afk 模式(不持久化)。 def set_runtime_afk(self, afk: bool) -> None: """Toggle invocation-only afk mode without persisting it.""" self._state.runtime_afk = afk + # 是否应自动批准工具调用(yolo 或 afk 任一成立)。 def is_auto_approve(self) -> bool: """True when tool calls should be auto-approved. @@ -177,26 +193,32 @@ def is_auto_approve(self) -> bool: """ return self._state.yolo or self.is_afk() + # 用户是否显式开启 yolo。 def is_yolo(self) -> bool: """True only when the user explicitly opted into yolo.""" return self._state.yolo + # 同 is_yolo(保留的别名)。 def is_yolo_flag(self) -> bool: """True only when the user explicitly opted into yolo (not via afk).""" return self.is_yolo() + # 是否处于 afk(无人在场)状态。 def is_afk(self) -> bool: """True when no user is present (away-from-keyboard).""" return self._state.afk or self._state.runtime_afk + # 是否仅持久化 afk 生效。 def is_afk_flag(self) -> bool: """True only when persisted afk mode is active.""" return self._state.afk + # 是否仅本次调用级 afk 生效。 def is_runtime_afk(self) -> bool: """True only when afk came from this invocation.""" return self._state.runtime_afk + # 请求批准:自动批准直接返回,否则把请求提交到运行时队列等待人工响应。 async def request( self, sender: str, @@ -239,6 +261,7 @@ def _elapsed_ms() -> int: action=action, description=description, ) + # 自动批准路径:yolo / afk。 if self.is_auto_approve(): from kimi_cli.telemetry import track @@ -259,6 +282,7 @@ def _elapsed_ms() -> int: ) return ApprovalResult(approved=True) + # 会话级自动批准路径:动作已在 auto_approve_actions 集合中。 if action in self._state.auto_approve_actions: from kimi_cli.telemetry import track @@ -279,6 +303,7 @@ def _elapsed_ms() -> int: ) return ApprovalResult(approved=True) + # 人工批准路径:创建请求并等待运行时队列返回响应。 request_id = str(uuid.uuid4()) display_blocks = display or [] source = get_current_approval_source_or_none() or ApprovalSource( @@ -352,6 +377,7 @@ def _elapsed_ms() -> int: ) return ApprovalResult(approved=True) case "approve_for_session": + # 批准并缓存该动作:后续同 action 的请求自动通过。 track( "tool_approved", tool_name=tool_call.function.name, diff --git a/src/kimi_cli/soul/btw.py b/src/kimi_cli/soul/btw.py index 5d639142c8..3c096f8e2d 100644 --- a/src/kimi_cli/soul/btw.py +++ b/src/kimi_cli/soul/btw.py @@ -30,8 +30,10 @@ from kimi_cli.soul.kimisoul import KimiSoul +# 副问题(/btw)最多允许的回合数。 _BTW_MAX_TURNS = 2 +# 注入给副问题 LLM 调用的 system-reminder,约束其只做纯文本回答、不调用工具。 SIDE_QUESTION_SYSTEM_REMINDER = """\ This is a side question from the user. Answer directly in a single response. @@ -51,6 +53,7 @@ # --------------------------------------------------------------------------- +# 暴露与主 agent 相同的工具定义(命中 prompt cache),但拒绝一切工具调用。 class _DenyAllToolset: """A toolset that exposes the same tool definitions as the agent (for prompt cache matching) but rejects every tool call with an error message.""" @@ -62,6 +65,7 @@ def __init__(self, source_tools: list[Tool]) -> None: def tools(self) -> list[Tool]: return self._tools + # 任何工具调用都返回「禁用」错误。 def handle(self, tool_call: ToolCall) -> ToolResult: return ToolResult( tool_call_id=tool_call.id, @@ -77,6 +81,7 @@ def handle(self, tool_call: ToolCall) -> ToolResult: # --------------------------------------------------------------------------- +# 构造与主 agent 对齐的 (system_prompt, history, toolset),以复用 prompt cache。 def _build_btw_context(soul: KimiSoul, question: str) -> tuple[str, list[Message], _DenyAllToolset]: """Build (system_prompt, history, toolset) aligned with the main agent. @@ -99,6 +104,7 @@ def _build_btw_context(soul: KimiSoul, question: str) -> tuple[str, list[Message # --------------------------------------------------------------------------- +# 执行副问题,返回 (response, error);最多运行 _BTW_MAX_TURNS 步。 async def execute_side_question( soul: KimiSoul, question: str, @@ -137,6 +143,7 @@ async def execute_side_question( text_chunks: list[str] = [] + # 流式收集文本片段并回调给调用方。 def _on_part(part: StreamedMessagePart) -> None: if isinstance(part, TextPart) and part.text: text_chunks.append(part.text) @@ -203,6 +210,7 @@ def _on_part(part: StreamedMessagePart) -> None: logger.warning("Side question failed: {error}", error=e) return None, str(e) finally: + # 无论成败都上报一次 tool_call 遥测事件。 elapsed = time.monotonic() - t0 kwargs: dict[str, bool | int | float | str | None] = { "tool_name": "btw", @@ -215,6 +223,7 @@ def _on_part(part: StreamedMessagePart) -> None: track("tool_call", **kwargs) +# 把 ToolResult 转为用于历史拼接的 tool 消息。 def _tool_result_to_message(tool_result: ToolResult) -> Message: """Convert a ToolResult to a tool-result Message for history.""" content = tool_result.return_value.message or "Tool call denied." @@ -230,6 +239,7 @@ def _tool_result_to_message(tool_result: ToolResult) -> Message: # --------------------------------------------------------------------------- +# 通过 wire 事件流执行副问题(供 Web UI / 非交互场景)。 async def run_side_question(soul: KimiSoul, question: str) -> None: """Execute a side question via wire events.""" if soul._runtime.llm is None: # pyright: ignore[reportPrivateUsage] diff --git a/src/kimi_cli/soul/compaction.py b/src/kimi_cli/soul/compaction.py index 177c5edce8..46a20657f8 100644 --- a/src/kimi_cli/soul/compaction.py +++ b/src/kimi_cli/soul/compaction.py @@ -14,15 +14,20 @@ from kimi_cli.utils.logging import logger from kimi_cli.wire.types import ContentPart, TextPart, ThinkPart +# 本模块实现上下文压缩(compaction):当历史过长时,用一次 LLM 调用把较早的 +# 消息压缩成摘要,同时保留末尾若干条消息,从而在控制 token 的同时尽量不丢信息。 + COMPACTION_SYSTEM_PROMPT = "You are a helpful assistant that compacts conversation context." COMPACTION_OUTPUT_PREFIX = "Previous context has been compacted. Here is the compaction output:" +# 压缩结果:压缩后的消息序列 + 本次压缩 LLM 调用的用量与 trace_id。 class CompactionResult(NamedTuple): messages: Sequence[Message] usage: TokenUsage | None trace_id: str | None = None + # 估算压缩后消息的 token 数;有真实用量时优先用用量,其余按文本长度估算。 @property def estimated_token_count(self) -> int: """Estimate the token count of the compacted messages. @@ -45,6 +50,7 @@ def estimated_token_count(self) -> int: return estimate_text_tokens(self.messages) +# 按文本长度粗略估算 token 数(约 4 字符 / token)。 def estimate_text_tokens(messages: Sequence[Message]) -> int: """Estimate tokens from message text content using a character-based heuristic.""" total_chars = 0 @@ -57,6 +63,7 @@ def estimate_text_tokens(messages: Sequence[Message]) -> int: return total_chars // 4 +# 判断是否应触发自动压缩:按占用比例或按保留空间任一条件先满足即触发。 def should_auto_compact( token_count: int, max_context_size: int, @@ -76,6 +83,7 @@ def should_auto_compact( ) +# 压缩器的协议:实现 compact 即可被 KimiSoul 调用。 @runtime_checkable class Compaction(Protocol): async def compact( @@ -107,10 +115,12 @@ def type_check(simple: SimpleCompaction): _: Compaction = simple +# 简单压缩实现:压缩除末尾保留消息外的全部历史,并拼接摘要。 class SimpleCompaction: def __init__(self, max_preserved_messages: int = 2) -> None: self.max_preserved_messages = max_preserved_messages + # 执行压缩:准备输入 -> 调用压缩 LLM -> 拼装「摘要 + 保留消息」。 async def compact( self, messages: Sequence[Message], @@ -147,10 +157,12 @@ async def compact( messages=compacted_messages, usage=result.usage, trace_id=result.trace_id ) + # prepare 的返回类型:待压缩消息(可为 None)+ 需保留的尾部消息。 class PrepareResult(NamedTuple): compact_message: Message | None to_preserve: Sequence[Message] + # 把历史拆分为「待压缩部分」与「末尾保留部分」,并构造压缩输入消息。 def prepare( self, messages: Sequence[Message], *, custom_instruction: str = "" ) -> PrepareResult: @@ -160,6 +172,7 @@ def prepare( history = list(messages) preserve_start_index = len(history) n_preserved = 0 + # 从末尾向前数出要保留的 user/assistant 消息。 for index in range(len(history) - 1, -1, -1): if history[index].role in {"user", "assistant"}: n_preserved += 1 diff --git a/src/kimi_cli/soul/context.py b/src/kimi_cli/soul/context.py index 864935d9dc..8639da3b69 100644 --- a/src/kimi_cli/soul/context.py +++ b/src/kimi_cli/soul/context.py @@ -17,6 +17,11 @@ from kimi_cli.utils.path import next_available_rotation +# 本模块实现会话上下文(Context)的持久化:以 JSONL 文件为后端, +# 按行写入 system_prompt / 消息 / token 用量 / 检查点等记录, +# 支持从文件恢复、检查点回退与清理,并维护 token 计数估算。 + +# 会话上下文:内存历史 + JSONL 文件后端的持久化。 class Context: def __init__(self, file_backend: Path): self._file_backend = file_backend @@ -27,6 +32,7 @@ def __init__(self, file_backend: Path): """The ID of the next checkpoint, starting from 0, incremented after each checkpoint.""" self._system_prompt: str | None = None + # 从文件后端恢复上下文(逐行解析记录)。 async def restore(self) -> bool: logger.debug("Restoring context from file: {file_backend}", file_backend=self._file_backend) if self._history: @@ -88,6 +94,7 @@ def system_prompt(self) -> str | None: def file_backend(self) -> Path: return self._file_backend + # 把系统提示词作为文件首条记录写入(空文件直接写,非空则通过临时文件原子前置)。 async def write_system_prompt(self, prompt: str) -> None: """Write the system prompt as the first record of the context file. @@ -98,6 +105,7 @@ async def write_system_prompt(self, prompt: str) -> None: """ prompt_line = json.dumps({"role": "_system_prompt", "content": prompt}) + "\n" + # 同步文件写入逻辑,放入线程池执行以避免阻塞事件循环。 def _write_system_prompt_sync() -> None: if not self._file_backend.exists() or self._file_backend.stat().st_size == 0: self._file_backend.write_text(prompt_line, encoding="utf-8") @@ -120,6 +128,7 @@ def _write_system_prompt_sync() -> None: self._system_prompt = prompt + # 创建一个检查点记录,并可选地追加一条 CHECKPOINT 消息。 async def checkpoint(self, add_user_message: bool): checkpoint_id = self._next_checkpoint_id self._next_checkpoint_id += 1 @@ -132,6 +141,7 @@ async def checkpoint(self, add_user_message: bool): Message(role="user", content=[system(f"CHECKPOINT {checkpoint_id}")]) ) + # 回退到指定检查点:旋转文件后端,仅恢复该检查点之前的内容。 async def revert_to(self, checkpoint_id: int): """ Revert the context to the specified checkpoint. @@ -199,6 +209,7 @@ async def revert_to(self, checkpoint_id: int): self._pending_token_estimate = estimate_text_tokens(messages_after_last_usage) + # 清空上下文(旋转文件后端并重置所有内存状态)。 async def clear(self): """ Clear the context history. @@ -229,6 +240,7 @@ async def clear(self): self._next_checkpoint_id = 0 self._system_prompt = None + # 追加消息到内存历史,并同步写入文件后端。 async def append_message(self, message: Message | Sequence[Message]): logger.debug("Appending message(s) to context: {message}", message=message) messages = [message] if isinstance(message, Message) else message @@ -239,6 +251,7 @@ async def append_message(self, message: Message | Sequence[Message]): for message in messages: await f.write(message.model_dump_json(exclude_none=True) + "\n") + # 更新权威 token 计数(来自 LLM 用量的真实值),并落盘。 async def update_token_count(self, token_count: int): logger.debug("Updating token count in context: {token_count}", token_count=token_count) self._token_count = token_count @@ -247,6 +260,7 @@ async def update_token_count(self, token_count: int): async with aiofiles.open(self._file_backend, "a", encoding="utf-8") as f: await f.write(json.dumps({"role": "_usage", "token_count": token_count}) + "\n") + # 解析一行 JSONL,失败时告警并返回 None。 def _parse_context_line( self, line: str, @@ -273,6 +287,7 @@ def _parse_context_line( return None return cast(dict[str, Any], line_json) + # 把一条记录应用到上下文状态,返回该行是否应保留(用于回退时重写文件)。 def _apply_context_record( self, line_json: dict[str, Any], diff --git a/src/kimi_cli/soul/denwarenji.py b/src/kimi_cli/soul/denwarenji.py index aa1ba8ef38..700eff4c96 100644 --- a/src/kimi_cli/soul/denwarenji.py +++ b/src/kimi_cli/soul/denwarenji.py @@ -3,21 +3,29 @@ from pydantic import BaseModel, Field +# 本模块定义 D-Mail(D 邮件)机制:向过去的检查点回传一条消息。 +# DenwaRenji 只维护「待发送的 D-Mail」与「检查点计数」两份会话级状态, +# 由 KimiSoul 主循环与 SendDMail 工具通过共享实例协作完成回传。 + +# D-Mail:指向某个历史检查点、需要回传的消息内容。 class DMail(BaseModel): message: str = Field(description="The message to send.") checkpoint_id: int = Field(description="The checkpoint to send the message back to.", ge=0) # TODO: allow restoring filesystem state to the checkpoint +# DenwaRenji 状态校验失败时抛出的异常。 class DenwaRenjiError(Exception): pass +# D-Mail 的会话级状态容器(仅内存,不负责持久化)。 class DenwaRenji: def __init__(self): self._pending_dmail: DMail | None = None self._n_checkpoints: int = 0 + # 由 SendDMail 工具调用,校验并登记一条待回传的 D-Mail。 def send_dmail(self, dmail: DMail): """Send a D-Mail. Intended to be called by the SendDMail tool.""" if self._pending_dmail is not None: @@ -28,10 +36,12 @@ def send_dmail(self, dmail: DMail): raise DenwaRenjiError("There is no checkpoint with the given ID") self._pending_dmail = dmail + # 由 soul 调用,同步当前已创建的检查点数量。 def set_n_checkpoints(self, n_checkpoints: int): """Set the number of checkpoints. Intended to be called by the soul.""" self._n_checkpoints = n_checkpoints + # 由 soul 调用,取出并清空待发送的 D-Mail。 def fetch_pending_dmail(self) -> DMail | None: """Fetch a pending D-Mail. Intended to be called by the soul.""" pending_dmail = self._pending_dmail diff --git a/src/kimi_cli/soul/dynamic_injection.py b/src/kimi_cli/soul/dynamic_injection.py index 16630cdd81..a24e189c30 100644 --- a/src/kimi_cli/soul/dynamic_injection.py +++ b/src/kimi_cli/soul/dynamic_injection.py @@ -13,6 +13,11 @@ from kimi_cli.soul.kimisoul import KimiSoul +# 本模块定义「动态注入」框架:在每次 LLM 步骤前,向历史中注入一段 +# 与当前运行时状态相关的提示词(如 plan 模式提醒、afk 模式提醒)。 +# 各 Provider 自行决定节流策略,由 KimiSoul 在每步统一收集并合并。 + +# 一段待注入的动态提示内容。 @dataclass(frozen=True, slots=True) class DynamicInjection: """A dynamic prompt content to be injected before an LLM step.""" @@ -21,6 +26,7 @@ class DynamicInjection: content: str # text content (will be wrapped in tags) +# 动态注入 Provider 的抽象基类。 class DynamicInjectionProvider(ABC): """Base class for dynamic injection providers. @@ -29,6 +35,7 @@ class DynamicInjectionProvider(ABC): (context_usage, runtime, config, etc.). """ + # 返回本步骤要注入的提示列表;实现类负责自身的节流。 @abstractmethod async def get_injections( self, @@ -36,6 +43,7 @@ async def get_injections( soul: KimiSoul, ) -> list[DynamicInjection]: ... + # 上下文压缩后回调:历史被重写,之前的注入可能被摘要吞掉,需重置节流状态。 async def on_context_compacted(self) -> None: """Called after the context is compacted (history is rebuilt). @@ -45,6 +53,7 @@ async def on_context_compacted(self) -> None: """ return None + # afk 模式切换时回调:重置节流,使新状态下的提醒可再次注入。 async def on_afk_changed(self, enabled: bool) -> None: """Called when afk mode is toggled at runtime. @@ -55,6 +64,7 @@ async def on_afk_changed(self, enabled: bool) -> None: return None +# 合并相邻的 user 消息,得到干净的 API 输入序列。 def normalize_history(history: Sequence[Message]) -> list[Message]: """Merge adjacent user messages to produce a clean API input sequence. diff --git a/src/kimi_cli/soul/dynamic_injections/afk_mode.py b/src/kimi_cli/soul/dynamic_injections/afk_mode.py index 183491851f..4e885127b4 100644 --- a/src/kimi_cli/soul/dynamic_injections/afk_mode.py +++ b/src/kimi_cli/soul/dynamic_injections/afk_mode.py @@ -10,8 +10,12 @@ if TYPE_CHECKING: from kimi_cli.soul.kimisoul import KimiSoul +# 本模块在 afk(away-from-keyboard,用户离开键盘)模式下注入提醒, +# 告知 agent 当前无人应答、所有工具调用会被自动批准、不得调用 AskUserQuestion。 + _AFK_INJECTION_TYPE = "afk_mode" +# 进入 afk 模式时注入的完整引导提示词。 _AFK_PROMPT_ROOT = ( "You are running in afk mode. No user is present to answer " "questions or approve actions. All tool calls are auto-approved by " @@ -25,6 +29,7 @@ "decisions to a human." ) +# 退出 afk 模式时追加到上下文的提醒。 AFK_DISABLED_REMINDER = ( "Afk mode is now disabled. The user is back at the terminal and CAN answer " "AskUserQuestion.\n" @@ -37,12 +42,14 @@ ) +# 仅在 afk 模式下注入一次 afk 引导的 Provider。 class AfkModeInjectionProvider(DynamicInjectionProvider): """Injects afk (away-from-keyboard) guidance when no user is present.""" def __init__(self) -> None: self._injected: bool = False + # 判断是否处于 afk 模式(且非子 agent),满足条件则注入一次。 async def get_injections( self, history: Sequence[Message], @@ -62,11 +69,13 @@ async def get_injections( self._injected = True return [DynamicInjection(type=_AFK_INJECTION_TYPE, content=_AFK_PROMPT_ROOT)] + # 上下文压缩后重置,允许下一步重新注入 afk 约束。 async def on_context_compacted(self) -> None: # Compaction rewrites history; the prior afk reminder may have been # summarized away, so let the next afk step restate the constraint. self._injected = False + # afk 切换后重置,使下一步可注入最新的 afk 引导。 async def on_afk_changed(self, enabled: bool) -> None: # A runtime toggle changes the latest truth about user presence. # Re-arm so the next LLM step can inject the current afk guidance. diff --git a/src/kimi_cli/soul/dynamic_injections/plan_mode.py b/src/kimi_cli/soul/dynamic_injections/plan_mode.py index d79571594d..61baadd92c 100644 --- a/src/kimi_cli/soul/dynamic_injections/plan_mode.py +++ b/src/kimi_cli/soul/dynamic_injections/plan_mode.py @@ -10,12 +10,16 @@ if TYPE_CHECKING: from kimi_cli.soul.kimisoul import KimiSoul +# 本模块在 plan 模式(只读研究 + 写计划文件)下周期性注入提醒, +# 约束 agent 不得改动系统,只能写计划文件,并以 ExitPlanMode 结束回合。 + # Inject a reminder every N assistant turns. _TURN_INTERVAL = 5 # Every N-th reminder is the full version; others are sparse. _FULL_EVERY_N = 5 +# 周期性注入 plan 模式只读提醒的 Provider。 class PlanModeInjectionProvider(DynamicInjectionProvider): """Periodically injects read-only reminders while plan mode is active. @@ -27,6 +31,7 @@ class PlanModeInjectionProvider(DynamicInjectionProvider): def __init__(self) -> None: self._inject_count: int = 0 + # 返回本步骤要注入的 plan 模式提醒;仅主 agent 注入,子 agent 不注入。 async def get_injections( self, history: Sequence[Message], @@ -98,6 +103,7 @@ async def get_injections( return [DynamicInjection(type="plan_mode", content=content)] +# 判断一条消息是否包含 plan 模式提醒(按稳定的前缀匹配)。 def _has_plan_reminder(msg: Message) -> bool: """Check whether a message contains a plan mode reminder. @@ -114,6 +120,7 @@ def _has_plan_reminder(msg: Message) -> bool: return False +# 生成完整的 plan 模式只读提醒。 def _full_reminder( plan_file_path: str | None = None, plan_exists: bool = False, @@ -185,6 +192,7 @@ def _full_reminder( return "\n".join(lines) +# 生成精简版 plan 模式提醒。 def _sparse_reminder(plan_file_path: str | None = None) -> str: parts = [ "Plan mode still active (see full instructions earlier).", @@ -210,6 +218,7 @@ def _sparse_reminder(plan_file_path: str | None = None) -> str: return " ".join(parts) +# 生成重入 plan 模式(已有计划文件)时的一次性提醒。 def _reentry_reminder(plan_file_path: str | None = None) -> str: """One-shot reminder when re-entering plan mode with an existing plan.""" lines = [ diff --git a/src/kimi_cli/soul/kimisoul.py b/src/kimi_cli/soul/kimisoul.py index 3f14c2a2f7..b34dd42762 100644 --- a/src/kimi_cli/soul/kimisoul.py +++ b/src/kimi_cli/soul/kimisoul.py @@ -106,11 +106,28 @@ def type_check(soul: KimiSoul): _: Soul = soul +# ───────────────────────────────────────────────────────────────────────────── +# 模块概述 +# ───────────────────────────────────────────────────────────────────────────── +# 本模块是 Kimi Code CLI 的"灵魂"(Soul)主循环实现。 +# 职责: +# 1. 接收用户输入(文本或 ContentPart),处理 slash 命令 / 技能 / 流程(flow)。 +# 2. 驱动一轮(turn)内的 step 循环:调用 LLM → 执行工具 → 追加上下文 → 判断是否继续。 +# 3. 上下文超限时自动压缩(compaction),并对 API 错误做重试与连接恢复。 +# 4. 通过 Wire 流式输出事件,并埋点 telemetry。 +# 核心类: +# - KimiSoul 单会话主循环(核心入口 run / _turn / _agent_loop / _step)。 +# - FlowRunner 节点图流程执行器(技能 flow 与 ralph 自动循环)。 +# - BackToTheFuture 上下文回溯异常(D-Mail 机制)。 +# ───────────────────────────────────────────────────────────────────────────── + + SKILL_COMMAND_PREFIX = "skill:" FLOW_COMMAND_PREFIX = "flow:" DEFAULT_MAX_FLOW_MOVES = 1000 +# 把 LLM API 异常归类为 (错误类型, 状态码),供遥测与重试判断使用。 def classify_api_error(e: Exception) -> tuple[str, int | None]: """Classify an LLM API exception into (error_type, status_code). @@ -156,6 +173,7 @@ def classify_api_error(e: Exception) -> tuple[str, int | None]: _RETRYABLE_STATUS_CODES = frozenset({408, 409, 429, 500, 502, 503, 504, 529}) +# 判断 API 错误是否"可重试",与 TS 端 isRetryableGenerateError 对齐,供遥测上报使用。 def is_retryable_api_error(e: Exception) -> bool: """Classify retryability for the ``api_error`` telemetry event. @@ -172,6 +190,7 @@ def is_retryable_api_error(e: Exception) -> bool: return isinstance(e, ChatProviderError) +# 提取 provider_type / protocol 遥测字段(LLM 缺失时返回空 dict)。 def _provider_telemetry_kwargs(llm: LLM | None) -> dict[str, str]: if llm is None or llm.provider_config is None: return {} @@ -179,6 +198,7 @@ def _provider_telemetry_kwargs(llm: LLM | None) -> dict[str, str]: return {"provider_type": provider_type, "protocol": provider_type} +# 上报 api_error 遥测事件(错误类型、模型、可重试性、耗时、状态码、trace_id 等)。 def _track_api_error( error: ChatProviderError, *, @@ -206,9 +226,11 @@ def _track_api_error( track("api_error", **properties) +# step(单步)的停止原因字面量类型。 type StepStopReason = Literal["no_tool_calls", "tool_rejected", "tool_call_repeat"] +# 单步执行结果:停止原因 + 助手消息。 @dataclass(frozen=True, slots=True) class StepOutcome: stop_reason: StepStopReason @@ -218,6 +240,7 @@ class StepOutcome: type TurnStopReason = StepStopReason +# 单轮执行结果:停止原因 + 最终消息(可能为 None)+ 已执行步数。 @dataclass(frozen=True, slots=True) class TurnOutcome: stop_reason: TurnStopReason @@ -228,6 +251,7 @@ class TurnOutcome: class KimiSoul: """The soul of Kimi Code CLI.""" + # 初始化 Soul:绑定 agent/runtime/context,装配压缩器、动态注入 provider、hook 引擎与 slash 命令。 def __init__( self, agent: Agent, @@ -288,55 +312,66 @@ def __init__( self._slash_commands = self._build_slash_commands() self._slash_command_map = self._index_slash_commands(self._slash_commands) + # 代理名称。 @property def name(self) -> str: return self._agent.name + # 当前模型名(未配置 LLM 时为空串)。 @property def model_name(self) -> str: return self._runtime.llm.chat_provider.model_name if self._runtime.llm else "" + # 当前模型能力集(未配置 LLM 时为 None)。 @property def model_capabilities(self) -> set[ModelCapability] | None: if self._runtime.llm is None: return None return self._runtime.llm.capabilities + # 是否显式开启 yolo(自动批准)模式。 @property def is_yolo(self) -> bool: """Whether explicit yolo mode is active.""" return self._approval.is_yolo() + # 工具审批是否被绕过(显式 yolo,或 afk 隐含)。 @property def is_auto_approve(self) -> bool: """Whether tool approvals are bypassed (explicit yolo, or implied by afk).""" return self._approval.is_auto_approve() + # 用户是否不在场(away-from-keyboard)。 @property def is_afk(self) -> bool: """Whether no user is present (away-from-keyboard).""" return self._approval.is_afk() + # 持久化的 afk 模式是否激活。 @property def is_afk_flag(self) -> bool: """Whether persisted afk mode is active.""" return self._approval.is_afk_flag() + # 是否为根会话(而非子代理)。 @property def is_root(self) -> bool: """Whether this soul is the root session rather than a subagent.""" return self._runtime.role == "root" + # 是否以子代理身份运行。 @property def is_subagent(self) -> bool: """Whether this soul is running as a subagent rather than the root session.""" return self._runtime.role == "subagent" + # 根会话最近一次 LLM trace id(仅根会话返回,用于 UI 事件)。 @property def root_trace_id(self) -> str | None: """The latest LLM trace id for this root session, for UI events.""" return self._root_trace_id if self.is_root else None + # 设置当前 trace id(写入 telemetry ContextVar,根会话额外缓存)。 def _set_trace_id(self, trace_id: str | None) -> None: from kimi_cli.telemetry import set_current_trace_id @@ -344,24 +379,29 @@ def _set_trace_id(self, trace_id: str | None) -> None: if self.is_root: self._root_trace_id = trace_id + # 是否处于 plan 模式(只读研究与规划)。 @property def plan_mode(self) -> bool: """Whether plan mode (read-only research and planning) is active.""" return self._plan_mode + # hook 引擎。 @property def hook_engine(self) -> HookEngine: return self._hook_engine + # 替换 hook 引擎并同步给工具集(若为 KimiToolset)。 def set_hook_engine(self, engine: HookEngine) -> None: self._hook_engine = engine if isinstance(self._agent.toolset, KimiToolset): self._agent.toolset.set_hook_engine(engine) + # 注册一个额外的动态注入 provider。 def add_injection_provider(self, provider: DynamicInjectionProvider) -> None: """Register an additional dynamic injection provider.""" self._injection_providers.append(provider) + # 从所有 provider 收集动态注入,单个失败隔离、不影响其余。 async def _collect_injections(self) -> list[DynamicInjection]: """Collect dynamic injections from all registered providers.""" injections: list[DynamicInjection] = [] @@ -377,6 +417,7 @@ async def _collect_injections(self) -> list[DynamicInjection]: ) return injections + # 通知各 provider 上下文已压缩,失败逐个隔离,避免中断压缩流程。 async def _notify_injection_providers_compacted(self) -> None: """Notify all injection providers that the context has been compacted. @@ -394,6 +435,7 @@ async def _notify_injection_providers_compacted(self) -> None: exc_info=True, ) + # 通知各 provider afk 模式已变化。 async def notify_afk_changed(self, enabled: bool) -> None: """Notify dynamic injection providers that afk mode changed.""" for provider in self._injection_providers: @@ -406,6 +448,7 @@ async def notify_afk_changed(self, enabled: bool) -> None: exc_info=True, ) + # 把 plan 模式状态(checker/path_getter/afk 检查)绑定到支持它的工具上。 def _bind_plan_mode_tools(self) -> None: """Bind plan mode state to tools that support it.""" if not isinstance(self._agent.toolset, KimiToolset): @@ -462,6 +505,7 @@ def path_getter() -> Path | None: if isinstance(ask_tool, AskUserQuestion): ask_tool.bind_afk(self._approval.is_afk) + # 首次激活时分配稳定的 plan 会话 id,并计算/持久化 slug 以跨进程存活。 def _ensure_plan_session_id(self) -> None: """Allocate a stable plan session ID on first activation.""" if self._plan_session_id is None: @@ -476,6 +520,7 @@ def _ensure_plan_session_id(self) -> None: self._runtime.session.state.plan_slug = slug self._runtime.session.save_state() + # 更新 plan 模式状态(手动或工具触发),并持久化到会话状态以跨进程存活。 def _set_plan_mode(self, enabled: bool, *, source: Literal["manual", "tool"]) -> bool: """Update plan mode state for either manual or tool-driven toggles.""" if enabled == self._plan_mode: @@ -494,6 +539,7 @@ def _set_plan_mode(self, enabled: bool, *, source: Literal["manual", "tool"]) -> self._runtime.session.save_state() return self._plan_mode + # 返回当前会话的 plan 文件路径(无 plan 会话时为 None)。 def get_plan_file_path(self) -> Path | None: """Get the plan file path for the current session.""" if self._plan_session_id is None: @@ -502,6 +548,7 @@ def get_plan_file_path(self) -> Path | None: return get_plan_file_path(self._plan_session_id) + # 读取当前 plan 文件内容。 def read_current_plan(self) -> str | None: """Read the current plan file content.""" if self._plan_session_id is None: @@ -510,12 +557,14 @@ def read_current_plan(self) -> str | None: return read_plan_file(self._plan_session_id) + # 删除当前 plan 文件。 def clear_current_plan(self) -> None: """Delete the current plan file.""" path = self.get_plan_file_path() if path and path.exists(): path.unlink() + # 工具触发切换 plan 模式,返回新状态(工具不隐藏,而是在调用时检查并拒绝)。 async def toggle_plan_mode(self) -> bool: """Toggle plan mode on/off. Returns the new state. @@ -525,10 +574,12 @@ async def toggle_plan_mode(self) -> bool: """ return self._set_plan_mode(not self._plan_mode, source="tool") + # UI/手动入口(slash 命令、快捷键)切换 plan 模式。 async def toggle_plan_mode_from_manual(self) -> bool: """Toggle plan mode from UI/manual entry points (slash command, keybinding).""" return self._set_plan_mode(not self._plan_mode, source="manual") + # UI/手动入口将 plan 模式设为指定状态(直接给定目标值,避免竞态)。 async def set_plan_mode_from_manual(self, enabled: bool) -> bool: """Set plan mode to a specific state from UI/manual entry points. @@ -537,6 +588,7 @@ async def set_plan_mode_from_manual(self, enabled: bool) -> bool: """ return self._set_plan_mode(enabled, source="manual") + # 安排下一轮注入 plan 模式激活提醒(状态未变化、_set_plan_mode 提前返回时使用)。 def schedule_plan_activation_reminder(self) -> None: """Schedule a plan-mode activation reminder for the next turn. @@ -547,6 +599,7 @@ def schedule_plan_activation_reminder(self) -> None: if self._plan_mode: self._pending_plan_activation_injection = True + # 消费手动切换排队的激活提醒(仅在 plan 模式且有待消费提醒时返回 True)。 def consume_pending_plan_activation_injection(self) -> bool: """Consume the next-step activation reminder scheduled by a manual toggle.""" if not self._plan_mode or not self._pending_plan_activation_injection: @@ -554,6 +607,7 @@ def consume_pending_plan_activation_injection(self) -> bool: self._pending_plan_activation_injection = False return True + # 是否开启思考(thinking)模式;未配置 LLM 或未设置 effort 时为 None。 @property def thinking(self) -> bool | None: """Whether thinking mode is enabled.""" @@ -563,6 +617,7 @@ def thinking(self) -> bool | None: return thinking_effort != "off" return None + # 构造状态快照:上下文占用、yolo/afk/plan 状态、token 数、MCP 状态。 @property def status(self) -> StatusSnapshot: token_count = self._context.token_count @@ -577,52 +632,63 @@ def status(self) -> StatusSnapshot: mcp_status=self._mcp_status_snapshot(), ) + # 返回 Agent。 @property def agent(self) -> Agent: return self._agent + # 返回 Runtime。 @property def runtime(self) -> Runtime: return self._runtime + # 返回 Context。 @property def context(self) -> Context: return self._context + # 上下文占用比例(token_count / max_context_size)。 @property def _context_usage(self) -> float: if self._runtime.llm is not None: return self._context.token_count / self._runtime.llm.max_context_size return 0.0 + # 返回会话的 WireFile。 @property def wire_file(self) -> WireFile: return self._runtime.session.wire_file + # MCP 状态快照(非 KimiToolset 时返回 None)。 def _mcp_status_snapshot(self): if not isinstance(self._agent.toolset, KimiToolset): return None return self._agent.toolset.mcp_status_snapshot() + # 启动延迟的 MCP 工具加载(不暴露 toolset 内部),返回是否有延迟加载。 async def start_background_mcp_loading(self) -> bool: """Start deferred MCP loading, if any, without exposing toolset internals.""" if not isinstance(self._agent.toolset, KimiToolset): return False return await self._agent.toolset.start_deferred_mcp_tool_loading() + # 等待在途的 MCP 启动完成。 async def wait_for_background_mcp_loading(self) -> None: """Wait for any in-flight MCP startup to finish.""" if not isinstance(self._agent.toolset, KimiToolset): return await self._agent.toolset.wait_for_mcp_tools() + # 对上下文做检查点(checkpoint)。 async def _checkpoint(self): await self._context.checkpoint(self._checkpoint_with_user_message) + # 将一条 steer 消息排队,注入当前轮。 def steer(self, content: str | list[ContentPart]) -> None: """Queue a steer message for injection into the current turn.""" self._steer_queue.put_nowait(content) + # 清空 steer 队列并作为后续 user 消息注入,返回是否消费过(/btw 已在 UI 层拦截)。 async def _consume_pending_steers(self) -> bool: """Drain the steer queue and inject as follow-up user messages. @@ -639,6 +705,7 @@ async def _consume_pending_steers(self) -> bool: consumed = True return consumed + # 把单条 steer 作为普通后续 user 消息注入上下文(做能力校验)。 async def _inject_steer(self, content: str | list[ContentPart]) -> None: """Inject a single steer as a regular follow-up user message.""" parts = cast( @@ -652,10 +719,13 @@ async def _inject_steer(self, content: str | list[ContentPart]) -> None: raise LLMNotSupported(self._runtime.llm, list(missing_caps)) await self._context.append_message(message) + # 可用 slash 命令列表。 @property def available_slash_commands(self) -> list[SlashCommand[Any]]: return self._slash_commands + # 一轮(turn)入口:刷新 OAuth、触发 UserPromptSubmit/Stop hook、处理命令,最后执行 _turn, + # 并在 finally 中统一收尾(补发 TurnEnd、遥测 turn_interrupted/turn_ended、取消审批源)。 async def run( self, user_input: str | list[ContentPart], @@ -838,6 +908,7 @@ async def run( reset_current_approval_source(approval_source_token) self._set_trace_id(None) + # 执行一轮:校验能力、做检查点、追加 user 消息,进入 _agent_loop。 async def _turn(self, user_message: Message) -> TurnOutcome: if self._runtime.llm is None: raise LLMNotSet() @@ -851,6 +922,7 @@ async def _turn(self, user_message: Message) -> TurnOutcome: logger.debug("Appended user message to context") return await self._agent_loop() + # 构建 slash 命令列表(内置注册表 + 标准技能 skill:* + 流程技能 flow:*)。 def _build_slash_commands(self) -> list[SlashCommand[Any]]: commands: list[SlashCommand[Any]] = list(soul_slash_registry.list_commands()) seen_names = {cmd.name for cmd in commands} @@ -901,6 +973,7 @@ def _build_slash_commands(self) -> list[SlashCommand[Any]]: return commands + # 把命令列表(含别名)索引成 dict。 @staticmethod def _index_slash_commands( commands: list[SlashCommand[Any]], @@ -912,9 +985,11 @@ def _index_slash_commands( indexed[alias] = command return indexed + # 按名称/别名查找 slash 命令。 def _find_slash_command(self, name: str) -> SlashCommand[Any] | None: return self._slash_command_map.get(name) + # 生成技能 slash 命令的执行器闭包:读取技能文本(可追加用户请求)后作为新 turn 执行。 def _make_skill_runner(self, skill: Skill) -> Callable[[KimiSoul, str], None | Awaitable[None]]: async def _run_skill(soul: KimiSoul, args: str, *, _skill: Skill = skill) -> None: from kimi_cli.telemetry import track @@ -934,6 +1009,8 @@ async def _run_skill(soul: KimiSoul, args: str, *, _skill: Skill = skill) -> Non _run_skill.__doc__ = skill.description return _run_skill + # 单轮主循环:初始化(清理过期 steer、加载 MCP 工具)→ step 循环 + #(守卫/压缩/检查点/执行/错误处理/结果解析)→ 轮结束并返回 TurnOutcome。 async def _agent_loop(self) -> TurnOutcome: """The main agent loop for one run. @@ -1108,6 +1185,7 @@ async def _agent_loop(self) -> TurnOutcome: # Consume any pending steers between steps before next iteration. await self._consume_pending_steers() + # 执行单步:通知投递 → 动态注入 → 历史归一化 → LLM 调用+重试 → 工具执行 → 上下文增长 → 结果解析。 async def _step(self) -> StepOutcome | None: """Run a single step and return a stop outcome, or None to continue. @@ -1345,6 +1423,7 @@ async def _kosong_step_with_retry() -> StepResult: return None return StepOutcome(stop_reason="no_tool_calls", assistant_message=result.message) + # 计算每次调用的生成覆盖参数(主要是 max_completion_tokens),不改动 chat_provider 实例本身。 def _compute_completion_overrides( self, chat_provider: ChatProvider, @@ -1386,6 +1465,7 @@ def _compute_completion_overrides( ) return {"max_completion_tokens": max_completion_tokens} + # 把助手消息与工具结果追加进上下文(工具结果能力校验失败则抛 LLMNotSupported)。 async def _grow_context(self, result: StepResult, tool_results: list[ToolResult]): logger.debug("Growing context with result: {result}", result=result) @@ -1409,6 +1489,7 @@ async def _grow_context(self, result: StepResult, tool_results: list[ToolResult] await self._context.append_message(tool_messages) # token count of tool results are not available yet + # 压缩上下文:准备压缩消息 → 重试调用 → 清空重建历史 → 恢复活跃后台任务快照 → 发事件与遥测。 async def compact_context( self, *, @@ -1643,6 +1724,7 @@ async def _compact_with_retry() -> CompactionResult: ) _hook_task.add_done_callback(lambda t: t.exception() if not t.cancelled() else None) + # 判断异常是否可重试(网络/超时/空响应/特定状态码 429/5xx)。 @staticmethod def _is_retryable_error(exception: BaseException) -> bool: if isinstance(exception, (APIConnectionError, APITimeoutError)): @@ -1657,6 +1739,7 @@ def _is_retryable_error(exception: BaseException) -> bool: 504, # Gateway Timeout ) + # 带连接恢复的执行包装:401 时刷新 OAuth 后重试,连接错误时调用 on_retryable_error 恢复后重试一次。 async def _run_with_connection_recovery( self, name: str, @@ -1742,6 +1825,7 @@ async def _run_with_connection_recovery( _connection_retried=True, ) + # 记录重试日志(第几次、上次错误、等待秒数)。 @staticmethod def _retry_log(name: str, retry_state: RetryCallState): error = retry_state.outcome.exception() if retry_state.outcome else None @@ -1757,6 +1841,7 @@ def _retry_log(name: str, retry_state: RetryCallState): else "unknown", ) + # 发送 StepRetry 事件(当前步、下次尝试、最大尝试、等待秒数、错误信息)。 def _emit_step_retry(self, retry_state: RetryCallState, *, max_attempts: int) -> None: error = retry_state.outcome.exception() if retry_state.outcome else None next_action = retry_state.next_action @@ -1773,6 +1858,7 @@ def _emit_step_retry(self, retry_state: RetryCallState, *, max_attempts: int) -> ) +# 需要回溯到历史检查点(D-Mail 机制)时抛出的异常,主循环捕获后回滚上下文并注入 D-Mail 消息。 class BackToTheFuture(Exception): """ Raise when we need to revert the context to a previous checkpoint. @@ -1784,6 +1870,7 @@ def __init__(self, checkpoint_id: int, messages: Sequence[Message]): self.messages = messages +# 节点图流程执行器:按 flow 定义的节点与边推进,用于技能 flow 与 ralph 自动循环。 class FlowRunner: def __init__( self, @@ -1796,6 +1883,7 @@ def __init__( self._name = name self._max_moves = max_moves + # 构造 ralph 自动循环:同一 prompt 反复投喂,直到模型选择 STOP,返回对应 FlowRunner。 @staticmethod def ralph_loop( user_message: Message, @@ -1836,6 +1924,7 @@ def ralph_loop( max_moves = total_runs return FlowRunner(flow, max_moves=max_moves) + # 沿流程节点图推进直到 END 或超过 max_moves,累计步数并做上限检查。 async def run(self, soul: KimiSoul, args: str) -> None: if args.strip(): command = f"/{FLOW_COMMAND_PREFIX}{self._name}" if self._name else "/flow" @@ -1876,6 +1965,7 @@ async def run(self, soul: KimiSoul, args: str) -> None: moves += 1 current_id = next_id + # 执行单个流程节点;decision 节点解析 结果匹配出边,无效选择则追加提示重试。 async def _execute_flow_node( self, soul: KimiSoul, @@ -1923,6 +2013,7 @@ async def _execute_flow_node( "Reply with one of the choices using ...." ) + # 构建节点提示词:decision 节点附带可选分支列表,并要求以 ... 回复。 @staticmethod def _build_flow_prompt(node: FlowNode, edges: list[FlowEdge]) -> str | list[ContentPart]: if node.kind != "decision": @@ -1943,6 +2034,7 @@ def _build_flow_prompt(node: FlowNode, edges: list[FlowEdge]) -> str | list[Cont ] return "\n".join(lines) + # 用选择结果精确匹配出边(无匹配或为空返回 None)。 @staticmethod def _match_flow_edge(edges: list[FlowEdge], choice: str | None) -> str | None: if not choice: @@ -1952,6 +2044,7 @@ def _match_flow_edge(edges: list[FlowEdge], choice: str | None) -> str | None: return edge.dst return None + # 执行一次流程内子 turn:发 TurnBegin/TurnEnd,调用 soul._turn 并返回 TurnOutcome。 @staticmethod async def _flow_turn( soul: KimiSoul, diff --git a/src/kimi_cli/soul/message.py b/src/kimi_cli/soul/message.py index e8e823d452..073e507c17 100644 --- a/src/kimi_cli/soul/message.py +++ b/src/kimi_cli/soul/message.py @@ -16,14 +16,20 @@ ) +# 本模块提供消息构造与转换的辅助函数:把字符串包装成 system/system-reminder +# 文本部件,把工具结果转成 tool 消息,以及按消息内容检测所需的模型能力。 + +# 把字符串包装成 文本部件。 def system(message: str) -> ContentPart: return TextPart(text=f"{message}") +# 把字符串包装成 文本部件。 def system_reminder(message: str) -> TextPart: return TextPart(text=f"\n{message}\n") +# 判断一条消息是否为内部 system-reminder 类型的 user 消息。 def is_system_reminder_message(message: Message) -> bool: """Check whether a message is an internal system-reminder user message.""" if message.role != "user" or len(message.content) != 1: @@ -32,6 +38,7 @@ def is_system_reminder_message(message: Message) -> bool: return isinstance(part, TextPart) and part.text.strip().startswith("") +# 把工具结果转换为一条 tool 角色消息(含错误/输出内容的规范化处理)。 def tool_result_to_message(tool_result: ToolResult) -> Message: """Convert a tool result to a message.""" if tool_result.return_value.is_error: @@ -62,6 +69,7 @@ def tool_result_to_message(tool_result: ToolResult) -> Message: ) +# 把输出(str / 单个部件 / 部件序列)统一展开为部件列表。 def _output_to_content_parts( output: str | ContentPart | Sequence[ContentPart], ) -> list[ContentPart]: @@ -77,6 +85,7 @@ def _output_to_content_parts( return content +# 检测消息内容所需的模型能力,返回缺失的能力集合。 def check_message( message: Message, model_capabilities: set[ModelCapability] ) -> set[ModelCapability]: diff --git a/src/kimi_cli/soul/slash.py b/src/kimi_cli/soul/slash.py index 7155271fc8..039fb3c269 100644 --- a/src/kimi_cli/soul/slash.py +++ b/src/kimi_cli/soul/slash.py @@ -23,6 +23,9 @@ if TYPE_CHECKING: from kimi_cli.soul.kimisoul import KimiSoul +# 本模块注册 KimiSoul 级 slash 命令(/init、/compact、/clear、/yolo、/afk、 +# /plan、/add-dir、/export、/import),每个命令通过 registry 装饰器注册。 + type SoulSlashCmdFunc = Callable[[KimiSoul, str], None | Awaitable[None]] """ A function that runs as a KimiSoul-level slash command. @@ -34,11 +37,13 @@ registry = SlashCommandRegistry[SoulSlashCmdFunc]() +# /init:分析代码库并生成 AGENTS.md 文件。 @registry.command async def init(soul: KimiSoul, args: str): """Analyze the codebase and generate an `AGENTS.md` file""" from kimi_cli.soul.kimisoul import KimiSoul + # 在临时上下文中运行一个独立的 KimiSoul 来执行 INIT 提示词。 with tempfile.TemporaryDirectory() as temp_dir: tmp_context = Context(file_backend=Path(temp_dir) / "context.jsonl") tmp_soul = KimiSoul(soul.agent, context=tmp_context) @@ -56,6 +61,7 @@ async def init(soul: KimiSoul, args: str): track("init_complete") +# /compact:手动触发上下文压缩,可附加自定义聚焦说明。 @registry.command async def compact(soul: KimiSoul, args: str): """Compact the context (optionally with a custom focus, e.g. /compact keep db discussions)""" @@ -77,6 +83,7 @@ async def compact(soul: KimiSoul, args: str): ) +# /clear(别名 /reset):清空上下文并重新写入系统提示词。 @registry.command(aliases=["reset"]) async def clear(soul: KimiSoul, args: str): """Clear the context""" @@ -94,6 +101,7 @@ async def clear(soul: KimiSoul, args: str): ) +# /yolo:切换 YOLO 模式(自动批准所有动作)。 @registry.command async def yolo(soul: KimiSoul, args: str): """Toggle YOLO mode (auto-approve all actions)""" @@ -122,6 +130,7 @@ async def yolo(soul: KimiSoul, args: str): wire_send(TextPart(text="You only live once! All actions will be auto-approved.")) +# /afk:切换 afk 模式(自动驳回 AskUserQuestion、自动批准工具调用)。 @registry.command async def afk(soul: KimiSoul, args: str): """Toggle afk mode (auto-dismiss AskUserQuestion, auto-approve tool calls)""" @@ -156,6 +165,7 @@ async def afk(soul: KimiSoul, args: str): ) +# /plan:切换 plan 模式,支持 on/off/view/clear 子命令。 @registry.command async def plan(soul: KimiSoul, args: str): """Toggle plan mode. Usage: /plan [on|off|view|clear]""" @@ -197,6 +207,7 @@ async def plan(soul: KimiSoul, args: str): wire_send(StatusUpdate(plan_mode=soul.plan_mode)) +# /add-dir:向工作区添加额外目录。 @registry.command(name="add-dir") async def add_dir(soul: KimiSoul, args: str): """Add a directory to the workspace. Usage: /add-dir . Run without args to list added dirs""" # noqa: E501 @@ -272,6 +283,7 @@ async def add_dir(soul: KimiSoul, args: str): logger.info("Added additional directory: {path}", path=path) +# /export:把当前会话上下文导出为 Markdown 文件。 @registry.command async def export(soul: KimiSoul, args: str): """Export current session context to a markdown file""" @@ -300,6 +312,7 @@ async def export(soul: KimiSoul, args: str): ) +# /import:从文件或会话 ID 导入上下文。 @registry.command(name="import") async def import_context(soul: KimiSoul, args: str): """Import context from a file or session ID""" diff --git a/src/kimi_cli/soul/toolset.py b/src/kimi_cli/soul/toolset.py index 5d66344aaa..d293e1173d 100644 --- a/src/kimi_cli/soul/toolset.py +++ b/src/kimi_cli/soul/toolset.py @@ -56,6 +56,10 @@ from kimi_cli.soul.agent import Runtime +# 本模块实现 KimiToolset:代理 agent 的工具集合。它负责按 import path 加载内置工具、 +# 惰性连接 MCP 工具、按步骤执行工具并注入 Pre/PostToolUse 钩子,同时在执行层 +# 做跨步骤的重复调用检测(去重),向 LLM 追加「重复调用」提醒以避免死循环。 + current_tool_call = ContextVar[ToolCall | None]("current_tool_call", default=None) _current_step_no: ContextVar[int | None] = ContextVar("current_step_no", default=None) @@ -74,6 +78,7 @@ def _get_session_id() -> str: return _current_session_id.get() +# 当前请求的 trace_id 遥测参数;不可用时返回空字典。 def _trace_id_kwargs() -> dict[str, str]: """``trace_id`` telemetry kwargs for the current request, empty when unavailable.""" from kimi_cli.telemetry import get_current_trace_id @@ -83,6 +88,7 @@ def _trace_id_kwargs() -> dict[str, str]: return {} +# 计算规范化参数的稳定 8 位哈希(与 TS args_hash 对齐)。 def _args_hash(canonical_args: str) -> str: """Stable 8-char hash of canonical tool-call arguments (TS args_hash parity).""" import hashlib @@ -90,6 +96,7 @@ def _args_hash(canonical_args: str) -> str: return hashlib.sha256(canonical_args.encode()).hexdigest()[:8] +# 获取当前工具调用;在工具的 __call__ 中调用时应非 None。 def get_current_tool_call_or_none() -> ToolCall | None: """ Get the current tool call or None. @@ -98,6 +105,7 @@ def get_current_tool_call_or_none() -> ToolCall | None: return current_tool_call.get() +# 返回与当前工具任务关联的步骤号。 def get_current_step_no() -> int | None: """Return the step number associated with the current tool task.""" return _current_step_no.get() @@ -113,6 +121,7 @@ def type_check(kimi_toolset: KimiToolset): _: Toolset = kimi_toolset +# 第一次重复提醒(完全相同的工具调用)。 _REMINDER_TEXT_1 = ( "\n\n\n" "You are repeating the exact same tool call with identical parameters." @@ -122,6 +131,7 @@ def type_check(kimi_toolset: KimiToolset): ) +# 构造第二次重复提醒(含工具名、重复次数与参数)。 def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str) -> str: return ( "\n\n\n" @@ -138,6 +148,7 @@ def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str ) +# 第三次(死循环)提醒:要求停止一切工具调用并返回文本总结。 _REMINDER_TEXT_3 = ( "\n\n\n" "You are stuck in a dead end and have repeatedly made the same function call without " @@ -150,6 +161,7 @@ def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str ) +# 触发各级提醒/强制停止的重复次数阈值。 _REPEAT_REMINDER_1_START = 3 _REPEAT_REMINDER_2_START = 5 _REPEAT_REMINDER_3_START = 8 @@ -158,6 +170,7 @@ def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str type RepeatAction = Literal["none", "r1", "r2", "r3", "stop"] +# 按连续重复次数选择要执行的去重动作与提醒文本。 def _build_repeat_reminder( streak: int, tool_name: str, canonical_args: str ) -> tuple[RepeatAction, str | None]: @@ -172,6 +185,7 @@ def _build_repeat_reminder( return "none", None +# 递归地对 JSON 值排序(dict 按 key 排序、list 递归),用于参数规范化。 def _sort_json_value(value: object) -> object: if isinstance(value, list): return [_sort_json_value(item) for item in cast("list[object]", value)] @@ -181,6 +195,7 @@ def _sort_json_value(value: object) -> object: return value +# 把工具参数规范化为稳定的 JSON 字符串。 def _canonical_tool_arguments(arguments: Any) -> str: try: return json.dumps( @@ -192,6 +207,7 @@ def _canonical_tool_arguments(arguments: Any) -> str: return str(arguments) +# 把(可能是字符串形式的)参数解析后规范化。 def _canonical_tool_arguments_text(arguments: str) -> str: try: return _canonical_tool_arguments(json.loads(arguments, strict=False)) @@ -199,10 +215,12 @@ def _canonical_tool_arguments_text(arguments: str) -> str: return arguments +# 由工具名 + 规范化参数构造去重用的调用键。 def _normalize_call_key(tool_name: str, arguments: str) -> ToolCallKey: return (tool_name, _canonical_tool_arguments_text(arguments)) +# 把去重提醒文本追加到 ToolReturnValue 的输出上。 def _append_reminder_to_return_value( return_value: Any, reminder_text: str = _REMINDER_TEXT_1 ) -> Any: @@ -226,6 +244,7 @@ def _append_reminder_to_return_value( return return_value.model_copy(update={"output": new_output}) +# 工具集合:管理内置工具、MCP 工具、步骤级去重与钩子触发。 class KimiToolset: def __init__(self) -> None: self._tool_dict: dict[str, ToolType] = {} @@ -250,9 +269,11 @@ def __init__(self) -> None: def set_hook_engine(self, engine: HookEngine) -> None: self._hook_engine = engine + # 注册一个工具。 def add(self, tool: ToolType) -> None: self._tool_dict[tool.name] = tool + # 从 LLM 工具列表中隐藏某工具,成功则返回 True。 def hide(self, tool_name: str) -> bool: """Hide a tool from the LLM tool list. Returns True if the tool exists.""" if tool_name in self._tool_dict: @@ -260,10 +281,12 @@ def hide(self, tool_name: str) -> bool: return True return False + # 恢复被隐藏的工具。 def unhide(self, tool_name: str) -> None: """Restore a hidden tool to the LLM tool list.""" self._hidden_tools.discard(tool_name) + # 按名称或类型查找工具。 @overload def find(self, tool_name_or_type: str) -> ToolType | None: ... @overload @@ -277,12 +300,14 @@ def find(self, tool_name_or_type: str | type[ToolType]) -> ToolType | None: return tool return None + # 返回当前对 LLM 可见的工具列表(已隐藏的除外)。 @property def tools(self) -> list[Tool]: return [ tool.base for tool in self._tool_dict.values() if tool.name not in self._hidden_tools ] + # 每步开始前调用:初始化去重状态并承接上一步的调用记录。 def begin_step(self, previous_calls: list[tuple[str, str]], *, step_no: int = 0) -> None: """Called before each step to set up deduplication state.""" self._current_step_no = step_no @@ -304,6 +329,7 @@ def begin_step(self, previous_calls: list[tuple[str, str]], *, step_no: int = 0) if self._consecutive_key is None and self._consecutive_count == 0: self._advance_consecutive_streak(self._previous_step_calls) + # 每步结束后调用:推进连续重复计数并返回本步的调用记录。 def end_step(self) -> list[tuple[str, str]]: """Called after each step to capture the calls made in this step.""" if not self._step_closed: @@ -312,6 +338,7 @@ def end_step(self) -> list[tuple[str, str]]: self._step_closed = True return list(self._current_step_calls) + # 推进「连续相同调用」计数(用于判断重复)。 def _advance_consecutive_streak(self, calls: list[ToolCallKey]) -> None: for call_key in calls: if call_key == self._consecutive_key: @@ -320,6 +347,7 @@ def _advance_consecutive_streak(self, calls: list[ToolCallKey]) -> None: self._consecutive_key = call_key self._consecutive_count = 1 + # 预测某个调用在本步结束后会达到的连续重复次数。 def _projected_streak_for_call(self, call_index: int) -> int: consecutive_key = self._consecutive_key consecutive_count = self._consecutive_count @@ -331,15 +359,18 @@ def _projected_streak_for_call(self, call_index: int) -> int: consecutive_count = 1 return consecutive_count + # 本步是否拦截过跨步重复调用。 @property def dedup_triggered(self) -> bool: """Whether a cross-step duplicate was blocked in the current step.""" return self._dedup_triggered + # 是否应强制结束本回合(触发了死循环停止)。 @property def force_stop_turn(self) -> bool: return self._force_stop_turn + # 处理一次工具调用:查表、解析参数、去重、执行并注入钩子,返回结果(可能是 task)。 def handle(self, tool_call: ToolCall) -> HandleResult: token = current_tool_call.set(tool_call) try: @@ -382,6 +413,7 @@ def handle(self, tool_call: ToolCall) -> HandleResult: ) original_task = self._current_step_tasks[call_key] + # 等待原始任务完成后复用其结果。 async def _await_dup() -> ToolResult: t0 = time.monotonic() try: @@ -422,6 +454,7 @@ async def _await_dup() -> ToolResult: return asyncio.create_task(_await_dup()) + # 跨步重复检测:若本调用在历史中已出现,则按连续次数附加提醒。 is_cross_step_dup = call_key in self._seen_call_keys reminder_text: str | None = None if is_cross_step_dup: @@ -453,6 +486,7 @@ async def _await_dup() -> ToolResult: tool = self._tool_dict[tool_name] + # 实际执行工具:触发 PreToolUse 钩子 -> 调用 -> 触发 PostToolUse 钩子。 async def _call(): tool_input_dict = arguments if isinstance(arguments, dict) else {} @@ -595,6 +629,7 @@ async def _call(): task = asyncio.create_task(_call()) if reminder_text is not None: + # 包一层,在工具结果返回后追加去重提醒文本。 async def _wrap_with_reminder( inner_task: asyncio.Task[ToolResult], text: str, @@ -612,6 +647,7 @@ async def _wrap_with_reminder( finally: current_tool_call.reset(token) + # 注册一个外部(wire 侧)工具。 def register_external_tool( self, name: str, @@ -638,6 +674,7 @@ def mcp_servers(self) -> dict[str, MCPServerInfo]: """Get MCP servers info.""" return self._mcp_servers + # 返回当前 MCP 启动状态的只读快照。 def mcp_status_snapshot(self) -> MCPStatusSnapshot | None: """Return a read-only snapshot of current MCP startup state.""" if not self._mcp_servers: @@ -659,14 +696,17 @@ def mcp_status_snapshot(self) -> MCPStatusSnapshot | None: servers=servers, ) + # 暂存 MCP 配置,供之后在后台启动加载。 def defer_mcp_tool_loading(self, mcp_configs: list[MCPConfig], runtime: Runtime) -> None: """Store MCP configs for a later background startup.""" self._deferred_mcp_load = (list(mcp_configs), runtime) + # 是否配置了但尚未开始的 MCP 加载。 def has_deferred_mcp_tools(self) -> bool: """Return True when MCP loading is configured but has not started yet.""" return self._deferred_mcp_load is not None + # 启动暂存的 MCP 加载(后台执行),返回是否真的启动了。 async def start_deferred_mcp_tool_loading(self) -> bool: """Start any deferred MCP loading in the background.""" if self._deferred_mcp_load is None: @@ -680,6 +720,7 @@ async def start_deferred_mcp_tool_loading(self) -> bool: await self.load_mcp_tools(mcp_configs, runtime, in_background=True) return True + # 按 import path 加载一批工具并注入依赖。 def load_tools(self, tool_paths: list[str], dependencies: dict[type[Any], Any]) -> None: """ Load tools from paths like `kimi_cli.tools.shell:Shell`. @@ -706,6 +747,7 @@ def load_tools(self, tool_paths: list[str], dependencies: dict[type[Any], Any]) if bad_tools: raise InvalidToolError(f"Invalid tools: {bad_tools}") + # 加载单个工具:导入模块 -> 取类 -> 按构造签名注入依赖。 @staticmethod def _load_tool(tool_path: str, dependencies: dict[type[Any], Any]) -> ToolType | None: logger.debug("Loading tool: {tool_path}", tool_path=tool_path) @@ -741,6 +783,7 @@ def _load_tool(tool_path: str, dependencies: dict[type[Any], Any]) -> ToolType | return tool_cls(*args) # TODO(rc): remove `in_background` parameter and always load in background + # 加载 MCP 工具:处理 OAuth、连接服务器、列出并注册工具。 async def load_mcp_tools( self, mcp_configs: list[MCPConfig], runtime: Runtime, in_background: bool = True ) -> None: @@ -867,10 +910,12 @@ async def _connect(): else: await _connect() + # 后台 MCP 加载任务是否仍在运行。 def has_pending_mcp_tools(self) -> bool: """Return True if the background MCP tool-loading task is still running.""" return self._mcp_loading_task is not None and not self._mcp_loading_task.done() + # 等待后台 MCP 加载完成。 async def wait_for_mcp_tools(self) -> None: """Wait for background MCP tool loading to finish.""" task = self._mcp_loading_task @@ -882,6 +927,7 @@ async def wait_for_mcp_tools(self) -> None: if self._mcp_loading_task is task and task.done(): self._mcp_loading_task = None + # 清理资源:取消后台加载、关闭 MCP 客户端。 async def cleanup(self) -> None: """Cleanup any resources held by the toolset.""" self._deferred_mcp_load = None @@ -897,6 +943,7 @@ async def cleanup(self) -> None: logger.warning("Failed to close MCP client", exc_info=True) +# 单个 MCP 服务器的连接状态与工具列表。 @dataclass(slots=True) class MCPServerInfo: status: Literal["pending", "connecting", "connected", "failed", "unauthorized"] @@ -904,6 +951,7 @@ class MCPServerInfo: tools: list[MCPTool[Any]] +# 包装一个 MCP 工具为 kimi 工具,执行前走审批、执行时透传到 MCP 客户端。 class MCPTool[T: ClientTransport](CallableTool): def __init__( self, @@ -929,6 +977,7 @@ def __init__( self._timeout = timedelta(milliseconds=runtime.config.mcp.client.tool_call_timeout_ms) self._action_name = f"mcp:{mcp_tool.name}" + # 请求审批后调用 MCP 客户端执行工具,并转换结果。 async def __call__(self, *args: Any, **kwargs: Any) -> ToolReturnValue: description = f"Call MCP tool `{self._mcp_tool.name}`." result = await self._runtime.approval.request(self.name, self._action_name, description) @@ -974,6 +1023,7 @@ async def __call__(self, *args: Any, **kwargs: Any) -> ToolReturnValue: raise +# 外部(wire 侧)工具:把调用转发给 wire 等待外部执行结果。 class WireExternalTool(CallableTool): def __init__(self, *, name: str, description: str, parameters: dict[str, Any]) -> None: super().__init__( @@ -1023,6 +1073,7 @@ async def __call__(self, *args: Any, **kwargs: Any) -> ToolReturnValue: MCP_MAX_OUTPUT_CHARS = 100_000 +# 返回媒体部件(图片/音频/视频)的数据大小,非媒体返回 None。 def _media_part_size(part: ContentPart) -> int | None: """Return the payload size of a media part, or ``None`` for non-media parts.""" if isinstance(part, ImageURLPart): @@ -1034,6 +1085,7 @@ def _media_part_size(part: ContentPart) -> int | None: return None +# 把 MCP 工具结果转换为 kosong 返回值,并对文本/媒体执行共享字符预算截断。 def convert_mcp_tool_result(result: CallToolResult) -> ToolReturnValue: """Convert MCP tool result to kosong tool return value.