跳到主要内容

抹平两层差异:kosong 抹平大模型,kaos 抹平执行环境

30 秒导读: 一个编码 agent 的上层逻辑(回合循环、工具、上下文管理)最怕两件事天天变:换大模型(Kimi/Claude/GPT/Gemini 的协议各不相同)和换执行环境(本地跑还是 SSH 到远程跑)。kimi-code 用两个"适配层"包把这两处差异关进盒子:packages/kosong 让上层只面对一套消息/工具/用量类型,由适配器落到各家协议;packages/kaos 让所有文件/进程操作只面对一套 Kaos 接口,由实现落到本地或远程。上层代码对"用哪个模型、在哪台机器上跑"基本无感。

本章只讲这两个适配层。工具本身怎么实现看 03-tools.md;回合循环怎么转看 01-loop.md;Agent 主机层怎么拼装看 02-agent.md


1. 这是什么(零基础也能懂)

先建立一个直觉:适配层就是翻译官 + 统一插座

  • kosong = 大模型翻译官。 上层想说"把这段对话发给模型,带上这些工具,流式收回复"。它只会说一种"普通话"(kosong 的 Message / Tool / TokenUsage 类型)。kosong 里每个 provider 适配器负责把普通话翻译成某一家模型的"方言"(Kimi 的 OpenAI 兼容协议、Anthropic 的 Messages API、Google 的 GenAI……),再把方言的流式回复翻译回普通话。
  • kaos = 执行环境统一插座。 上层想说"读这个文件""跑这条命令"。它只往一个插座 Kaos 上插。插座背后接的是本地机器(LocalKaos)还是一条 SSH 隧道(SSHKaos),上层不关心。

两者的共同价值观是同一句话:

把"易变的外部世界"收敛成一个稳定接口,让上层只依赖接口,不依赖具体家数。

给谁用、解决什么问题: 给 Agent 主机层(agent-core)用。它想要"换个模型只改配置、不改逻辑""同一套工具既能在本地跑也能远程跑"。没有这两层,每加一家模型、每支持一种远程执行,上层就要改一遍——适配层就是拦住这种扩散的墙。

一个最小直觉(kosong 的调用长这样):

// 示意,非源码:上层只碰这一套类型
const provider = createProvider({ type: 'anthropic', model: 'claude-...', apiKey });
const result = await generate(
provider, // 哪家模型,由 type 决定
'你是一个编码助手', // system prompt
[readFileTool, bashTool], // 一套 Tool
[createUserMessage('帮我改 bug')], // 一套 Message
);
result.message.toolCalls; // 收回来还是同一套类型

换成 type: 'kimi'type: 'google-genai',上面这段一个字不用改——这就是"抹平"。


2. 顶层全景(它大概怎么转)

两层各守一条边界。先看它们在整条链路里的位置:

agent-core(回合循环 / 工具 / 上下文)
│ │
想调模型 │ │ 想读文件/跑命令
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ kosong (LLM 抽象) │ │ kaos (执行抽象) │
│ 一套 Message/Tool │ │ 一套 Kaos 接口 │
│ 一套 generate 循环 │ │ 文件 + 进程 + 环境 │
└─────────┬───────────┘ └──────────┬──────────┘
│ 适配器翻译 │ 实现落地
┌─────────┼──────────┐ ┌─────────┴─────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
Kimi Anthr OpenAI Google … LocalKaos SSHKaos
(各家 HTTP 协议) (本机进程/fs) (ssh2 隧道)

两层职责一句话:

抹平的是上层看到的统一物底层落到
LLM 抽象packages/kosong各家大模型协议差异Message / Tool / TokenUsage / generate()Kimi / Anthropic / OpenAI(两代)/ Google 的 HTTP 契约
执行抽象packages/kaos本地 vs 远程执行差异Kaos 接口(fs + 进程 + 环境探测)LocalKaos(node fs / child_process)、SSHKaos(ssh2)

两层之间互不知道对方存在:kosong 只管消息进出模型,kaos 只管文件与进程。把它们缝到一起的是 agent-core——见 §5 接缝

主线走一遍(高层):

  1. agent-core 用 createProvider(config) 造出一个 ChatProvider,再用 getModelCapability(wire, model) 查这个模型支不支持图片/视频/thinking。
  2. 每一回合,agent-core 调 kosong 的 generate(provider, ...),拿回一条组装好的 assistant 消息(文本 + thinking + 工具调用)。
  3. 消息里若有工具调用,agent-core 去执行工具;工具里所有"读文件、跑命令"都打到 Kaos 上,由当前绑定的实现(本地或远程)真正落地。
  4. 工具结果作为新的 Message 回到第 2 步,进入下一回合。

3. kosong 抹平大模型(上半)

这一层的全部工作可以浓缩成一句:上层只面对一套类型,由适配器落到各家协议。 下面拆成"统一的类型 → 统一的循环 → 各家的翻译 → 能力位穿针"四步,由浅入深。

3.1 统一的语言:一套消息 / 工具 / 用量

kosong 先定义了所有 provider 都必须说的"普通话",这些类型在 packages/kosong/src/ 根目录:

类型文件是什么
Messagemessage.ts:92一条消息:role + 内容块数组 + 工具调用数组
ContentPartmessage.ts:38内容块联合:text / think / image_url / audio_url / video_url
Tooltool.ts:8工具定义:name + description + JSON Schema 参数
TokenUsageusage.ts:7四栏用量:普通输入 / 输出 / 缓存读 / 缓存写
StreamedMessagePartmessage.ts:82流式片段:一个 ContentPart、一个 ToolCall、或一段工具参数增量

关键设计:内容是"块数组"而不是一根字符串(Message.content: ContentPart[])。因为多模态、thinking、工具调用要并存,一根字符串装不下。think(模型的推理内容)是一等公民块,和普通 text 分开——各家适配器再决定怎么把它塞进自家协议(Kimi 落到 reasoning_content 字段,Anthropic 落到 thinking 块)。

用量为什么是四栏而不是"输入/输出两栏"?因为 prompt 缓存要单独记账:inputCacheRead(命中缓存的便宜 token)和 inputCacheCreation(写缓存)必须和 inputOther 分开,上层才能算准成本(usage.ts:7 TokenUsage,inputTotal() 把三种输入相加)。

3.2 统一的循环:generate()ChatProvider 契约

上层只调一个函数——generate()(generate.ts:87),它是"发一次、收一条组装好的消息"的唯一入口。它对 provider 的全部要求写在一个接口里:ChatProvider(provider.ts:220)。

ChatProvider 的核心成员:

成员签名要点干什么
generate(...)provider.ts:246把 system+tools+history 发出去,返回一个 StreamedMessage(可 for await 的流)
withThinking(effort)provider.ts:253返回一个"带某档思考强度"的浅拷贝
withMaxCompletionTokens(cap, opts)provider.ts:268返回一个"补全预算被夹到 cap"的浅拷贝(可选)
uploadVideo(input, opts)provider.ts:273上传视频、返回一个可放进消息的 video_url 块(可选)
readonly thinkingEffort / maxCompletionTokensprovider.ts:226/237让 host 读到"这实例实际会往线上发什么"

为什么 generate() 是自由函数,不是 provider 的方法? 因为流式片段的组装逻辑是所有 provider 共享的,不该每家抄一遍。provider 只负责把自家的流翻成 StreamedMessagePart 序列;generate() 统一负责把这些片段合并成最终消息。

这个"怎么读流"很关键,单独讲。generate() 的核心是一个 for await 循环(generate.ts:145),它做三件事:

每收到一个 StreamedMessagePart:
① 相邻同类片段就地合并(mergeInPlace)
text+text → 拼接; think+think → 拼接; ToolCall+参数增量 → 追加参数
② 合并不了(来了个新块)→ 把 pending 冲刷进 message,再开新的 pending
③ 并行工具调用的参数增量,按 index 路由到正确的那个调用(见下)

真源码:合并规则在 mergeInPlace(message.ts:171),冲刷在 flushPart(generate.ts:317)。"工具调用何时算完整"不是靠一个 done 事件,而是靠合并边界推断——来了个不能合并的片段,就说明上一个工具调用收全了(generate.ts 注释,onToolCall 特意推迟到流结束后按最终顺序触发,见 generate.ts:247-253)。

一个不显然的坑:并行工具调用的参数会交错。 OpenAI 系流式可能这样发:调用0的头 → 调用1的头 → 调用0的参数 → 调用1的参数。若只按"追加到最近一个"顺序合并,参数就会串到错误的调用上。kosong 的解法是给每个片段带一个 index,generate() 用一张 toolCallIndexMap 把增量路由回正确的调用(generate.ts:167-184)。这是"抹平"里最容易被忽视、却决定正确性的一处。

3.3 各家的翻译:六个适配器

createProvider(config)(providers/index.ts:25)按 config.type 分派到具体适配器。六个 wire 类型:

type适配器类底层 SDK / 协议特色处理
kimiKimiChatProvider(kimi.ts:394)OpenAI 兼容 + Moonshot 私有扩展reasoning_contentmessages[].tools 动态工具、$ 内建函数
anthropicAnthropicChatProvider(anthropic.ts:875)@anthropic-ai/sdk Messages API必填 max_tokensthinking 块、interleaved-thinking beta
openaiOpenAILegacyChatProvider(openai-legacy.ts:466)OpenAI Chat Completions与 Kimi 共享 finish_reason / usage 解析
openai_responsesOpenAIResponsesChatProvider(openai-responses.ts:1014)OpenAI Responses APIreasoning item、加密推理内容回传
google-genaiGoogleGenAIChatProvider(google-genai.ts:730)Google GenAIinlineData/fileData 多模态、thinkingBudget/thinkingLevel
vertexai同上(复用 GenAI 类)Vertex 变体google-genai 同实现

它们的翻译工作可以归纳成"三进三出":

进(kosong → 各家): 出(各家 → kosong):
Message → 各家消息格式 各家流事件 → StreamedMessagePart
Tool → 各家工具/函数声明 各家 usage → TokenUsage
ContentPart(含 think/媒体)→ 各家块 各家 finish → FinishReason

几处共享的翻译零件,避免每家重抄,值得单独记:

  • OpenAI 系共享层 openai-common.ts:toolToOpenAI(:80)、extractUsage(:160,含 Moonshot 私有 cached_tokens)、normalizeOpenAIFinishReason(:206,把 stop/length/content_filter 映射到统一枚举)。Kimi 和 OpenAI-legacy 都用它。
  • 流式工具调用解码 chat-completions-stream.ts:29 convertChatCompletionStreamToolCall:把 OpenAI 风格的增量(可能参数先于函数名到)缓冲、补齐,再发成带 index 的片段,供 §3.2 的路由用。
  • 工具调用 id 归一化 tool-call-id.ts:21 normalizeToolCallIdsForProvider:各家对 id 的字符集/长度限制不同(Kimi 限 64 字符、仅 [A-Za-z0-9_-],见 kimi.ts:86 的 policy)。这一步在发送前把历史里的 id 批量改写成合法且不重复的版本。
  • 连续 user 回合合并 merge-user-messages.ts:40 mergeConsecutiveUserMessages:Anthropic、Gemini 严格要求 user/assistant 交替,连着两个 user 会被拒(HTTP 400)。压实后(compaction)或"工具结果后紧跟用户插话"都会产生连续 user 回合,所以这两家在转换边界统一压平;宽松的 OpenAI/Kimi 保留原结构。把算法放一处,防止某家漏做(这正是当年 Gemini 回归 bug 的根因,见该文件注释)。

举一个"同一个概念、各家落点不同"的例子——thinking(思考强度):

providerwithThinking('high') 落到哪依据
Kimiextra_body.thinking = { type:'enabled', effort:'high' }kimi.ts:567
Anthropic按模型 profile 选 budget/adaptive 模式anthropic.ts:1185 + anthropic-profile.ts:113
OpenAI Responsesreasoning_effort = 'high'openai-responses.ts:1147
Google映射成 thinkingLevel(gemini-3)或 thinkingBudget 数值google-genai.ts:920

上层只说一句 withThinking('high'),四家各自翻译。Anthropic 尤其讲究:anthropic-profile.ts 维护一张"模型家族 → 支持哪些 effort 档、能不能关思考"的矩阵(matchKnownAnthropicModelProfile,:113),连 max_tokens 默认值都按模型版本夹到官方上限(resolveDefaultMaxTokens,anthropic.ts:256)——因为 Anthropic 的 max_tokens 是必填,发大了会被服务端拒。

3.4 能力位:视频输入如何一路穿到工具

不是每个模型都能吃图片/视频/音频。kosong 用 ModelCapability(capability.ts:11)这张能力位描述一个模型接受哪些模态:

interface ModelCapability {
image_in; video_in; audio_in; thinking; tool_use;
max_context_tokens; dynamically_loaded_tools?;
}

能力从两条路来,再合并:

  1. 静态表 getModelCapability(wire, model)(providers/index.ts:54)——纯查表,不建 provider。按模型名前缀匹配,比如 getGoogleGenAIModelCapability(capability-registry.ts:192)判定 Gemini 系天然支持 image/video/audio。Kimi wire 返回 UNKNOWN_CAPABILITY,因为它的能力来自 host 的目录配置而非模型名。
  2. 目录/配置 catalog.ts——catalogModelToCapability(:122)把 models.dev 风格的公开目录条目(modalities.input 里有没有 video)归一成同一个 ModelCapability

一条"视频输入"能力如何从这里一路穿到工具,是理解这层设计的最好案例:

① 静态表/目录 → ModelCapability{ video_in: true }
│ agent-core: resolveModelCapabilities 合并声明与探测
▼ (provider-manager.ts:228)
② KosongLLM.capability (kosong-llm.ts:72)

├─(a) 发送前:downgradeUnsupportedMedia 剔除模型吃不下的媒体
│ 把 image/video/audio 块换成占位文本 (kosong-llm.ts:300)

└─(b) 工具侧:capability.video_in 为真才挂"视频上传"能力
createVideoUploader(provider) 绑定 provider.uploadVideo
(agent/tool/index.ts:779)


③ provider.uploadVideo → KimiFiles.uploadVideo 上传到 Moonshot 文件服务
返回 video_url 块(url = ms://<file-id>) (kimi-files.ts:75)

要点:能力位是"双向闸门"。发送方向(a),模型吃不下的媒体在 downgradeUnsupportedMedia 被降级成占位文字,避免请求打过去才 400。工具方向(b),只有 video_in 为真才给模型挂上"能读视频"的工具,且这个工具的落地是 provider 自己的 uploadVideo——目前只有 Kimi 实现了(KimiFiles,kimi-files.ts:40),它把视频传到 Moonshot 文件服务、换回一个 ms:// 引用块。其它 provider 的 uploadVideoundefined,createVideoUploader 直接返回 undefined,工具就不挂——能力开关一路贯穿到底。

3.5 认证与克隆:短命凭证不进长命状态

一处工程细节值得学:per-request 认证。OAuth bearer token 是短命的,不能塞进长命的 SDK client。resolveAuthBackedClient(request-auth.ts:54)定义了统一优先级:有 clientFactory 就用它;没有 per-request auth 且有缓存 client 就复用;否则临时 build(auth) 一个新 client。走 OAuth 路径时每次请求现造 client,把短命凭证挡在共享状态之外(注释里坦白:牺牲了连接池复用,但对"一回合一次调用"的工作负载可接受)。

与之配套的是 provider 的浅拷贝语义:withThinking / withMaxCompletionTokens 都返回克隆(kimi.ts:_clone,:640),而克隆共享底层 _client——因为每回合都要按当前输入大小夹一次预算(withMaxCompletionTokens),克隆必须廉价。源码注释特意警告:永远别在克隆上替换 _client,否则原实例会拿到一个被关掉的 socket。


4. kaos 抹平执行环境(下半)

上半把"模型"关进盒子,下半把"在哪台机器上跑"关进盒子。名字是 KAOS = Kimi Agent Operating System——它就是给 agent 的一层"操作系统门面"。

4.1 一个接口收下所有文件与进程操作

核心是 Kaos 接口(kaos.ts:12)。它把 agent 需要的一切外部副作用收成四组方法:

代表方法说明
路径(同步)pathClass() / normpath() / gethome() / getcwd()posix vs win32 由环境决定
目录(异步)iterdir() / glob() / mkdir() / chdir()遍历、匹配、建目录
文件(异步)readText() / readBytes() / writeText() / stat()读写、元数据,readTexterrors 参数照搬 Python open(...)
进程exec(...args) / execWithEnv(args, env)返回一个 KaosProcess
派生withCwd(cwd) / withEnv(env)返回换了 cwd/环境的 Kaos

进程被抽象成 KaosProcess(process.ts:10):stdin/stdout/stderr 三条流 + pid + wait() + kill() + dispose()。注释点明它"刻意最小",这样才能同时被本地子进程、SSH 会话、容器运行时实现。

所有文件/进程类工具最终都打到这个接口上。 比如 Bash 工具持有一个 Kaos,执行时走 this.kaos.execWithEnv(...)(tools/builtin/shell/bash.ts:297),而从不直接碰 node:child_process(该文件顶注:Execution goes through Kaos, never directly via node:child_process)。文件读工具同理,走 kaos.readBytes(tools/builtin/file/read.ts:109)。这就是"抹平"在下半的兑现:工具代码不知道自己在本地还是远程。

4.2 两个实现:本地 vs 远程

同一个接口,两套落地:

实现文件背后造法
LocalKaoslocal.ts:188node 的 fs / child_processawait LocalKaos.create()(:215)
SSHKaosssh.ts:435ssh2 库的 SFTP + exec 通道await SSHKaos.create(options)(:483)

对上层来说两者可互换:exec 在本地是 spawn 子进程(local.ts:727),在远程是开一条 ssh exec 通道(ssh.ts:855);readText 在本地读磁盘,在远程走 SFTP。withCwd/withEnv 在两边都返回一个共享连接、只换了 cwd/环境层的新实例(local.ts:224ssh.ts:466)。想把整个 agent 从本地搬到远程,只需在启动时绑定一个 SSHKaos 而不是 LocalKaos——工具、循环、prompt 全都不用动。

边界诚实:SSHKaos.osEnv 目前会抛"远程环境探测尚未接线"(ssh.ts:448),即远程还没做完整的 OS/shell 探测。所以本地是完整实现,SSH 是骨架+大部分 fs/exec 已可用。

4.3 谁是"当前"环境:AsyncLocalStorage 绑定

工具不是每次都被显式塞一个 Kaos——很多模块函数直接调 getCurrentKaos()(current.ts:16)。"当前是哪个 Kaos"用 Node 的 AsyncLocalStorage 存(current.ts):

setCurrentKaos(kaos) 一次性绑定(进程启动) current.ts:33
runWithKaos(kaos, fn) 给某段异步子树绑定,互不串 current.ts:42
getCurrentKaos() 读当前;没绑定就抛清晰错误 current.ts:16

current.ts 还把接口方法都导出成模块级便捷函数(readText/exec/glob……,:48 起),内部都是 getCurrentKaos().xxx。好处:调用点写 import { readText } from '@moonshot-ai/kaos' 就行,不用到处传 Kaos 实例;runWithKaos 保证并发的不同 agent 上下文各绑各的、不互相污染。agent-core 在需要处 LocalKaos.create() 并绑定(如 rpc/core-impl.ts:1163agent/replay/build.ts:13)。

4.4 环境探测:让同一套命令在 mac/Linux/Windows 都能跑

Environment(environment.ts:29)是对目标机 OS/shell 的一次探测:osKind / osArch / osVersion / shellName / shellPathKaos.osEnv 持有它,Bash 工具据此决定用哪个 shell、渲染哪套命令描述(bash.ts:231)。

两个探测函数都写成纯函数 + 注入依赖,好处是同一套测试能在任意 host OS 上跑:

  • detectEnvironment(deps)(environment.ts:75):POSIX 上在 /bin/bash/usr/bin/bash→…里找 bash,找不到退回 /bin/sh;Windows 上定位 Git Bash(顺着 git.exegit --exec-path、常见安装路径找,找不到抛 KaosShellNotFoundError 带安装提示)。生产用 detectEnvironmentFromNode()(:250)喂 Node 默认值并记忆化(环境一辈子不变)。
  • probeLoginShellPath(deps)(login-shell-path.ts:47):解决一个真实痛点——从 GUI 启动器起的 kimi-code,process.env.PATH 可能缺 /opt/homebrew/bin,导致 Bash 工具找不到 gh 等命令。它跑一次用户登录 shell($SHELL -l -c /usr/bin/env)抽出其 PATH,把当前 PATH 缺的绝对路径条目追加进去(mergeLoginShellPath,:92)。安全细节:只导入绝对路径条目(空/./相对项都是 cwd 相关的查找,追加会扩大搜索路径),且用绝对路径 /usr/bin/env 调用,避免仓库里植入的 env 二进制在启动时被执行。

5. 接缝:与 agent-core 怎么缝

两层自己不认识 agent-core;缝合发生在 agent-core 一侧。最主要的一条缝是 packages/agent-core/src/agent/turn/kosong-llm.ts

KosongLLM(kosong-llm.ts:69)实现回合循环要的 LLM 契约,内部把它翻译成 kosong 的 generate()。它做三件事:

  1. 持有 provider 与 capability。 构造时收下 ChatProviderModelCapability(能力由 provider-manager.ts:233getModelCapability 探测 + 用户声明合并而来,resolveModelCapabilities)。
  2. 每次 chat 现算预算、发一次。 chat()(:89)先用 applyCompletionBudget 在一个丢弃式浅拷贝上夹补全预算(:114,不回写 this.provider,好让上层重试仍用同一个长命 client),再调 generate()。发送前对消息跑 downgradeUnsupportedMedia(§3.4a)剔除模型吃不下的媒体。
  3. 把 kosong 的流回调翻译成 loop 的 per-delta 回调。 buildKosongCallbacks(:204)把 kosong 的 onMessagePart 拆成 onTextDelta / onThinkDelta / onToolCallDelta,并且在这里也做一遍并行工具调用的 index 路由(把交错的参数增量对到正确的 toolCallId,:263-288)——这样上层 UI 能边流边显示正确的工具参数。

第二条缝在工具侧:agent-core 造工具时用 provider.uploadVideo 拼出"视频上传器"(agent/tool/index.ts:779,见 §3.4b)。

kaos 一侧的缝更简单:agent-core 在启动/请求处 LocalKaos.create()setCurrentKaos,工具构造时把 Kaos 注入进去,之后工具只认接口。


6. 巧妙之处(可借鉴)

  • "何时算完整"用合并边界推断,而非 done 事件。 provider 只需把自家流翻成片段序列,generate() 统一判定工具调用收全没(generate.ts flushPart)。少一类每家都要正确实现的 done 事件,就少一处出错点。
  • 并行工具调用双层 index 路由。 kosong 内一层(generate.ts:167)保证组装正确,agent-core 回调再一层(kosong-llm.ts:263)保证流式 UI 正确。交错参数不串台。
  • 共享翻译零件按"协议家族"而非"厂商"归组。 OpenAI-common 被 Kimi 和 OpenAI-legacy 共用;连续 user 合并被 Anthropic 和 Gemini 共用。归组维度是"谁的 wire 长得像",不是"谁家的品牌"。
  • 能力位是双向闸门,一路贯穿到工具。 同一个 video_in 既在发送侧降级媒体,又在工具侧决定挂不挂上传器——一个开关,两处生效,不会出现"挂了工具但模型其实不支持"的错配。
  • 短命凭证永不进长命状态。 resolveAuthBackedClient 让 OAuth 路径每请求现造 client,克隆共享 _client 保持廉价——两条约束都写进注释当护栏。
  • 探测全是纯函数 + 注入依赖。 detectEnvironment / probeLoginShellPathplatform/env/execFileText 都作为参数注入,同一套测试跨 OS 复用,生产入口再喂 Node 默认值并记忆化。

7. 边界与局限(诚实)

  • SSH 是骨架。 SSHKaos.osEnv 明确抛"未接线"(ssh.ts:448),远程环境探测未实现;远程用法目前偏向 fs/exec,完整 OS 探测还没做。
  • 视频上传只 Kimi 有。 其余 provider 的 uploadVideoundefined(provider.ts:273 标可选);非 Kimi 模型即便声明 video_in,也没有配套上传落地。
  • getModelCapability 对 Kimi wire 返回 UNKNOWN(providers/index.ts:66):Kimi 的能力必须由 host 的目录/配置补(resolveModelCapabilities 里合并),否则能力位是全 false 的 UNKNOWN_CAPABILITY
  • 能力位靠模型名前缀匹配(capability-registry.ts):上游发了个新命名而前缀没覆盖,就落到 UNKNOWN_CAPABILITY——保守失败(当作不支持),不会误报支持。
  • login-shell PATH 探测只在 POSIX 跑(login-shell-path.ts:48 win32 直接返回),且失败静默(坏 profile / 无 shell → PATH 原样不动)。

8. 代码地图(导航索引)

用符号名 grep 比行号抗漂移。下表是读这两层该打开的关键锚点。

主题文件符号
generate 循环 / 流片段合并packages/kosong/src/generate.tsgenerateflushParttoolCallIndexMap
provider 契约packages/kosong/src/provider.tsChatProviderThinkingEffortFinishReasonStreamedMessage
统一消息/工具/用量类型packages/kosong/src/{message,tool,usage}.tsMessageContentPartmergeInPlaceToolTokenUsage
provider 分派 / 静态能力表packages/kosong/src/providers/index.tscreateProvidergetModelCapability
能力位定义packages/kosong/src/capability.tsModelCapabilityUNKNOWN_CAPABILITY
能力前缀匹配packages/kosong/src/providers/capability-registry.tsgetAnthropicModelCapabilitygetGoogleGenAIModelCapability
目录归一(models.dev)packages/kosong/src/catalog.tscatalogModelToCapabilityinferWireTypecatalogBaseUrl
Kimi 适配器packages/kosong/src/providers/kimi.tsKimiChatProviderconvertMessagewithThinking_clone
Anthropic 适配器 + profilepackages/kosong/src/providers/{anthropic,anthropic-profile}.tsAnthropicChatProviderresolveDefaultMaxTokensmatchKnownAnthropicModelProfile
OpenAI 共享翻译零件packages/kosong/src/providers/openai-common.tstoolToOpenAIextractUsagenormalizeOpenAIFinishReason
流式工具调用解码packages/kosong/src/providers/chat-completions-stream.tsconvertChatCompletionStreamToolCall
工具调用 id 归一packages/kosong/src/providers/tool-call-id.tsnormalizeToolCallIdsForProvidersanitizeToolCallId
连续 user 回合合并packages/kosong/src/providers/merge-user-messages.tsmergeConsecutiveUserMessages
per-request 认证packages/kosong/src/providers/request-auth.tsresolveAuthBackedClientrequireProviderApiKey
视频上传(Kimi)packages/kosong/src/providers/kimi-files.tsKimiFiles.uploadVideo
Kaos 接口 / 进程packages/kaos/src/{kaos,process}.tsKaosKaosProcess
本地 / 远程实现packages/kaos/src/{local,ssh}.tsLocalKaosSSHKaos
当前 Kaos 绑定packages/kaos/src/current.tsgetCurrentKaosrunWithKaossetCurrentKaos
环境探测packages/kaos/src/environment.tsdetectEnvironmentdetectEnvironmentFromNode
登录 shell PATH 补全packages/kaos/src/login-shell-path.tsprobeLoginShellPathmergeLoginShellPath
与 agent-core 的缝(LLM)packages/agent-core/src/agent/turn/kosong-llm.tsKosongLLMdowngradeUnsupportedMediabuildKosongCallbacks
与 agent-core 的缝(能力/视频)packages/agent-core/src/session/provider-manager.tsagent/tool/index.tsresolveModelCapabilitiescreateVideoUploader