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。
// 拦截消息(不调用 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)
任何服务都可以被替换。提供同名服务的插件自动参与优先级竞争:
provide.use(agent, myAgent, { priority: 20 });3. 事件监听 (EventBus)
松耦合的发布/订阅模式,用于响应系统事件而不干预流程:
events.use.on('outbound:message', async (msg) => { /* 记录日志、统计等 */ });4. 贡献点 (Contribution Points)
向共享产物提交一块内容,排布权归收集方。与 hooks 的分工:改写或截停既有流程 → hooks;向共享产物添加自己的一块 → 贡献点。贡献者拿只读视图、不掌握控制流(无短路、无排序影响力、看不到他人产出),因此重复注入、排布漂移、错误连坐在 API 上无法表达。contributions 描述符来自 @aalis/api-contributions,默认提供者是 @aalis/plugin-contributions。
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,钩子表与贡献点表在各自的契约包,增广的模块要分开写。服务类型随描述符走,不往扩展点表里登记服务名。
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 与多实现解析
服务注册
provide.use(llm, deepseekService, { priority: 10 });provide 不接受 capabilities 选项——内核 DI 只关心「按名解析服务实例」。 领域级筛选(如按 LLM 模型的 tool-calling / vision 能力挑模型、按 storage root 选权限) 由各 -api 包在服务实例自己的元数据上处理(如 LLM 把能力挂在 model handle 上), 不再经过内核的服务能力匹配层。
服务消费
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,同时就绪者按登记序),然后:
- Phase A 成批关闭:本轮目标不再是 active 的(含 required 依赖已不在、仍在后台初始化的),连同 required 服务会随这批一起失去全部提供者的下游,它们之间的次序由关停编排按实际绑定决定。
- 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:command | InboundPhaseData | plugin-commands | (无) |
inbound:trigger | InboundPhaseData | 生效的触发插件(如 plugin-trigger-policy) | (无) |
inbound:flow | InboundPhaseData | plugin-flow-control | (无) |
inbound:dispatch | InboundPhaseData | — | 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:
// 定义钩子的插件
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 条消息时,在本轮最后一条输入之后注入一条继续执行的系统提示。事件列表
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 块(带日期和来源标注)