数据截至 (上游 commit cdaa80b77807)
再工程化:DI×Scope 引擎与 REST/WS 服务器
30 秒导读: 前四章讲的是 v1 那套「一个
Agent类拎起一切」的引擎(见 02-agent.md)。 这一章讲 Kimi Code 的第二代内核:把那个巨型类拆成上百个小 Service,用一台 VS Code 式的 依赖注入(DI)容器装配起来;再给每个 Service 打上 App / Session / Agent 三级作用域标签, 让容器自动按作用域建树、按作用域拆树。拆完之后,再套一层服务化外壳——kap-server把整棵服务树反射式地暴露成 REST + WebSocket,klient在客户端把同一棵树用契约复刻回来。 v1 与 v2 目前并存:v1 经node-sdk交付(稳定),v2 经kap-server + klient交付(实验)。
本章只讲架构演进与服务化,不讲前端如何消费这套 API(那是 06-surfaces.md)。
1. 这章要解决的问题:巨型 Agent 类为什么撑不住
先看 v1 的形态,才懂 v2 为什么要重来一遍。
v1 的核心是一个类:Agent(packages/agent-core/src/agent/index.ts:115)。它的构造函数亲手
new 出十几到二十几个「管理器」——上下文、压缩、权限、技能、工具、计划、目标、后台任务、
用量记录……全塞进一个类里 。从它的 import 头就能数出来:
BackgroundManager · FullCompaction · MicroCompaction · CronManager · ConfigState
ContextMemory · GoalMode · HookEngine · InjectionManager · PermissionManager
PlanMode · AgentRecords · ReplayBuilder · SkillManager · SwarmMode · ToolManager
TurnFlow · UsageRecorder · KosongLLM · LlmRequestLogger · LlmRequestRecorder ...
依据:packages/agent-core/src/agent/index.ts:27-63(这些 manager 的 import 与实例化)。
这种写法的三个痛点,决定了 v2 的方向:
| 痛点 | 具体表现 | v2 的回应 |
|---|---|---|
| 装配写死 | 谁依赖谁,靠构造函数里手写 new 的顺序;加一个能力要改中心类 | 依赖注入:声明依赖,容器负责装配 |
| 生命周期混一锅 | "全局只有一份"的东西(配置、模型目录)和"每个会话一份""每个 agent 一份"的东西混在同一层 | 三级作用域:每样东西显式声明活在哪一层 |
| 难以远程暴露 | 一个大类,方法散落,没有统一的"每个能力=一个可寻址端点"结构 | 每个能力=一个 Service=一个可反射调用的通道 |
v1 并非没有 DI——它已经有一台完整的 VS Code 式容器(
packages/agent-core/src/di/), 服务层也已按IXxxService规范切好(packages/agent-core/src/services/)。v2 的真正跃迁不是 "引入 DI",而是给 DI 加一个作用域维度,并把整个 Agent 能力面彻底 Service 化。
2. 顶层全景:两代引擎 + 一层外壳
先给一张大盘图,建立坐标系。后面每一节都在填其中一格。
┌───────────────────────────── 交付面(06 章讲) ─────────────────────────────┐
│ TUI / CLI kimi-web 编辑器(ACP) 嵌入宿主 │
└───────┬───────────────────┬──────────────────┬────────────────┬─ ─────────┘
│ │ │ │
┌──────────────┴───────┐ ┌────────┴─────────┐ │ │
│ v1 交付:node-sdk │ │ v2 交付:klient │ │ │
│ (稳定,进程内直调) │ │ 契约 facade + zod │ │ │
└──────────┬───────────┘ └────────┬─────────┘ │ │
│ │ ipc | ws | memory │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ kap-server │ REST /api/v1 │
│ │ Fastify + /api/v1 │ WS /api/v1/ws │
│ │ /api/v1/debug 反射 │ (本章 §6) │
│ └──────────┬───────────┘ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────────────┐
│ v1 引擎 │ │ v2 引擎 agent-core-v2 │
│ agent-core │ │ DI × Scope:App/Session/Agent 三级 │
│ 巨型 Agent 类 │ │ ~上百个 scoped Service(本章 §3-5) │
└──────────────────────┘ └──────────────────────────────────────┘
怎么读这张图:下面两块是两代引擎,上面两条是它们各自的交付路径;kap-server 与 klient
只服务 v2,是本章的服务化外壳。
各部件一句话职责:
| 部件 | 干什么 | 在哪个包 |
|---|---|---|
| DI 容器 | 声明依赖、自动装配、按作用域建/拆树 | agent-core-v2/src/_base/di/ |
| scoped 注册表 | 每个 Service 声明"我活在 App / Session / Agent 哪一层" | _base/di/scope.ts |
| Scope 树 | App(1)→Session(N)→Agent(N)的运行期容器树 | _base/di/scope.ts |
kap-server | 把 v2 引擎包成 REST + WS + 反射 RPC 服务器 | packages/kap-server |
klient | 客户端契约 facade,zod 校验每次调用,复刻服务树 | packages/klient |
transcript | 同构渲染数据层,所有 transcript 线类型的唯一所有者 | packages/transcript |
3. DI 基座:v1 已经有的那台容器
这一节讲 v2 站在的地基——它原封不动继承自 v1 的 DI 内核,理解它才看得懂 §4 的作用域是加在
哪里的。设计刻意抄 VS Code 的 vs/platform/instantiation,所以心智模型可以直接搬过来
(依据:packages/agent-core/src/di/README.md:18-20)。
3.1 四个零件
DI 容器由四个概念 拼成,先用一句话各自点破:
| 零件 | 是什么 | 文件 |
|---|---|---|
ServiceIdentifier<T> | 服务的"名字牌":一个可调用的品牌值,既当 Map 的 key,又当构造参数装饰器 | _base/di/instantiation.ts |
SyncDescriptor<T> | 服务的"配方":包住构造函数 + 静态参数 + 是否延迟实例化 | _base/di/descriptors.ts |
ServiceCollection | 一个容器里的"注册册":id → 配方 或 现成实例 | _base/di/serviceCollection.ts |
InstantiationService | 运行期容器:解析、缓存、建子容器、查环、销毁 | _base/di/instantiationService.ts |
3.2 装配靠"装饰器即注入",不靠手写 new
核心手法:任何构造函数参数只要用一个服务标识符去装饰,容器就会在构造时自动把它解析出来 注入进去。静态参数在前,服务参数在后。
// 示意,非源码。重点看:@ILogger / @IClock 不用调用方传,容器自动注入。
class Foo {
constructor(
public readonly prefix: string, // 静态参数(调用方给)
@ILogger private readonly _logger: ILogger, // 服务参数(容器给)
@IClock private readonly _clock: IClock, // 服务参数(容器给)
) {}
}
// createInstance(Foo, 'hello') —— 只需给 'hello',logger/clock 自动到位
真实的用法长得几乎一样:每个 Service 类的构造函数把它依赖的所有 IXxxService 用装饰器列出来。
看 v2 里最典型的一例——会话生命周期服务,构造函数一口气声明了 24 个依赖
(packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts:140-171),没有一个
是手动 new 的。
依据:packages/agent-core/src/di/README.md:95-143(@IFoo 构造参数注入的完整说明)。
3.3 两件"底座级"能力:查环 + 生命周期
两件事让这台容器可以放心用来装几百个服务:
- 建树前先查环。 容器在真正跑任何构造函数体之前,先用
Graph(_base/di/graph.ts)把@IFoo声明出来的依赖子树走一遍,叶子优先逐个构造;若图卡住(还有节点但没有根),抛CyclicDependencyError并打印出环的路径A -> B -> A。 - 销毁按构造逆序(LIFO)。
dispose()幂等;先递归拆子容器,再把自己缓存的实例按构造的 反序销毁;只有带dispose()方法的实例才会被调用(鸭子类型)。
依据:packages/agent-core/src/di/README.md:200-245(查环两道防线 + 生命周期契约)。
3.4 延迟实例化:注册了但不一定构造
SyncDescriptor 的第三个参数打开延迟实例化:accessor.get(IFoo) 先返回一个 Proxy,
第一次真正读它的属性/调它的方法时才跑构造函数。好处是"注册了但本次会话没用到"的服务
不付构造成本。v2 用一个枚举表达这层意图:
// _base/di/extensions.ts:5 —— 就两个值
export enum InstantiationType {
Eager = 0, // 解析时直接构造,不套 Proxy
Delayed = 1, // 套 Proxy,首次访问才构造
}
一个反直觉的坑(v2 特有): 在 v2 里
Eager并不会自动实例化——它只是在get时 跳过延迟 Proxy。有些服务只为"构造副作用"存在(注册内建工具、订阅钩子),没人会去注入它们, 于是必须手动点火。见 §5.3。
4. v2 的关键跃迁:给 DI 加一个 Scope 维度
这是整章的枢纽。v1 已经有容器,v2 加的是"每个服务活在哪一层生命周期"。
4.1 三级作用域
v2 定义了三级生命周期作用域,用一个枚举钉死顺序:
// _base/di/scope.ts:12-16
export enum LifecycleScope {
App = 0, // 进程级:全局唯一一份
Session = 1, // 会话级:每个会话一份
Agent = 2, // agent 级:每个 agent 一份
}
每样能力注册时就声明自己属于哪一级。注册函数把作用域当第一个参数:
// _base/di/scope.ts:27 registerScopedService(scope, id, ctor, type, domain)
registerScopedService(
LifecycleScope.App, // 我是全局单例
ISessionLifecycleService,
SessionLifecycleService,
InstantiationType.Eager,
'sessionLifecycle',
);
三级各装什么,举几个代表(全量见 §5 目录):
| 作用域 | 装的是什么 | 代表 Service |
|---|---|---|
| App(全局一份) | 鉴权、配置、模型/提供商目录、工作区注册表、会话生命周期、网关、协议 | IConfigService、IModelCatalogService、ISessionLifecycleService、IRestGateway |
| Session(每会话一份) | agent 生命周期、审批、终端、todo、问题、子 agent、会话级 MCP | IAgentLifecycleService、ISessionMcpService、ISessionSubagentService |
| Agent(每 agent 一份) | 回合循环、工具执行/注册/去重、上下文记忆、压缩、权限、技能、计划、用量 | IAgentLoopService、IAgentToolExecutor、IAgentContextMemoryService、IAgentFullCompactionService |
规模感受一下:v2 里 registerScopedService(...) 一共调用约 124 次,粗略分布是 App ≈47、
Session ≈23、Agent ≈42(依据:对 packages/agent-core-v2/src 的 registerScopedService(
计数)。v1 那个近 700 行的 Agent 类,在 v2 里摊成了上百个各管一件事的小 Service。
4.2 Scope 树:一台容器裂变成一棵容器树
作用域不是标签而已,它对应运行期真实的容器父子关系。Scope(_base/di/scope.ts:177)
包住一个 InstantiationService,createChild 生一个子 Scope:
createAppScope() ← 进程启动,建 App 根容器
│ App 容器(装全部 App-scope Service)
│
createChild(Session, sid) ← 开一个会话
│ Session 子容器(装全部 Session-scope Service)
│ ↑ @IConfigService 等 App 服务从父容器透上来
│
createChild(Agent, aid) ← 会话里起一个 agent
Agent 子容器(装全部 Agent-scope Service)
↑ Session/App 服务都能透上来解析
三条硬规则,保证这棵树只能顺着生命周期长:
- 只能往深长。 子作用域的
kind必须严格大于父的,否则抛错 (_base/di/scope.ts:231-240,按声明的拓扑childIndex <= parentIndex即 throw)。App 不能直接生 Agent 得先有 Session。 - 子容器只装本层服务,父层按需透上来。 建子容器时
buildCollection(kind)只把该层注册的 服务塞进去(_base/di/scope.ts:122);父层服务在解析时沿容器链向上查到。 - 销毁是级联的。
Scope.dispose()先递归拆所有子 Scope,再拆自己 (_base/di/scope.ts:268-284)。关一个会话,它名下所有 agent 容器连带释放。
4.3 为什么这样就取代了巨型 Agent 类
一句话:"谁活多久"从代码结构里自然掉出来,不再靠一个中心类记账。
- 想加一个 agent 级能力?写一个
IAgentXxxService,底部registerScopedService(Agent, ...), 它就自动在每个 agent 容器里各有一份,构造函数里声明依赖即可——不碰任何中心类。 - 一个 agent 结束,它的容器
dispose(),这个 agent 独占的所有服务一起释放;别的 agent 与 会话毫发无伤。v1 里这种"只清理某个 agent 的那部分状态"要在大类里手写。 - 配置这种"全局一份"的东西注册在 App,任何深处的 Agent 服务都能透上来读同一份,不必逐层传参。
5. v2 的目录:按 _base + 四级作用域切分
这一节是 v2 源码的地图。目录布局本身就把 §4 的作用域画了出来:agent-core-v2/src/ 下,
_base 是 DI 内核,其余基本按作用域分文件夹。
agent-core-v2/src/
├── _base/ ← L0 DI 内核:di/(scope 在此)、event、lifecycle、log、execEnv...
├── agent/ ← Agent 作用域:每个能力一个 Service
│ loop · toolExecutor · toolRegistry · toolDedupe · toolSelect
│ contextMemory · contextInjector · fullCompaction · contextSize
│ permissionGate · permissionMode · permissionPolicy · permissionRules
│ skill · swarm · plan · goal · usage · activityView · llmRequester ...
├── session/ ← Session 作用域
│ agentLifecycle · subagent · mcp · terminal · todo · question
│ approval · interaction · sessionMetadata · sessionInit · cron ...
├── app/ ← App(全局)作用域
│ auth · config · model · provider · modelCatalog · workspaceRegistry
│ sessionLifecycle · gateway · protocol · bootstrap · flag · telemetry ...
├── os/ ← 执行环境抽象(对应 kaos 角色):interface/ + backends/
├── persistence/ ← 存储抽象:interface/ + backends/(memory|minidb|node-fs)
├── tool/ ← 工具契约与校验(args/input-schema/path-access/rule-match)
└── wire/ ← 线协议:record · op · model · migration(与 transcript 对接)
每个能力目录里几乎都是同一对文件:xxx.ts(接口 + 标识符 + 错误)和 xxxService.ts(实现类 +
底部 registerScopedService)。这个"契约/实现分离 + 自注册"约定是 v1 服务层就立下的规范
(数据参考:packages/agent-core/src/services/AGENTS.md:40-58,命名与文件约定)。
5.1 _base:DI 内核与其它 L0 原语
_base/di/ 就是 §3 那台容器 + §4 的 scope.ts。此外 _base 还放事件(event.ts)、
生命周期(lifecycle/)、日志(log/)等所有域都要用的底座。index.ts 顶部按层 re-export
所有域,导入包 = 触发全部 scoped 注册的副作用(依据:包入口就是一排 export *,任何导入都会拉起全部域模块的 scoped 注册,agent-core-v2/src/index.ts:1-6)。
5.2 os/:执行环境接口 = kaos 角色在 v2 的落点
os/interface/ 定义了引擎与真实操作系统之间的所有原语接口——进程、文件系统、文件监视、终端、
环境;os/backends/node-local/ 提供 Node 实现。这一层承担的正是 v1 里 kaos 抹平执行环境的角色
(见 04-providers.md)。接口刻意贴近熟悉的形状:
// os/interface/hostProcess.ts:41-49 —— 有意贴近 Python subprocess.Popen
export interface IHostProcessService {
readonly _serviceBrand: undefined;
spawn(command: string, args?: readonly string[], options?: HostProcessOptions): Promise<IHostProcess>;
}
IHostProcessService 绑在 App 作用域,任何要起子进程的域都透上来用它(依据:
os/interface/hostProcess.ts:9 起的接口定义)。persistence/ 是同样的套路:接口一层、多后端一层。
5.3 一个真实装配路 径:App → Session → Agent 是怎么长出来的
把 §3-5 串起来,追一次"开会话、起 agent"的容器裂变,这也是理解整套引擎运转的主线。
第一步——建 App 根。 进程启动调 bootstrap(),createAppScope 建 App 容器,并把一批
种子实例塞进去(把 process.env / homeDir 观测成一个冻结快照、文件存储根、技能发现):
// app/bootstrap/bootstrap.ts:118-124
export function bootstrap(input = {}, extraSeeds = []) {
const options = resolveBootstrapOptions(input);
const app = createAppScope({
extra: [...bootstrapSeed(input), ...storageSeed(options), ...skillSeed(), ...extraSeeds],
});
return { app };
}
第二步——建 Session 子容器。 ISessionLifecycleService(App 级)负责这件事:解析工作区、
拼出会话的存储地址,createScopedChildHandle(instantiation, Session, sid, { extra: sessionContextSeed(ctx) })
生一个 Session 容器,唯一的每会话种子是"会话身份"ISessionContext(依据:
workspace/sessionLifecycle/sessionLifecycleService.ts:220-287,materializeSession)。随后它强制点火
那些"只为副作用存在"的会话级服务(外部钩子、cron),否则它们的订阅永远不发生。
第三步——建 Agent 子容器。 IAgentLifecycleService(Session 级)负责:算出 agent 的家目录与
存储地址,createScopedChildHandle(instantiation, Agent, agentId, { extra: [[IAgentScopeContext, ...]] })
生 Agent 容器,唯一的每 agent 种子是身份(agentId + 存储 scope):
// session/agentLifecycle/agentLifecycleService.ts:163-172(节选)
const handle = createScopedChildHandle(
this.instantiation, LifecycleScope.Agent, agentId,
// 唯一的每-agent 种子:身份。其它 agent 级服务要么从 IAgentScopeContext 推导配置,
// 要么顺着 scope 树解析(如会话共享的 MCP 管理器)。
{ extra: [[IAgentScopeContext, makeAgentScopeContext({ agentId, agentScope })]] },
);
第四步——点火 agent 级副作用服务。 因为 v2 的 Eager 不自动构造(§3.4 的坑),
create 流程尾部逐个显式 get/激活副作用服务——封 IWireService、登记会话元数据、
恢复 IEventDispatcher、bindBootstrap、激活 IAgentToolActivationService——让构造/激活副作用在
第一回合之前发生(依据:session/agentLifecycle/agentLifecycleService.ts:183-199,
原先的 igniteEagerServices 一揽子调用已拆进这段流程)。
这条链走完,一棵 App(1) → Session(N) → Agent(N) 的活容器树就立起来了,回合循环
(见 01-loop.md)此后在最深的 Agent 容器里跑。