跳到主要内容

再工程化: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:107)。它的构造函数亲手 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:26-62(这些 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-serverklient 只服务 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 Codevs/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 里最典型的一例——会话生命周期服务,构造函数一口气声明了 14 个依赖 (packages/agent-core-v2/src/app/sessionLifecycle/sessionLifecycleService.ts:117-132),没有一个 是手动 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(全局一份)鉴权、配置、模型/提供商目录、工作区注册表、会话生命周期、网关、协议IConfigServiceIModelCatalogServiceISessionLifecycleServiceIRestGateway
Session(每会话一份)agent 生命周期、审批、终端、todo、问题、子 agent、会话级 MCPIAgentLifecycleServiceISessionMcpServiceISessionSubagentService
Agent(每 agent 一份)回合循环、工具执行/注册/去重、上下文记忆、压缩、权限、技能、计划、用量IAgentLoopServiceIAgentToolExecutorIAgentContextMemoryServiceIAgentFullCompactionService

规模感受一下:v2 里 registerScopedService(...) 一共调用约 124 次,粗略分布是 App ≈47、 Session ≈23、Agent ≈42(依据:对 packages/agent-core-v2/srcregisterScopedService( 计数)。v1 那个近 700 行的 Agent 类,在 v2 里摊成了上百个各管一件事的小 Service。

4.2 Scope 树:一台容器裂变成一棵容器树

作用域不是标签而已,它对应运行期真实的容器父子关系Scope(_base/di/scope.ts:107) 包住一个 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:139-154,if (kind <= this.kind) throw)。App 不能直接生 Agent 得先有 Session。
  • 子容器只装本层服务,父层按需透上来。 建子容器时 buildCollection(kind) 只把该层注册的 服务塞进去(_base/di/scope.ts:77-90);父层服务在解析时沿容器链向上查到。
  • 销毁是级联的。 Scope.dispose() 先递归拆所有子 Scope,再拆自己 (_base/di/scope.ts:160-178)。关一个会话,它名下所有 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 注册的副作用(依据:agent-core-v2/src/index.ts:1-3)。

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:1-10 文件头说明)。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(依据: app/sessionLifecycle/sessionLifecycleService.ts:155-198,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 的坑), igniteEagerServices 手动 get 一长串观察者/注册器服务,让它们的构造副作用(注册内建工具、 订阅回合结束、发布活动视图投影)在第一回合之前发生(依据: session/agentLifecycle/agentLifecycleService.ts:204-236,注释解释得很直白)。

这条链走完,一棵 App(1) → Session(N) → Agent(N) 的活容器树就立起来了,回合循环 (见 01-loop.md)此后在最深的 Agent 容器里跑。


6. 服务化外壳(一):kap-server 把整棵服务树变成 HTTP

引擎有了,kap-server 负责把它搬到网络上。它的对外接口刻意和 v1 服务器一致:/api/v1 REST + /api/v1/ws WebSocket(依据:packages/kap-server/src/start.ts:1-8 文件头)。

6.1 引导:一个 Fastify + 一个 Core Scope

start.tsstartServer() 是组合根:调 bootstrap() 建 Core(App)Scope,起一台 Fastify, 路由处理器全部通过 core.accessor.get(IXxx) 解析引擎服务。默认绑 127.0.0.1:58627

主线大致是:

startServer()
├─ 实例注册表:把自己登记到 <home>/server/instances/<id>.json(多实例共存 + 端口 +1 退让)
├─ 鉴权:createCredentialValidator(持久 bearer token,可选额外 rpcToken)
├─ bootstrap({homeDir, configPath}, [...seeds]) → core: Scope (start.ts:205)
├─ Fastify:host 检查 / origin 检查 / 全局 bearer-auth 钩子 / 安全头
├─ registerApiV1Routes(app, core, {...}) (start.ts:361)
├─ registerWsV1(core, {...}) + server.on('upgrade', ...) (start.ts:377)
└─ listenWithPortRetry(...) ← EADDRINUSE 就 port+1 重试

依据:packages/kap-server/src/start.ts:142-214(引导前段)、:361-467(REST/WS 挂载与 upgrade)。

6.2 反射式 RPC:/api/v1/debug 直接寻址整棵 scoped 注册表

这是 kap-server 最有意思的一块,也是它把"整棵服务树"字面暴露出去的方式。

普通 /api/v1/* 路由是精心设计的 REST 面;而 /api/v1/debug/*没有 facade 的反射派发器: URL 里的 :service 是一个装饰器 id(通道名),:method 靠反射调用。三条路由正好镜像三级作用域:

GET|POST /api/v1/debug/:service/:method ← App/Core scope
GET|POST /api/v1/debug/session/:session_id/:service/:method ← Session scope
GET|POST /api/v1/debug/session/:sid/agent/:agent_id/:service/:method ← Agent scope

依据:packages/kap-server/src/transport/serviceDispatcherRoutes.ts:76-96(三条路由 + channels 内省)。

派发的三步——定位 scope → 查服务 → 反射调方法——在 dispatcher.ts 里:

// transport/dispatcher.ts:119-142(节选)dispatch(...) 的骨架
const service = await resolveService(core, scopeKind, params, serviceName, lookup); // 顺 scope 树取到实例
const member = (service as Record<string, unknown>)[method];
// 属性读(如 mode / rules)直接回;方法则 apply 参数后回。结果都过 assertSerializable。

resolveScope(dispatcher.ts:39-73)按 scopeKind 从 Core 走到 Session 再走到 Agent 容器, 会话/agent 不存在就抛 session.not_found / agent.not_found。查哪些服务可达,由一个 lookup 决定——debug 面用的是整棵 scoped 注册表,无白名单(registerDebugRoutes.ts:22-27, resolveAnyScopedServiceId)。这意味着每一个 Service 的每一个方法都能被反射调用。

正因为它把整个引擎不设防地摊开,门禁是三重的:

门禁规则依据
部署形态必须 --debug-endpoints 绑定是环回(loopback)才挂载start.ts:172(两个条件 &&)
全局鉴权和所有 /api/* 一样过 bearer-auth 钩子start.ts:270-286
序列化返回值必须可序列化,否则拒绝dispatcher.ts:141 assertSerializable

这套反射面正是 apps/kimi-inspect(服务浏览器)的数据源:它 GET /api/v1/debug/channels 一次拉到整份线协议,再按 Service 面板逐个调用。


7. 服务化外壳(二):klient 在客户端复刻同一棵树

服务器把树摊开成 HTTP,klient 负责在客户端把它重新拼成一个类型安全、可校验的 facade。 它是"契约驱动"的:每个可调用方法在一份契约里登记输入/输出的 zod schema。

7.1 三段式 facade:global / session / agent

klient 的调用面按作用域分三段,和引擎的 Scope 树一一对应:

// 用法示意,非源码
klient.global.config.get('some.key') // App scope
klient.session(sid).prompt.submit(input) // Session scope
klient.session(sid).agent(aid).loop.status() // Agent scope

createKlientFromChannel(packages/klient/src/core/klient.ts:45-98)把三段 facade 建出来: global 一段、session(id) 生一段并再挂 agent(id) 子段;每段还各带一个事件 hub。

7.2 每次调用都过 zod:漂移的绊线

关键在那个 call:调用前 parseInput、拿到结果后 parseOutput,都按契约用 zod 校验

// core/klient.ts:51-61(节选)
const procedure = globalContract[service]?.[method]; // 查契约
const wireArgs = validate ? parseInput(name, procedure, args) : args; // 入参校验
const data = await channel.call(scope, service, method, wireArgs); // 过传输
return validate ? parseOutput(name, procedure, data) : data; // 出参校验

契约本身就是一张张 zod 表,每个方法列出 input 元组与 output:

// contract/global/config.ts:26-40(节选)configContract
get: { input: z.tuple([z.string()]), output: z.unknown() },
set: { input: z.tuple([z.string(), z.unknown(), configTargetSchema.optional()]), output: noResult },
diagnostics: { input: z.tuple([]), output: z.array(configDiagnosticSchema) },

默认开校验;文件头写明它"是漂移的绊线"——服务端契约一改而客户端没跟上,zod 会在最早的边界报错, 而不是让脏数据默默流过(依据:packages/klient/src/core/klient.ts:20-27validate 注释)。

7.3 传输在创建时选一次:ipc / memory,同一个 Klient

facade 之下是一个极窄的传输 SPI——KlientChannel:只有 call / listen / close 三个方法, 且不知道上面是哪个 facade 方法触发的(依据:packages/klient/src/core/channel.ts:34-50)。

传输经子路径入口选一次,之后行为完全一致:

子路径传输典型场景
@moonshot-ai/klient/ipcUnix 域套接字连到 IPC 宿主跨进程(CLI ↔ 本地 server)
@moonshot-ai/klient/memory进程内直派发,不走字节同进程 / 测试

以 ipc 为例:IpcChannel.callScopeRef 翻成 core|session|agent 三态,带上 sessionId/agentId 发帧,用客户端自选 id 关联结果,每调用有 30s 期限;断链即拒绝在途调用、 不自动重连(可续连的故事归 WS 传输)(依据:packages/klient/src/transports/ipc/channel.ts:36-114)。

小结这层的对称性:引擎有一棵 Scope 树,kap-server 把它反射成三条 URL,klient 把三条 URL 复刻成 global/session/agent 三段 facade。 三处的作用域坐标是同一套。


8. 服务化外壳(三):transcript 同构渲染数据层

transcript 是一个框架无关的纯 TypeScript 包,专管"一次对话该怎么被渲染"的数据模型,浏览器 和服务器共用同一套(同构)。它是所有 transcript 线类型的唯一所有者:kap-server 把引擎事件转成 transcript,再经 REST + WS 发出;客户端复用它的 reducer,不在本地重造模型 (数据参考:仓库 CLAUDE.mdpackages/transcript 的描述)。

它的目录就是它的职责清单:

子目录管什么
model/数据模型:turn / frame / interaction / task / todo / item / meta
ops/幂等操作:applyOperation 把一个 op 叠加到 agent 状态上(L2 reducer)
granularity/订阅粒度 off/turn/block/delta(L3)
view/框架无关的视图注册表(L4)
history/历史回填、按 turn 分组
pagination/turn-cursor 分页(before_turn 往前翻页)
wire/线协议 schema 与事件类型

最能体现设计的是订阅粒度——同一份数据,不同消费者按需订阅不同细度:

// transcript/src/granularity/grade.ts:20 —— 四档
export type TranscriptGrade = 'off' | 'turn' | 'block' | 'delta';

四档从省到全:off 什么都不发;turn 只发回合头与全局状态(适合"回合完成"通知); block 加步骤头与整块状态快照;delta 是包含逐字 append 的全量流。升/降级各有收敛规则: 降级只丢在途内容,下一个 flush 快照自动重新对齐;升级则由服务端用 L1 重建一份 reset 快照 (依据:transcript/src/granularity/grade.ts:1-43,needsResetOnTransition)。

这套设计让一个前端(如 kimi-inspect)可以"REST 拿整页 + WS 只收 delta 增量"地拼出实时视图—— 但那属于消费侧,是 06-surfaces.md 的内容。


9. v1 与 v2 如何并存

一句话:两代引擎同时活着,走两条不交叉的交付路径,由不同前端各取所需。

v1 路径(稳定) v2 路径(实验)
┌─────────────────────────┐ ┌───────────────────────────────────┐
│ 前端 → node-sdk │ │ 前端 → klient → (ipc|ws|memory) │
│ (进程内直调) │ │ → kap-server → agent-core-v2 │
│ → agent-core │ │ REST /api/v1 + WS │
│ 巨型 Agent 类 │ │ DI × Scope 引擎 │
└─────────────────────────┘ └───────────────────────────────────┘

两条路径的证据:

  • v1 经 node-sdk。 node-sdk 的公共出口全部从 @moonshot-ai/agent-core(v1)re-export, 没有一处引用 agent-core-v2(依据:packages/node-sdk/src/index.ts:49-111 皆导入 @moonshot-ai/agent-core;对 node-sdk/srcagent-core-v2 无命中)。
  • v2 经 kap-server + klient。 apps/kimi-code 同时依赖 @moonshot-ai/agent-core-v2@moonshot-ai/kap-server,并有 dev:kap-server 脚本以 --debug-endpoints 起 v2 服务器 (依据:apps/kimi-code/package.json 的 deps 与 dev 脚本)。

两代对外都自称 /api/v1,接口形状一致——这正是"并存不打架"的前提:前端切后端不改协议, kimi-web 的开发侧边栏甚至能在运行时切换 v1/v2 后端(数据参考:仓库 CLAUDE.mdapps/kimi-web 的描述)。


10. 巧妙之处(可借鉴)

  • 作用域即容器父子关系,而非一个标签枚举。 很多框架的"scope"只是个字符串 key;这里 Session/Agent 是真实的子 InstantiationService,于是"关会话=拆子树"是 dispose() 的天然 结果,不用手写清理清单(_base/di/scope.ts:160-178)。

  • 子容器只装本层、父层透上来。 建 Agent 容器只塞 Agent 级服务,配置这类全局单例从 App 根 透上来解析——既隔离了每-agent 状态,又不必逐层传参(_base/di/scope.ts:77-90)。

  • 反射 RPC + 三重门禁。 把整棵注册表零白名单反射暴露,换来 kimi-inspect 这种"零手写端点" 的调试器;风险靠"loopback ∧ --debug-endpoints ∧ bearer"三把锁而不是逐个端点评审来兜 (start.ts:172)。

  • 契约 zod 当漂移绊线。 客户端每次调用都按契约校验入/出参,服务端一改契约、客户端没同步, 会在最早边界炸出来(core/klient.ts:51-61)。

  • Eager 不自动构造,靠显式点火。 违反直觉,但把"注册"与"构造副作用"解耦——只在需要副作用 时才手动 get,顺序与时机完全可控(agentLifecycleService.ts:204-236)。


11. 边界与局限

  • v2 是实验态。 交付主力仍是 v1/node-sdk;v2 走 kap-server+klient,仓库里明确标注为实验路径。

  • 反射 debug 面是 dev-only。 非环回绑定或不带 --debug-endpoints根本不挂载;它不是生产 API,别把 apps/kimi-inspect 的用法当正式集成方式(start.ts:172registerDebugRoutes.ts:1-14)。

  • ipc 传输不自动重连。 断链即拒绝在途调用并保持关闭;需要可续连要走 WS 传输 (transports/ipc/channel.ts:1-8)。

  • 反射调用受序列化约束。 返回不可序列化的值会被 assertSerializable 拒绝,不是所有内部服务 方法都适合经 debug 面直调(dispatcher.ts:135-141)。

  • 冷会话只重建主 agent。 resume 只重新物化 main agent;server 重启前创建的子 agent,其元数据与 wire 日志仍在,但 agent 容器未物化,反射寻址会报 agent.not_found(dispatcher.ts:55-71)。


12. 横向对比

同货架其它编码 agent 里,把引擎"服务化 + 作用域化"到这个程度的并不多见。可对照的两条轴:

  • DI 血统。 这套容器是 VS Code vs/platform/instantiation 的近乎逐字移植(标识符即装饰器、 accessor.getcreateChild)——熟悉 VS Code 扩展宿主的人几乎零成本迁移 (packages/agent-core/src/di/README.md:18-31)。

  • 单体 vs 分层。 v1 的巨型 Agent 类代表"一个类拎起一切"的主流写法;v2 代表"上百个 scoped Service + 反射外壳"的另一极。两者在同仓并存,恰好是一份"重构前/后"的活教材。


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

主题文件符号
DI 容器总说明packages/agent-core/src/di/README.md
服务标识符 / 装饰器注入agent-core-v2/src/_base/di/instantiation.tscreateDecoratorIInstantiationService
服务配方agent-core-v2/src/_base/di/descriptors.tsSyncDescriptor
运行期容器(解析/查环/销毁)agent-core-v2/src/_base/di/instantiationService.tsInstantiationService
实例化类型agent-core-v2/src/_base/di/extensions.tsInstantiationType
三级作用域 + Scope 树agent-core-v2/src/_base/di/scope.tsLifecycleScoperegisterScopedServiceScopecreateScopedChildHandle
组合根 / App 建根agent-core-v2/src/app/bootstrap/bootstrap.tsbootstrapcreateAppScope
会话生命周期(建 Session 子容器)agent-core-v2/src/app/sessionLifecycle/sessionLifecycleService.tsSessionLifecycleService.materializeSession
agent 生命周期(建 Agent 子容器 + 点火)agent-core-v2/src/session/agentLifecycle/agentLifecycleService.tsAgentLifecycleService.doCreateigniteEagerServices
REST/WS 网关契约agent-core-v2/src/app/gateway/gateway.tsIRestGatewayIWSGateway
执行环境接口(kaos 角色)agent-core-v2/src/os/interface/hostProcess.tsIHostProcessService
服务器引导packages/kap-server/src/start.tsstartServerlistenWithPortRetry
debug 面挂载(整棵注册表)packages/kap-server/src/transport/registerDebugRoutes.tsregisterDebugRoutes
反射派发路由packages/kap-server/src/transport/serviceDispatcherRoutes.tsregisterServiceDispatcherRoutes
反射派发核心packages/kap-server/src/transport/dispatcher.tsresolveScoperesolveServicedispatch
klient facade 工厂packages/klient/src/core/klient.tscreateKlientFromChannel
传输 SPIpackages/klient/src/core/channel.tsKlientChannelScopeRef
ipc 传输packages/klient/src/transports/ipc/channel.tsIpcChannel
契约(zod)示例packages/klient/src/contract/global/config.tsconfigContract
transcript 订阅粒度packages/transcript/src/granularity/grade.tsTranscriptGradeneedsResetOnTransition
v1 巨型 Agent 类(对照)packages/agent-core/src/agent/index.tsAgent
v1 交付出口packages/node-sdk/src/index.ts