Skip to content

Aalis 架构总览 ​

本文档描述 Aalis 框架的整体架构设计、核心流程和扩展机制。

设计哲学 ​

Aalis 核心遵循忒修斯之船原则:Core 只提供最小化基础设施(事件、服务容器、插件生命周期),所有功能——LLM 调用、消息存储、对话编排、平台接入——由可插拔插件提供,中间件钩子与贡献点也是插件提供的服务。可替换的是服务提供者;内置能力(events / logger / config / lifecycle / provide / services)不可经 provide 替换。

插件的交界面是 definePlugin({ name, uses, provides, apply(caps) }):能力经描述符显式声明,按激活绑定。没有默认注入。业务服务接口由对应的 @aalis/api-* 包以描述符导出,core 不持有任何业务接口。详见 api 包架构。

@aalis/core 对外暴露:

  • 运行时基础设施:App / definePlugin / defineService / Logger,以及内置能力描述符(events / lifecycle / logger / config / provide / services)
  • 扩展点 AalisEvents(由 @aalis/api-* 经 declaration merging 注入业务事件)。钩子与贡献点的扩展点 HookContextMap / ContributionPointMap 在各自的契约包 @aalis/api-hooks / @aalis/api-contributions。服务类型随描述符走,没有服务名类型表
  • 宿主入口:app.plugin / app.pluginAll / app.bind / app.plugins
  • 运行态:各实例的配置、禁用态与服务偏好。登记时交来的配置原样生效,core 不合并默认值、不读配置文档;管理动作(plugins.enable / disable / updateConfig / bounce)只改运行态,不写文档

配置文档不在 core。契约在 @aalis/api-host-config:HostConfig(host-config 服务,文档读写加 save())由宿主提供;AalisConfig 只声明宿主层字段(name / logLevel / plugins / disabledPlugins / servicePreferences)加 [key: string]: unknown 兜底,业务字段(owners / deniedCapabilities / authorityOverrides / confirmOverrides 等)由对应 api-* 经 declaration merging 注入。要跨重启保留管理动作的结果,调用方在动作成功后经 host-config 写文档并 save()。

业务数据契约与领域类型一律不在 core:Message / ContentSegment / ToolCall(OpenAI 协议形状,跨载体复用)与 getSenderLabel / prefixSender / getMessageName 在 @aalis/schema-message,ToolDefinition / ToolFunction 在 @aalis/api-tools,UserIdentity 在 @aalis/api-authority,ModelRef / resolveLLMModel 在 @aalis/api-llm。

宿主层 vs 核心层(Bootstrap 边界) ​

@aalis/core 在物理上是环境无关的内存运行时:运行时代码不加载外部包,package.json 没有 dependencies,源码不 import 任何 node:fs / node:path / node:os / node:child_process,不调用 process.cwd() / process.argv / console.*,也不读 process.env(devMode 由宿主显式注入)。这意味着同一份 core 理论上可跑在浏览器、Worker、Deno 等任何 JS 运行时。多运行时冒烟(test/smoke/core-multi-runtime.test.ts)把同一段场景分别放进 Node(按包名解析到构建产物)与无头 Chromium 的主线程、module Worker 里实跑,核对注册激活、重载、清理、慢激活与停机的结果。

业务插件同样受约束:直接 import node:fs / node:child_process / node:os / node:http(s) 被 biome 拦截,必须改走 @aalis/api-storage / @aalis/api-process。完整白名单与豁免理由见 node-usage-policy。

环境耦合全部收敛在宿主层 @aalis/runtime(monorepo 由仓库根 src/index.ts 一行 startAalis 拉起)。重启策略与 dev 开关经 new App({ ... }) 注入;插件发现与配置文档由宿主持有,插件定义连同各实例的配置、禁用标记经 app.pluginAll 交给 core,宿主路径、日志、配置文档与热扫描以服务的形式登记在根激活上:

宿主件接到 core 的方式默认实现(在 @aalis/runtime)职责
宿主路径host-paths 服务(@aalis/api-host-paths)startAalis 解析数据目录与安装集(parseHostArgs / resolveDataDir / derivePaths)数据目录、安装集与数据目录布局的绝对路径
读日志host-logs 服务(@aalis/api-host-logs)createHostLogs():历史读 logs/latest.log,实时接日志中枢读日志流:最近的历史与此后的实时日志(core 只给写日志的 logger)
配置文档host-config 服务(@aalis/api-host-config)createFsYamlConfigProvider() + createFsSecretsProvider() + createConfigStore() / installHostConfig()配置与密钥文件读 / 写 / fs.watch 热重载
插件发现app.pluginAll;热扫描为 plugin-source 服务(@aalis/api-plugin-source)createFsPluginLoader() / createNodeModulesPluginLoader() + createPluginDiscovery()扫描 packages/ 或项目依赖 + dynamic import,整批登记
重启策略AppOptions.restartStrategy(RestartStrategy)createProcessRespawnStrategy()child_process.spawn 重启进程
dev 开关AppOptions.devMode(boolean)process.env.NODE_ENV !== 'production'dev 校验开关

startAalis 的装配顺序见 @aalis/runtime。

另外 @aalis/runtime 的 startAalis 还负责 stdout/stderr console-sink、文件日志、终端状态恢复、子命令分发、SIGINT 优雅退出 —— 这些都是纯宿主关切,core 完全不知情。

系统分层 ​

┌──────────────────────────────────────────────────────────────┐
│                    平台层 (Platform Layer)                    │
│   CLI  ·  WebUI (Express+WS+React)  ·  OneBot v11/v12       │
├──────────────────────────────────────────────────────────────┤
│                  网关入站相位 (Gateway Phases)               │
│   confirm → command → trigger → flow → dispatch              │
│   trigger 要不要开口:@/名字/计数评分/闲置主动开口           │
│   flow 节流硬闸:禁言/冷却/限速                              │
├──────────────────────────────────────────────────────────────┤
│                    任务编排层 (Task Layer)                    │
│   SessionManager: 会话树 · 子任务并行 · 平台配置继承     │
│   Scheduler: Cron 定时任务 · 主动执行                      │
│   TodoList: 任务跟踪 · 子任务协调                          │
├──────────────────────────────────────────────────────────────┤
│                    对话编排层 (Agent Layer)                    │
│   DefaultAgent: 消息构建 → LLM 调用 → 工具循环 → 上下文裁剪   │
├──────────────────────────────────────────────────────────────┤
│                    服务层 (Service Layer)                      │
│   LLM · Memory · Embedding · VectorStore · Persona · Tools   │
│   Skills · ImageRecognition · WebSearch · Office              │
│   Hooks · Contributions(钩子与贡献点)                       │
│        接口由 api-* 提供,实现可多提供者并存                  │
├──────────────────────────────────────────────────────────────┤
│                    核心框架层 (Core Layer)                     │
│   App · definePlugin / defineService · PluginManager          │
│   EventBus · ServiceContainer                                  │
│   Logger · 内置能力描述符                                      │
│   DisposableChain(资源内核,不导出)                         │
│   扩展点:AalisEvents                                          │
│   (业务接口均在 api-*,类型随描述符走)                       │
└──────────────────────────────────────────────────────────────┘

消息处理完整流程 ​

用户输入 (CLI / WebUI / OneBot)
  │
  ▼
Platform 适配器接收 → 发出 inbound:message 事件
  │
  ▼
plugin-gateway 入站相位链(任一相位吞掉即终止,消息不触达 agent)
  │     inbound:command   指令解析与执行,以及指令意图确认的回复
  │     inbound:trigger   要不要开口:生效的触发插件判定(默认规则:禁言关键词、点名、计数与评分)
  │     inbound:flow      节流硬闸:禁言、回复后冷却、限速
  │     inbound:dispatch  默认动作 → Agent.handleMessage(incoming)
  │
  ├─ 1. hooks.use.run('agent:input:before', { message }, defaultAction)
  │     │
  │     ├─ [预处理中间件] 图像识别、文件提取等
  │     └─ 全部通过 → defaultAction() 交给通道:会话空闲时开一轮,在跑的一轮由真人消息开且还收输入时
  │        待插入(下一次调用模型之前插进去),否则排队;带来源的注入各自成一轮。新消息不打断在跑的一轮
  │
  ├─ 1a. hooks.use.run('agent:turn:before', { turnId, sessionId, inputs }, defaultAction)
  │     │
  │     ├─ [回合中间件] 会话状态、检查点、人设身份等
  │     └─ 全部通过 → defaultAction() 先归档这一轮的输入(带 turnId),再跑这一轮
  │
  ├─ 2. buildMessages()
  │     └─ [系统提示词] + [历史消息(≤50)] + [这一轮的输入(可以有几条)]
  │
  ├─ 3. 组装 agent:prompt 贡献 → 物化为带归属标识的 system 块
  │     ├─ plugin-memory-vector / memory-summary: 语义记忆、摘要(context 槽)
  │     ├─ plugin-user-profile / user-relation: 档案、关系(identity 槽)
  │     └─ plugin-skills: 技能库路标与已激活正文(knowledge 槽)
  │
  ├─ 4. hooks.use.run('agent:llm:before') ← 拦截者审已成型的 messages
  │     └─ plugin-tool-search: 替换工具列表为搜索层
  │
  ├─ 4a. 插入点:取走待插入的输入,归档、追加到末尾,再增量组装、重跑 agent:llm:before,取空为止
  │
  ├─ 5. trimMessages() ← 按 token 预算裁剪
  │
  ├─ 6. LLM.chatStream() → 流式输出 → outbound:stream 事件
  │
  ├─ 7. hooks.use.run('agent:llm:after')
  │
  ├─ 8. 工具调用循环 (最多 maxToolIterations 次,没设不限)
  │     ├─ hooks.use.run('agent:tool:before')
  │     ├─ tools.use.execute() ← 权限检查 + 执行
  │     ├─ hooks.use.run('agent:tool:after')
  │     └─ 追加工具结果 → 继续调用 LLM
  │
  ├─ 9. hooks.use.run('agent:reply:before')
  │     └─ plugin-persona: outputFormat JSON 解析
  │
  ├─ 10. 保存到 memory (用户+助手消息)
  │
  └─ 11. events.use.emit('outbound:message') → 各平台输出给用户

核心扩展机制 ​

Aalis 提供四种互补的扩展手段,覆盖不同粒度的定制需求:

1. 中间件管道 (Hooks) ​

插件通过 hooks.use.middleware(hook, fn) 注册中间件,拦截核心流程的各阶段。中间件可修改数据或中断流程。同一钩子内多个 handler 按注册顺序执行洋葱模型(无优先级数字);跨钩子顺序由调度方(如 plugin-gateway)显式决定。hooks 描述符来自 @aalis/api-hooks,默认提供者是 @aalis/plugin-hooks。

typescript
// 拦截消息(不调用 next = 丢掉这条输入,不进一轮)
hooks.use.middleware('agent:input:before', async (data, next) => {
  if (shouldBlock(data.message)) return; // 丢掉
  data.message.content += ' [已审核]';   // 修改
  await next();                           // 继续
});

详见 api-hooks

2. 服务替换 (Service IoC) ​

任何服务都可以被替换。提供同名服务的插件自动参与优先级竞争:

typescript
provide.use(agent, myAgent, { priority: 20 });

详见 service.md — 服务

3. 事件监听 (EventBus) ​

松耦合的发布/订阅模式,用于响应系统事件而不干预流程:

typescript
events.use.on('outbound:message', async (msg) => { /* 记录日志、统计等 */ });

4. 贡献点 (Contribution Points) ​

向共享产物提交一块内容,排布权归收集方。与 hooks 的分工:改写或截停既有流程 → hooks;向共享产物添加自己的一块 → 贡献点。贡献者拿只读视图、不掌握控制流(无短路、无排序影响力、看不到他人产出),因此重复注入、排布漂移、错误连坐在 API 上无法表达。contributions 描述符来自 @aalis/api-contributions,默认提供者是 @aalis/plugin-contributions。

typescript
contributions.use.contribute('agent:prompt', {
  id: 'my-block',
  anchor: 'context',
  build: async view => (view.dryRun ? null : `补充上下文:${await load(view.sessionId)}`),
});

for (const { key, spec } of contributions.use.collect('my-plugin:panel')) { /* ... */ }

5. Declaration Merging ​

第三方插件可通过 TypeScript 声明合并扩展事件、钩子与贡献点的类型表。事件表 AalisEvents 在 core,钩子表与贡献点表在各自的契约包,增广的模块要分开写。服务类型随描述符走,不往扩展点表里登记服务名。

typescript
declare module '@aalis/core' {
  interface AalisEvents {
    'scheduler:tick': [jobId: string];
  }
}
declare module '@aalis/api-hooks' {
  interface HookContextMap {
    'schedule:before': { jobId: string; cron: string };
  }
}
declare module '@aalis/api-contributions' {
  interface ContributionPointMap {
    'my-plugin:panel': { id: string; render(): string };
  }
}

服务 IoC 与多实现解析 ​

服务注册 ​

typescript
provide.use(llm, deepseekService, { priority: 10 });

provide 不接受 capabilities 选项——内核 DI 只关心「按名解析服务实例」。 领域级筛选(如按 LLM 模型的 tool-calling / vision 能力挑模型、按 storage root 选权限) 由各 -api 包在服务实例自己的元数据上处理(如 LLM 把能力挂在 model handle 上), 不再经过内核的服务能力匹配层。

服务消费 ​

typescript
const reply = await llm.use.chat({ messages }); // 业务句柄:每次调用打到本插件选中的那一家

use 只在已经写进 uses 的描述符所装配的选择层上,转发给本插件选中的那一家(不再有 capabilities 参数),选择、缺席与固定同一家的规则见 选择层与业务层。动态按名查询走 services.use.get,不产生依赖边。

多实现解析顺序 ​

同一服务可有多个提供者。挑选规则为 本插件偏好 > 全局偏好 > 优先级 > 注册顺序,细则见 选择层与业务层。

llm 服务:
  [0] plugin-llm-deepseek (priority=10)   ← 没有偏好时 use 打到这一家
  [1] plugin-llm-openai   (priority=0)

服务偏好 ​

用户可通过配置(全局的 servicePreferences,只对一个插件的 pluginServicePreferences)或 WebUI(设置「系统」的服务偏好、插件详情的服务偏好)切换首选提供者 (管理面代码为 services.use.prefer(key, contextId),给 { for: 实例 id } 是只对那个插件生效的本插件偏好)。什么时候不该设本插件偏好见 运行态与配置文档。 偏好指向的那家在场时就是选中的那一家,即使其 priority 低于其他 entry;切换偏好会发出 service:changed, 业务句柄随之打到新选中的那一家,follow 订阅者随之重挂。普通插件不写偏好。

插件生命周期 ​

register
  │
  ▼
pending ──(所有 required 依赖满足)──→ activating ──→ active
  ▲                                                    │
  │                                                    │
  └───(依赖服务被移除)────────────────────────────────┘

disabled ←─(手动禁用)─ active
  │
  └─(手动启用)─→ pending → ...

统一状态机:recompute() ​

PluginManager 只有一个外部可见的状态变更入口:recompute()。服务变更、enable / disable / updateConfig / bounce、转入后台的激活落定都汇入同一状态机。关机不经它:App.stop() 先冻好一张停机计划,stopAll() 执行它。

optional 上下线与选中的那一家换人不改变目标态,不级联 bounce。有状态接线走选择层的 follow。

单轮两阶段(拓扑保证) ​

每轮 recompute 先按 provider→consumer 拓扑排序(Kahn,同时就绪者按登记序),然后:

  1. Phase A 成批关闭:本轮目标不再是 active 的(含 required 依赖已不在、仍在后台初始化的),连同 required 服务会随这批一起失去全部提供者的下游,它们之间的次序由关停编排按实际绑定决定。
  2. Phase B 正向遍历 activate:提供者先于消费者 active。单个激活至多等 AppOptions.slowThresholdMs(默认 60000),超过即 warn 点名、转入后台,接着激活后面的插件;后台期间它登记的服务不对外,依赖方保持 pending,它落定后另触发一次重算。

如本轮有变动则进入下一轮,直到稳定或达到轮次上限(maxRounds = 2×插件数 + 8)。

整体停机(app.stop())单飞:先冻闸并进入停机态(apply 尚未完成的激活此刻收到 lifecycle.use.signal 的 abort),再 idle(),再发 app:stopping,等监听器完成(单个至多等 slowThresholdMs)后执行停机计划;仍在初始化的激活在收尾段至多再等 disposeTimeoutMs,到期不再等待、继续。每次调用都返回完整停机的同一 Promise;监听器与清理回调不能 await 或返回它,以免等待自身。全部 active 插件与根激活进同一张计划:每个激活 drain 后 close。边规则与依赖成环时的处理见 插件定义与能力。单插件 unload / disable / bounce 同样先关正在用它的 required 下游(传递闭包),下游收尾时提供者仍在。动态 services.use.get 不产生依赖边,关停期间可能取到空。

隔离粒度 ​

  • 完全隔离 — 需要独立事件总线、独立日志通道时,应直接 createApp(options) 创建新的 App 实例(每个 App 自有事件总线与服务容器)。Logger 可注入独立 LogHub 隔离日志缓冲。
  • 按会话/租户差异化配置不需要激活隔离——用键控解析(参考会话设置的读取器 fields.get(sessionId))。
  • 选择层的 follow(attach):选得到即以选中那一家的业务句柄调 attach,换人时先跑上次返回的清理再以新选中那一家的句柄调,选不到、下线与关闭时清理。attach 必须同步返回函数 cleanup(或不需要清理时不返回);thenable 会被接住并 warn。这是消费枢纽型服务、建立有状态资源的入口(参见 docs/core/context.md)。

激活关闭时的推荐 API ​

场景API
监听事件events.use.on(event, fn) — 关闭时自动注销
注册中间件hooks.use.middleware(hook, fn) — 关闭时自动注销
发布服务provide.use(descriptor, impl) — 关闭时自动注销
清理外部资源(连接、定时器、子进程)lifecycle.use.onDrain / lifecycle.use.onDispose
跟随提供者x.follow(attach)

中间件钩子管道 ​

钩子(Hook)是命名的中间件管道,插件可拦截核心流程的各阶段。

执行模型 ​

hooks.use.run(hookName, data, defaultAction?) → reachedEnd: boolean
  │
  ▼
handler A(先注册)─── await fn(data, next)
  │ next()              │ 不调用 next() → 链终止
  ▼                      ▼
handler B(后注册)   管道返回 false,defaultAction 不执行
  │ next()
  ▼
defaultAction()        ← 所有 handler 通过后执行

关键约定:

  • 同一钩子内多个 handler 按 注册顺序 执行洋葱模型,无优先级数字
  • 不调用 next() 即中止整个管道(含 defaultAction);hooks.use.run() 返回 false
  • 没有 hooks 提供者时 hooks.use.run() 返回被拒的 Promise,defaultAction 不执行
  • 跨钩子的顺序由调度方(如 plugin-gateway)显式决定

Gateway 入站生命周期相位 ​

入站消息按以下命名相位顺序串行执行;任一相位被 swallow 即停止后续调度:

相位数据载荷占据者默认动作
inbound:commandInboundPhaseDataplugin-commands(无)
inbound:triggerInboundPhaseData生效的触发插件(如 plugin-trigger-policy)(无)
inbound:flowInboundPhaseDataplugin-flow-control(无)
inbound:dispatchInboundPhaseData—agent.handleMessage(message)

房间类别为 owner-only 的会话(owner 自己的会话)跳过 inbound:trigger 与 inbound:flow,指令相位之后直接派发,见 plugin-gateway 的结构性放行。

InboundPhaseData = { message, metadata, agent },对象在各相位间共享传递。 第三方插件可注册到任一相位获得清晰的语义位置——无需理解优先级数字、无需与其他插件协商占位。

其他钩子 ​

钩子名数据用途
outbound:dispatch{ message, metadata }出站主管道;默认动作是 emit('outbound:message'),handler 可脱敏 / 限速 / 审计。
agent:input:before{ message }每条输入一次,修改/丢掉收到的消息(图像识别、文件提取)
agent:turn:before{ turnId, sessionId, inputs }一轮开始,next 之后是这一轮的全部(会话状态、检查点、人设身份);turnId 是 agent 在回合开始时生成的回合 id
agent:turn:after{ inputs, reply, outcome, error?, sessionId, turnId }agent 回复周期完成后(摘要触发、子任务完成检测、会话记下这一轮的结局)
agent:llm:before{ messages, tools, sessionId }修改发给 LLM 的消息列表和工具(记忆注入、技能注入、工具搜索替换)
agent:llm:after{ response, messages }处理 LLM 返回的响应
agent:tool:before{ name, args, toolCallContext }修改工具调用参数
agent:tool:after{ name, result, toolCallContext }处理工具返回结果
agent:reply:before{ content, sessionId }修改最终回复内容(persona JSON 解析)

入站请使用 inbound:* 相位,出站请使用 outbound:dispatch。

遥测事件 ​

gateway:phase:done 在每个 inbound 相位结束后发出,携带 { phase, reachedEnd, durationMs, sessionId, platform },可用于度量耗时与 swallow 率,对主流程零侵入。

扩展自定义钩子 ​

插件可以定义并触发自己的钩子,第三方可注入 handler:

typescript
// 定义钩子的插件
await hooks.use.run('my-plugin:before', { task: taskData }, async () => {
  // defaultAction
});

// 注入 handler 的第三方插件
hooks.use.middleware('my-plugin:before', async (data, next) => {
  data.task.modified = true;
  await next();
});

权限与安全 ​

权限是两条正交的轴:轴 A「等级」决定谁可以执行,轴 B「确认」做 HITL 意图核对 (owner 同样受确认轴约束,以抵御提示词注入借权)。两轴由 plugin-authority 统一裁决。

轴 A · 等级与权限组裁决(开放整数等级) ​

每个外部身份 → 一个整数等级(缺省 0,封禁 = 负数),另可属于任意几个权限组
owner        → ∞(靠 owners 列表归属,不在等级轴上,永不能被设成有限值)
每个操作     → 一个最低等级 minLevel
权限组       → 一组放行条目(authorityGroups:tool:… / command:… 可带通配,tool-group:<分组名>)

minLevel 解析(首个命中赢):
  1. authorityOverrides[capability]  ← owner 逐条覆盖成任意整数
  2. risk 派生                       ← safe→0 · sensitive→1 · dangerous→2
  3. visibility 兜底(无 risk 时)   ← public→0 · restricted→2

裁决 resolveAccess(首个命中赢):
  1. deniedCapabilities glob 全局硬禁 → 拒(压过一切,含 owner;配置总闸,非 per-user)
  2. isOwner                          → 放行
  3. 封禁(level < 0)                → 拒(负数门槛、权限组都不放行)
  4. level >= minLevel                → 放行
  5. 所属权限组的条目覆盖这个操作     → 放行;否则拒

没有命名档位(受信/管理员等表面名仅是等级整数的别称)、没有能力委托图、 没有按人禁用的清单;按人放行用权限组,它只加放行。owner 只管理权限(不能自授): WebUI authority 页(仅 owner)+ 指令 /level(设某用户等级)与 /perm(设某用户所属的权限组)。 模型详见 docs/plugins/plugin-authority.md。

轴 B · 确认(HITL 意图核对) ​

确认轴与等级轴正交,在 plugin-authority 的执行守卫里完成。risk 声明同时设两轴默认值 (如 dangerous = visibility:'restricted' + confirm:'session')。

授权已过、命中 confirm 的操作
  │
  ▼
可免确认? (只对参与者都是 owner 的回合;always 只有确认覆盖设为 off 能免)
  ├─ confirmOverrides 设为 off → 免
  ├─ skipConfirm(受信来源:scheduler 触发的指令、runtime 一次性子命令)→ 免
  ├─ restrictedPolicy 白名单命中 → 免
  └─ 否则(含有别人的回合)
       ├─ 指令:在同一会话里问发出者本人,只认他的下一条消息(y 确认,其余取消,超时取消)
       └─ 工具:owner 审批(approvals)
            ├─ 这一轮里有 owner 的话(她亲手发起,或它的续做、她那一轮派的子任务,不论在哪个会话)→ 推给在场的审批面,owner 批准才执行
            └─ 其余(起它的那一轮不是人亲手的,如定时任务、编排与它们派的子任务;别人亲手发起的)→ 立即拒绝,不推给任何面

confirm:'always' 是最高危档:每次都必须有人确认(cron 等无人可问的场景直接拒),只有确认覆盖设为 off 时对参与者都是 owner 的回合免。 审批的在场、排队、时限与审计见 api-approval。

上下文窗口管理算法 ​

trimMessages() 把要发出的列表分成头部(第一条非系统消息之前:人设、技能、摘要等;属于历史的系统消息不算)、历史与本轮三段,按段裁剪适配 LLM 上下文窗口:

裁剪预算 = contextLength × trimThresholdRatio - maxTokens - 512(安全余量),下限 1024
单位 = ⌈裁剪预算 / 4⌉;历史可用量 = 裁剪预算 - 头部 - 单位(本轮固定留一个单位)

裁剪步骤(第 1 步每次都做,之后仍超预算才往下走,任一步之后回到预算内即停):
  1. 历史成段丢弃: 超出可用量时(不看本轮多大)丢到累计第一次达到 k 个单位的那一条,切在工具调用组中间时整组丢
  2. 裁本轮追加的: 截短过长工具输出、精简思考内容、较早的工具调用组压成摘要(历史里的与最后一组不动)
  3. 本轮超过一个单位仍超时: 历史接着按单位丢,丢到下一个单位整数倍或丢完
  4. 注入的系统消息合计超过 memoryTokenBudget 时按比例缩减,仍超时删除
  5. 最后手段: 截短最后一组里过长的工具结果与它的思考内容

始终不动: 首条系统消息、本轮的输入
不删不压、只在第 5 步截短: 最后一组工具调用、末条消息(格式重试的反馈)

切点只由头部与历史定,两次丢弃之间发给模型的开头逐字不变,服务商的前缀缓存照常命中;第 3、4 步会改开头。
走过第 4 步且删掉 ≥6 条消息时,在本轮最后一条输入之后注入一条继续执行的系统提示。

细节见 plugin-agent 的上下文裁剪算法。

事件列表 ​

core 自持的八个基础设施事件(app:* 四个屏障、service:changed / plugin:* / plugins:changed 四个通知)及其时序见 core/events.md;业务事件由各 -api 包注入,按包查见扩展点索引 §2。 总线上没有 dispose 事件,清理副作用用 lifecycle.use.onDrain / lifecycle.use.onDispose;memory:clear 是钩子不是事件,见 api-hooks。

向量语义记忆 ​

索引流程 ​

仅对已落库的入站用户消息建索引(inbound:message:archived),助手/出站消息不入向量库。

inbound:message:archived → embedding.embed(prefixSender(text)) → vectorstore.add(vector, metadata)

检索与注入 ​

检索通过 agent:prompt 贡献(context 槽)完成,非独立 hook、无优先级数字;token 干跑(dryRun)跳过真实检索。

agent:prompt 贡献 (context 槽):
  1. 提取最后一条用户消息
  2. embedding.embed(query)
  3. vectorstore.search(queryVector, topK*4)  ← 粗召回
  4. 阈值过滤 + 跨会话模式过滤
  5. 时间加权重排:
     finalScore = (1-timeWeight) * semanticScore + timeWeight * recencyScore
     recencyScore = exp(-0.1 * daysSince)
  6. 取前 topK,扩展上下文窗口并去重
  7. 物化为 system 块(带日期和来源标注)

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布