跳转到内容

插件架构

Wuu 通过 Go 运行时宿主和 Electron 桌面宿主加载插件。一个包可以使用其中一个或两个宿主,也可以只提供声明式贡献。本页说明当前实现如何发现、激活、组合和清理这些贡献;包字段和代码示例见编写插件。

Go 宿主负责工具分派、执行身份、服务路由、持久化与权限边界,以及资源清理。存储实现与权限策略通过服务选择,见可替换提供者。插件负责功能逻辑、提示词、业务状态和后台策略。自动化、记忆、子 Agent 等功能通过宿主服务工作,而不是另外维护一套会话引擎。

桌面宿主负责窗口、应用状态与动作、输入提交,以及通向 Go app-server 的桥接。所选 ui.shell 提供者负责应用布局和展示。插件通过公开 SDK 注册视图和贡献,不会获得 Electron 主进程对象,也不能直接访问宿主 React 状态。宿主提供 React 和共享 UI 组件,让插件控件遵循相同的渲染与样式契约。

内置包和外部包使用相同的运行时与桌面注册契约。内置来源影响发现和信任,不会改变工具、能力或 UI 注册的含义。复制一个包并使用内置插件的 ID,不会让它成为官方包。

Wuu 只维护一套官方插件编写 SDK:packages/plugin-sdk 中的 TypeScript @wuu/plugin-sdk。内置和外部插件使用相同的运行时与桌面契约。运行时传输仍是 与语言无关的 JSONL,其他语言编写的外部进程也可直接实现该契约,但 Wuu 不为 这些进程提供第二套官方编写 SDK。

Go 核心继续担任运行时宿主。internal/sdk 是 Wuu 入口使用的内部应用嵌入接口, 不是插件编写 API。

启用 Peers 后,新的协作请求会在默认桌面对话的来源轮次下方显示为紧凑卡片。 点击卡片会在主对话区域打开目标会话,标题栏提供返回来源会话的入口,并保留 尚未发送的草稿。会话消息的来源链接也会在主对话区域打开。卡片沿用行内报错 提示的间距和圆角,字号跟随 UI 字号偏好。

状态对应本次请求,不跟随目标会话后续的活动变化。已归档或缺失的目标显示为 不可用;刷新失败时显示“状态暂不可用”。来源会话可见时会刷新卡片,禁用 Peers 后会移除卡片。

此初版显示 Peers 仍保留的请求:最多 64 条记录,已结束的请求可在七天后或 需要腾出容量时清理。没有来源轮次 ID 的旧记录不显示卡片。原始工具结果和 会话消息仍保留在对话历史中。

九个内置业务插件为 todo、ask-user、memory、subagent、peers、 automation、goal、dream 和 next-message。源码位于 plugins/<id>/src/,均导出公开的 TypeScript RuntimePlugin 契约。构建会在内置 manifest 旁生成不依赖外部 npm 包的 runtime.mjs。Go 嵌入这些包,继续担任运行时宿主;原来的 wuu-<id>-plugin Go 辅助可执行文件不再需要。

桌面使用自身的 Electron 可执行文件以 Node 模式启动插件;CLI 使用 PATH 中的 Node,或 WUU_NODE_EXECUTABLE 指定的绝对路径。内置运行时无需单独安装 npm 依赖。内置与第三方插件遵循相同的 JSONL 初始化、服务协商、激活、取消和 关闭契约。

此次变化只替换实现语言,不改变插件身份或持久数据布局。现有设置、批准记录、 插件存储键、记忆 Markdown 文件和会话请求身份仍使用原有位置,无需导出导入, 也不会保留第二套 Go 实现。替换时会重新创建模块变量和定时器。Automation 使用稳定的分派身份恢复持久任务;Goal 将未完成目标恢复为暂停状态,等待明确 恢复;Dream 保留设置、运行状态和失败退避。禁用插件会停止它负责的行为, 同时保留持久数据和会话历史。

编辑热更新流程和可执行验收命令见开发内置插件。

发现流程依次读取用户包、工作区包、已授权开发目录和内置包。用户包通常位于 $WUU_HOME/plugins,工作区包位于工作区的 .wuu/plugins,同时兼容旧的单数 plugin 目录。ID 重复时,通常由后发现的候选覆盖之前的候选;内置包最后参与发现。经过明确授权的同 ID 开发包可以覆盖对应内置包,但仍保留开发来源标记。普通未信任的同 ID 副本不会获得这种覆盖权限。

发现 manifest 不等于执行插件。宿主还会检查启用状态、信任、平台与最低版本兼容性、包关系,以及运行时协商结果。必需包依赖可使用旧格式 requires ID 或带版本的 dependencies 条目。必需依赖缺失或不兼容时,消费者及其传递必需依赖者保持未激活;breaks 和必需依赖环可能拒绝整个激活计划。匹配的可选依赖在可行时增加排序约束,但其缺失或失败不阻止消费者。conflicts 只产生诊断,不自动决定保留哪一方。

安装本地包即信任其记录来源并启用。来源相同的更新继承信任与启停状态;来源改变需要确认。开发授权只适用于特定目录。包身份包含内容和来源上下文,因此仅凭 ID 或版本字符串,无法证明当前执行的就是已批准的字节。实际安装和更新流程见插件管理。

包关系不会安装依赖、启用被禁用的包或授予信任,也不会授予服务调用权限:运行时 provided_services 和 required_services 单独协商服务名与主版本。详见包关系。

未覆盖组合配置时,产品只包含通过依赖解析的包。移除或禁用附加插件后,其必需包消费者仍保持安装,但不会激活,并显示依赖诊断;可选消费者和独立包仍可激活。这不会放宽显式选择:用户组合中选定的模块或服务提供者不可用时,仍会拒绝准备。安装或启用被选中激活的消费者时,其必需依赖也必须可用。

App-server 客户端可以通过 extension/package/update 一次提交相关启停选择,例如 {"updates":[{"id":"plugin:user:provider","action":"disable"},{"id":"plugin:user:consumer","action":"disable"}]}。宿主校验完整选择,准备一个候选,通过一次 compare-and-exchange 持久化设置,再激活该 generation。每项可提供 fingerprint,用于拒绝过期的目录选择。数组不能为空,包 subject ID 不能重复,动作只能是 enable 或 disable;不能同时提供顶层单包字段,即使字段值为空。信任和待处理包更新仍使用原有单包请求。批量请求不会授予信任或自动选择依赖。对于请求显式启用、已获信任且被选中的包,准备阶段必须满足其必需的包依赖和服务依赖。校验或准备失败,以及已确认的 compare-and-exchange 拒绝,会保留原设置和 generation。写入结果未知时,应重新读取配置再决定下一步,不能假定已经回滚或自动重试。提交后的激活失败按下文约定报告已提交状态。

运行时将已准入的包关系带入同一张服务生命周期依赖图。满足条件的可选包依赖使提供方先于消费方准备和激活,并在消费方清理完成后退出,但不会传播提供方故障。如果服务绑定后某条可选包边形成环,宿主会忽略这条顺序边,而不是拒绝整个 generation。registry.introspect 的 dependencies 返回实际采用的图,dependency_issues 说明未采用的包关系(optional_missing、optional_version_mismatch 或 optional_cycle)。包版本与服务主版本独立;可选包依赖不授予服务调用权限。

热更新不会自动把进程或模块变量迁移到新实现。需要保留的业务状态应写入宿主存储或会话记录;持久数据格式可能变化时,由插件明确管理版本。会话历史与插件实现的生命周期彼此分离。

运行时 generation 是一组一起生效的插件资源,包括所选包、进程、工具、能力、hook、技能、MCP 连接、服务注册表和受管包快照。构建候选 generation 不会立即替换活动版本。开发发布可以在轮次运行期间进行:每个仍被引用的 generation 拥有自己的包快照,替换已发布源码不会改变运行中工作使用的文件。宿主协调目录读取与发布,不会在整个轮次期间阻止发布。

自动观察到仅新增、由默认组合纳入的附加插件时,可在后台准备候选,新旧会话继续使用已提交的 generation。前提是配置与来源授权不变、已有包不变,且必需 provider 健康。发布前,宿主重新核对目录 epoch、配置、包内容和基准 generation;后续变更或关闭会取消尚未发布的候选。同 ID 替换、移除、禁用、信任或配置变更,以及显式 composition 选择仍采用阻止新准入的切换流程。包发现与不可变快照复制仍同步执行;此优化移除的是初始化和准备回调的等待,并非这些开销。若结构性错误导致整个候选图被拒绝,也不会无限期豁免准入检查。

Lifecycle v2 将契约声明与状态验证分开。initialize 返回声明,不调用服务。完整依赖图绑定后,宿主按 provider 先于消费者的顺序调用 prepare。这个回调只能调用已声明服务中标记了 preparation: true 的方法;这些方法承诺不进行持久化写入或产生外部效果。所选原生与进程 provider 均遵循此规则。插件在 activate 中启动定时器等活动行为。准备阶段还可能建立 MCP 连接。这些都是可信进程:限制宿主服务调用,并不能阻止初始化代码直接访问文件系统、网络或产生其他外部效果。准备阶段不是无副作用的沙箱。

Initialize 还包含 supported_host_features,以精确的版本化名称声明宿主语义,与服务发现分开。插件可返回 required_host_features;SDK 会在回复前逐项对照宿主声明检查,宿主也会拒绝不支持的要求。缺少声明表示不支持任何此类能力;未知的宿主声明名称允许向前兼容,但不能代表其他能力。所需能力不是可选项,不能降级。含 authority 的文档 descriptor 会被 SDK 自动要求 state.documents.authority.v1;未声明要求的旧插件保持原行为。

运行时切换先准备候选,再提交相关包或策略的持久化变更,最后才调用原生 activate。准备或持久化提交失败时,关闭候选,旧运行时保持当前状态,不会调用候选的 activate。

provider 先于包和服务消费者准备和激活。必需 provider 初始化或激活失败时,阻止其传递必需消费者,包括经过纯声明式包的依赖关系。被阻止的消费者不暴露工具、hook、技能、MCP 服务或桌面贡献。独立包仍可激活;provider 自身的运行时失败不会自动移除其独立声明式贡献。

已激活的 provider 进程退出后,现有恢复检查会在准备替换版本之前撤销其传递必需消费者。这会执行正常的消费者清理,并阻止后续访问其工具、hook、MCP 服务、技能和桌面贡献。替换失败不会重新启用这些消费者。检测遵循现有恢复检查时机,不是即时崩溃通知。

撤销只作用于受影响的 generation。仍被引用的会话和工作者工具集不再列出或加载该代的插件技能,在其他 checkout 工作时也一样;独立的工作区技能仍然可用。旧 generation 中的故障不会撤销同包 ID 的健康新版本。已经物化的提示词和会话历史不会改写,已完成的效果也不会撤销。

禁用和移除在变更提交后生效,无需等待空闲轮次就会撤销仍被引用的各 generation 中受影响的插件;应核对操作结果。需要重新启动 app-server 且不激活插件贡献时,可使用其安全模式选项。

桌面端另有原生菜单 打开恢复中心…,不依赖应用渲染器正常运行。使用 --safe-mode(或 WUU_SAFE_MODE=1)启动桌面端,会打开独立诊断窗口,不启动普通应用后端或插件,也不加载普通配置。已保存的数据保持不变,不会替换存储提供者。

恢复中心提供有次数上限的重新加载、确认后诊断/正常重启,以及可预览的最小诊断导出。停止等待不等于取消后端任务。重启可能中断正在运行的任务,外部操作可能已经发生。正常重启使用原配置,并非回滚到上次可用状态。恢复中心不会重放任务或永久修改插件策略。报告仅包含版本、平台和模式,不包含原始日志、配置、路径或会话内容;导出时不会覆盖已有文件。

提交成功后,如果激活失败,宿主仍发布处于降级状态的新 generation,不恢复旧代。操作会明确报告变更已提交、但运行时激活失败,插件清单保留失败诊断。变更处理仍会同步已提交的设置,完成适用的卸载收尾,并通知客户端。Refresh 接纳这个已发布状态并记录激活错误,不会把它当作刷新失败,在每次监听重试时反复激活。激活已经产生的外部效果无法回滚。

仅新增插件时,宿主可以异步准备候选,其他轮次继续使用已提交的服务图。CLI 发布成功不代表已运行的 app-server 已准备好新插件。开始依赖新插件的工作前,应检查同一服务器的 config/read 清单或 plugin/inventory/changed 通知:准确的已发布 fingerprint 必须已启用,并在已提交的 graph_generation 中具有 runtime_state: "active"。出现 last_error 或失败运行状态时,应报告诊断,而不是重试依赖它的模型调用。已有轮次仍保留原来的绑定。

普通同 ID 更新发布后,已有会话在下一次空闲轮次边界接入新 generation。正在执行的轮次、其未完成工作者(包括终态收尾)和受管进程继续使用原 generation,直到这些工作结束。宿主整体重建插件绑定,但保留会话历史和身份;旧 generation 在最后一个引用释放后清理。已被替代的空闲运行时无需等待下一条用户消息就会释放。仅目录信息变化或提交前失败,不会替换健康的已有 generation。后续清理错误单独报告,不会回滚已提交的变更。

本机浏览器和电脑操作权限的生命周期短于被保留的工具与提示词代码。激活替换版本前,宿主撤销旧 generation 的本机控制归属,并停止受影响的预览。禁用、移除、关闭运行时,以及绑定活动的 MCP 连接断开或替换,也会撤销对应所有者。插件撤销按插件 ID 与 generation ID 定位,MCP 连接撤销还会定位到确切的连接身份。旧所有者的迟到故障不会停止健康的替换版本。同一插件 generation 内重新建立的健康 MCP 连接,可以为明确的新用户任务提供服务。撤销后,受影响任务不能继续本机操作,旧所有者也不能重新取得控制权或切换到另一种本机控制后端;需要明确的新指令才能开始新工作。

Generation 拥有的 host.background 任务与会话工作者的生命周期不同。发布时,宿主在激活替换版本前,停止旧 generation 接收新的后台任务,并取消已有任务和自主服务执行。插件自行创建的定时器及其他直接效果仍须显式清理;宿主取消不能撤销已经产生的效果。

准备阶段中,候选初始化失败或必需服务协商失败时,不能替换已有的健康运行时。新发现的可选插件可以单独处于失败状态,不必停掉其他插件。明确移除插件与替换失败是不同操作。

自动刷新无法准备同一候选时,重试等待从 1 秒逐步增加,最长 30 秒。等待期间,后台通知和排队回合共享约 1 秒一次的包内容检查;配置或新发布的目录 epoch 可触发立即检查。输入发生变化时可以提前重试。刷新成功会重置等待时间。租约占用采用独立的短间隔重试,不计为候选失败。目录中的手动「刷新」操作跳过这段自动等待,返回准备错误,并在提交前失败时保留上一代运行时。

热更新保留会话历史,以及用于支持该机制的 provider 路由的会话级 prompt_cache_key。稳定的缓存键不是内容哈希,也不保证缓存命中。仅实现变化时,模型可见前缀可以保持不变;工具定义、提示词片段或延迟发现状态变化会改变请求内容,即使缓存键相同,也可能无法复用 provider 前缀。

依赖者先于其 provider 清理;禁用或移除时撤销仍被引用的旧 generation 也遵循这一顺序。可选依赖失败不会级联阻止消费者。清理会关闭 MCP 连接、移除提示词和压缩注册、取消插件执行、关闭进程、撤销服务路由,并删除受管快照。关闭路由前,shutdown 仍可访问释放插件工作所需的宿主服务。插件仍须自行停止定时器、订阅和后台任务;宿主无法推断扩展代码创建的任意资源。

每个 generation 使用独立的 cordis-go 作用域管理原生和进程运行时客户端,已解析的依赖图提供准备与激活注入关系;注册表不再维护第二套原生 provider 生命周期。重复运行时身份会在启动进程前导致候选被拒绝。客户端资源归属于激活作用域之外,因此准备完成但未激活或激活失败的客户端仍能正常关闭,provider 也会保留到消费者清理返回。Wuu 串行执行生命周期变更,撤销返回前会等待清理完成;准备、激活和清理中的 panic 会转为可定位到提供者的错误,已关闭的 generation 不能再次激活。执行和 generation 租约仍遵守前述版本保留规则,Cordis 不会单独重载仍被引用的 generation。

清理过程产生结构化撤销记录,保存在 Wuu 主目录的 plugin-generation-revocations.jsonl 中。记录将发布成功与旧资源清理失败分开,便于定位未正常关闭的资源。

运行时进程写入遵循取消信号和有上限的传输等待时间。消息传输中断或执行取消消息无法送达时,宿主会关闭该传输;尚未发送的调用被取消时,其他已运行的工作不受影响。关闭时,macOS/Linux 会终止运行时进程组,Windows 会请求终止进程树;日志管道的排空等待也有上限。Windows 运行时已退出时,进程树清理仅作尽力尝试。关闭期间的异常非零退出仍会记录在撤销报告中。运行时 stderr 仅保留最近 64 KiB,并标注已省略的早期输出,避免日志缓冲无限增长。这些限制不会缩短工具本身的执行期限,也不会改变前述 generation 保留规则。

运行时插件使用 JSONL 通信。版本化服务统一通过 host.service.call 调用:provider 声明名称、版本和方法,消费者声明服务名与主版本。注册表将消费者绑定到 provider,并提供调用者身份。缺少必需 provider 时,消费者无法正常启用;同名同主版本的多个 provider 必须明确选择,存在歧义的候选组合会在激活前被拒绝。

在可信的用户配置中选择运行时服务提供者:

{
"composition": {
"version": 1,
"services": [
{ "service": "search.provider", "major": 1, "provider": "my-search" }
]
}
}

所选包须已启用、受信任,并声明指定服务主版本。绑定不会安装或启用包。重复绑定、指定提供者不可用或服务有歧义时,准备失败,之前运行的 generation 保持不变。所选提供者崩溃或移除后,调用失败,不会回退到其他已注册实现。没有显式绑定时,服务必须恰好有一个提供者。注册表检查会同时返回版本、提供者 ID 和 binding_source,后者标识选择来源层;无歧义的自动发现服务使用 unique-provider。

组合版本 1–5 与产品 CalVer、服务版本相互独立。版本 1 支持内联 profile 和模块/服务选择;bundle 与补丁要求版本 2 或以上。新配置使用版本 5。由于绑定能选择执行和凭据策略,正常启动忽略项目中的 composition 覆盖,包括大小写变体。配置变更在 generation 准备时应用,已接纳的执行保留原注册表。恢复选择会创建新的 generation,不会撤销外部效果,也不会自动迁移提供者数据。

读取版本 1/2 时,通过只读兼容映射保留从宿主迁出的行为:选择 wuu.session-execution 时补入 wuu.agent.policy.default;选择该会话消费者或 kernel 时补入 wuu.model-execution 和原生模型提供者 wuu.model.native;选择内置桌面 surface 时补入 wuu.default-application。旧产品会话还会选择 wuu.engines.native 与 wuu.engine-execution;只有 kernel 的状态作用域不会因此加载引擎。显式替换绑定和显式排除保持不变,空产品仍为空。检查结果以 migration:v1-to-v3 或 migration:v2-to-v3 标注这些补入项。原配置不会被改写,仍可配合旧程序恢复。

版本 3 保留原有目录,不会重复这些执行迁移。版本 1–3 还会以 migration:vN-to-v4 投影显式资源授权边界:会话宿主选择 wuu.resource-access 和原生资源策略,但保留显式排除与替换。版本 4 不会补足缺失依赖,必须明确包含所需提供者或选择替代实现。版本 1–4 同时选择 renderer 和旧 goal 或 automation 运行时时,还会通过 migration:vN-to-v5 保留原有功能界面;显式 UI 排除仍有效。版本 5 独立选择运行时包与 UI 包。将旧配置改为版本 5 前,应显式包含映射后的提供者,详见配置升级。降级不能仅改版本号,应恢复此前兼容的配置和提供者版本。

开发者和运行中的 agent 可以通过公开的 host.composition 服务试跑独立运行时,显式为逻辑 agent 选择组合并撤销选择,无需发布全局 generation。

具名运行时组合也可以选择完整的模块集合。例如,安装并启用两个服务组合示例包后:

{
"composition": {
"version": 1,
"profile": "writing",
"profiles": {
"writing": {
"modules": ["text-analysis-example", "writing-report-example"],
"services": [
{ "service": "example.text.analysis", "major": 1, "provider": "text-analysis-example" }
]
}
}
}
}

profile 选择 profiles 中的一项。每个 profile 提供 modules、有序的 bundles 或 patches,并显式包含所需依赖。模块顺序不决定服务选择或生命周期顺序。显式空数组不启动任何模块;同时省略 profile、modules 和 patches 时,保留已启用、受信任的默认发现结果。未知 profile 或 bundle、重复 bundle、重复模块、被模块集合排除的绑定目标、不可用的指定模块或指定运行时启动失败都会拒绝准备。普通模块选择不能安装、启用或授予包信任。筛选发生在运行时、hook、技能和 MCP 贡献加载之前。

Bundle 是可复用的静态配置。例如,多个 profile 可以共享同一个分析能力:

{
"composition": {
"version": 3,
"profile": "writing",
"bundles": {
"analysis": {
"patches": [{
"modules": { "text-analysis-example": true },
"services": [{ "service": "example.text.analysis", "major": 1, "provider": "text-analysis-example" }]
}]
}
},
"profiles": {
"analysis": { "bundles": ["analysis"] },
"writing": {
"bundles": ["analysis"],
"patches": [{ "modules": { "writing-report-example": true } }]
}
}
}
}

解析从空模块集合开始,按列表顺序应用所选 profile 的 bundle,再应用其内联 modules / services,然后应用其 patches。之后应用顶层 modules / services,最后应用顶层 patches。每个内联 modules 数组替换完整集合;服务按名称和主版本逐项覆盖。只有 patches 的组合也从空集合开始。Bundle 包含补丁和可选模块默认值,不执行代码,也不引用其他 bundle。

版本 2 及之后的 bundle 可以使用 optional_modules 数组声明包 ID。这些包在该 profile 中默认启用,并在可用时被选中;包缺失、不兼容、未受信任或被用户显式禁用时,不会阻止组合启动。此默认值不会授予信任。显式模块选择或 patch 优先于可选默认值;必需的服务绑定仍要求其 provider 可用。

补丁中的 modules 将模块 ID 映射为 true(包含)或 false(排除)。services 替换指定绑定;remove_services 接受 { "service": "...", "major": 1 } 条目以移除显式选择。移除绑定会恢复唯一提供者解析;若仍有多个提供者,准备仍然失败。排除模块不会悄悄删除指向它的绑定,须在同一组合中移除或替换这些绑定。同一补丁中的重复或冲突操作会失败,后续补丁可以有意覆盖之前的选择。配置保存会保留显式空数组。每次解析都会创建独立状态,一个 profile 不会修改另一个。

原生模块包括 kernel(原生服务集合)、wuu.default-loop(driver.default)、wuu.tool-execution(工具安全服务消费者)和 wuu.session-execution(所选驱动消费者)。状态、权限、Project 协调和应用外壳分别有自己的提供者协议。显式模块集合需包含所需的全部依赖。排除 kernel 会移除其服务和工具结果物化功能,不会自动选择替代实现。安全模式不启动任何组合模块,即使配置指定了不可用的包;发现功能仍可用于恢复。

plugin/registry/introspect 返回 composition.profile、按顺序应用的 layers、最后修改模块集合的 module_source,以及每个所选模块的 source、包版本和当前运行时状态。来源标识内联选择(profile:<名称> 或 composition)或从零计数的补丁(bundle:<名称>:patch:<索引>、profile:<名称>:patch:<索引>、composition:patch:<索引>)。原生模块版本标识产品构建,服务版本另行记录。没有运行时的包不带运行时状态。即使排除了原生注册表检查提供者,此诊断仍然可用。同 ID 更新保留正在执行的工作;移除模块会撤销所有保留 generation 中的对应模块,并让消费者先完成清理,再停止提供者。

包依赖与所选服务依赖使用同一张 generation 生命周期图。提供者先于消费者激活,清理按相反顺序进行,期间提供者资源保持可用。必需服务缺失、主版本不兼容或提供者失败会阻止依赖链上的消费者运行。可选服务缺失不会阻止激活;存在时仍参与生命周期排序。跨越包与服务的依赖循环会在激活前导致准备失败。新版本改变依赖时,仍被保留的 generation 使用自己的解析结果。移除提供者会停止必需消费者,独立插件继续运行。

wuu.state.sqlite 和 wuu.state.jsonl 提供 state.documents@1,外部提供者可以实现同一协议。产品会话存储通过所选文档服务保存历史、执行/恢复状态、驱动绑定和 Project 状态。旧存储转换保留来源,遇到未知非空表或目标数据冲突时明确失败。仅选择后端不会完成数据迁移。

消费者声明 requireDocumentStore() 并使用 documentStore(host);提供者声明 documentStoreDescriptor()。提供者指南说明有界查询、绑定 revision 的游标、完全相同事务的回执重试、可选压缩/导入和离线迁移。客户端始终使用捕获的 generation。普通方法不声明清理权限;关闭持久化需要生命周期所有者的准入。

提供者可通过 documentStoreDescriptor({ checkpoint: true }) 声明 checkpoint {},使用 state.documents.checkpoint.input.v1 / output.v1,并允许准备阶段调用。返回 { version: 1, revision, documents_digest, receipts_digest },摘要算法与完整导出一致。宿主在启动时使用已提交的 checkpoint,仍会验证存活进程是否共享同一存储。两个原生后端按 generation/revision 保存摘要,并根据变更键和新回执增量更新;元数据缺失或过期时重建。旧存储的准备阶段只计算首次摘要,不写文件,激活时仅在绑定和 revision 未变化时保存。普通启动不进行全库物理完整性审计;显式 check、导出和迁移仍检查完整数据。未提供该能力的实现继续使用分页校验。摘要不匹配会拒绝接入,能力调用失败也不会切换到其他提供者。

桌面启动优先加载当前工作区,并尝试恢复窗口记录中指定的会话。旧会话恢复失败时保留原数据和目录入口,打开可用的新对话并说明原因。已完成的旧数据导入为当前权威数据,启动不再重新计算旧源数据库的指纹。旧投影清理分批保存进度,跳过无法解码的记录;当前会话的原子写入和执行恢复保证不变。

SQLite 使用完整同步的 WAL 事务。JSONL 追加带校验和的记录,并从日志重建索引;只丢弃不完整的末尾记录,完整但损坏的记录返回 state_corrupt。两者均通过操作系统锁串行执行操作,并验证同一个持久权威指针。迁移先准备并检查新 generation,再原子发布该指针。之后所有旧句柄,包括其他进程中的句柄,都返回 state_binding_changed。直接选择另一后端会返回 state_migration_required。应停止业务消费者、迁移,再选择并启动目标提供者。发布前中断时原存储仍是权威;发布后目标存储成为权威。旧 generation 保留用于检查,不进行双写。非空目录丢失权威指针时明确失败,不会悄悄创建空库。

管理命令显式指定文档存储目录:

Terminal window
wuu state check --dir "$STORE_DIR"
wuu state export --dir "$STORE_DIR" --file snapshot.json
wuu state import --dir "$NEW_STORE_DIR" --backend jsonl --file snapshot.json
wuu state migrate --dir "$STORE_DIR" --from sqlite --to jsonl

快照包含文档和完整持久回执序列。导入要求空目标目录,在发布前检查恢复数据,不覆盖已有状态。check 校验数据并报告数量与绑定身份,不输出文档值。文件系统屏障必须成功;不支持目录同步时返回错误,指针发布后的错误明确报告已提交的绑定。依赖替换实现前,需要在目标文件系统和平台验证持久性。回执完整保留、不裁剪。export {} 仍会将完整快照载入内存;提供者通过 documentStoreDescriptor({ paged_export: true }) 显式启用分页,客户端使用 exportPage() 调用具有 v2 输入/输出 schema 的同一方法。checkpoint 扫描在有序文档页和回执页之间固定 revision 与 generation,默认请求每页 32 MiB、256 项。请求上限为 32 MiB、10000 项。多项页须满足字节目标;单条超大记录可超过目标,但仍受外部现有 64 MiB 帧限制约束,不能回退。payload 内存为 O(页 + 最大记录),另需 O(回执 ID) 校验元数据。JSONL 冷启动重放大帧仍有独立的内存边界。完整契约见分页导出。运行 node scripts/document-state-e2e.mjs 可在独立目录仅安装公开 SDK,验证进程消费者、重启重试、后端迁移、撤销和 CLI 数据转移。可复查证据保存在 .tmp/e2e/document-state/。

空驱动 profile 通过服务图选择 driver.default。原生 wuu.default-loop 模块与外部提供者使用相同的驱动协议、声明式依赖、取消和清理路径。排除该模块会停止其驱动;组合可以改为包含提供 driver.default 的外部插件。没有提供者时,新回合会明确失败。实验性的 driver.<profile> 桥接将提供者和回调表固定在同一 generation。描述信息、创建和恢复调用遵循回合取消。描述信息须包含非空 ID 与语义化版本,读取失败会在创建实例前返回错误。创建、恢复及终态 checkpoint 须匹配所选驱动的 ID、版本和 checkpoint 契约。未知终态或不兼容 checkpoint 会明确失败,保留最近接受的状态。取消后迟到的实例会收到有时限的关闭请求。回合在保存 checkpoint 后失败或取消时,返回结果会保留最近接受的 checkpoint,防止应用终结流程将它覆盖为实例初始状态。即使取消导致终态回执未能送达,执行中已观察到的历史仍可用于最终持久化。driver.<profile> 版本 2 在失败状态、回执和 checkpoint 之外返回结构化 failure,保留取消、期限到期、空回答、摘要输出超限、提供者分类及已消耗的恢复预算。使用 driverServiceDescriptor(profile) 并返回 DriverRunResult;服务版本 2 与驱动身份和 checkpoint 版本独立。

会话在创建或恢复驱动实例之前,持久化绑定服务 profile、提供者 ID、驱动 ID/版本及 checkpoint 契约。更改默认配置不会替换这一身份。重建运行时使用保存的 profile,在推理前拒绝不同提供者或不兼容版本。组合仍须为该服务选择已保存的提供者;改变同一服务的绑定后,旧会话保持只读,直到恢复原来的选择。绑定与 checkpoint 写入在事务中校验已保存身份。会话不存在或写入失败时,不会确认 checkpoint 已保存。Fork 创建在同一事务中继承来源绑定,并以空的执行 checkpoint 开始。明确标记为临时的会话在内存中保存绑定与 checkpoint,运行时重建会保留它们;关闭会话或进程会丢弃这些状态。

所选文档存储保留驱动身份和 checkpoint。旧数据转换保留原 session_driver_bindings 记录,并将历史原生会话绑定到已知默认实现。没有记录 profile/提供者的旧外部 checkpoint 会返回迁移诊断;宿主不能安全推断缺失绑定。备份必须一起保留绑定、checkpoint 和历史。忽略这些身份的旧版本不能安全恢复替代驱动。

Go 嵌入调用方也须显式选择驱动。StreamRunner 要求提供 LoopDriver 或 AcquireLoopDriver;worker manager 缺少 AcquireLoopDriver 时,在生命周期钩子、模型请求和工具执行之前失败。loopdriver.AcquireLocal(driver) 绑定由调用方持有的进程内驱动,每次获取时检查其持久化身份。

后台 worker 同样使用所选驱动。在产生效果前,会保存服务 profile、提供者 ID、驱动 ID/版本及 checkpoint 契约。后续回合和重启恢复保留该绑定;提供者缺失或不兼容时明确失败。驱动选择功能引入前的记录保留原生默认驱动身份。worker 执行持有 generation 租约,直到终态持久化和清理完成,并使用与父执行不同的回调 ID。待恢复的 checkpoint 与其执行输入单独保存,不覆盖最近已提交的终态结果,避免崩溃恢复抹去尚待投递的结果;终态快照提交对应的执行 ID。worker checkpoint 属于恢复数据,须与历史一同保留;忽略驱动绑定的旧版本不能安全恢复使用替代驱动的 worker。

所有驱动,包括默认多轮提供者,均声明 driver.turn 主版本 4 和 driver.checkpoint 主版本 1。SDK 的 driverTurn(host, execution) 提供以下操作:

操作可观察行为
read()返回绑定的模型默认参数、可用工具及包含已恢复工具回执的当前活动历史。
settings()返回所选预算、压缩设置、系统段元数据及可用资源,不执行这些资源。
contextState() / saveContextState(state)读取或保存绑定到当前身份的请求插入段和上下文估算。模型请求前应保存插入段;缺少 retained 会清空插入段,缺少 usage 则保留原估算。估算不能覆盖权威推理用量。
observe(observation)报告请求结构和压缩决策;before_compact、after_compact 调用已配置的钩子;提案失败时,after_compact 通过 compact.failure 携带分类原因。
compact(messages, budget)向所选压缩提供者提交明确的恢复观测并取得提案,由驱动另行验证、提交;分类失败抛出 DriverOperationError。
archiveContext()归档已接纳的原始历史并返回持久化地址,不替换活动上下文。
admitInput()在驱动选定的边界消费排队输入,返回新增贡献与活动上下文;引导输入先归档,再返回其持久化地址。
requestContext()读取仅供请求使用的上下文段,包括类型化块及其模型消息投影。驱动决定组装和保留方式,读取不会将它们追加到历史。
transformRequest(request)对请求的独立副本执行已配置的请求变换,不接纳输入或调用模型;强制选择的工具须存在于结果工具集中。
context()返回模型活动历史、执行内 revision、持久化归档位置,以及是否支持持久化替换。
replaceContext(revision, messages)先归档已接纳的原始消息,再原子提交驱动的替换内容及并发持久化的尾部消息;过期 revision 在写入前被拒绝。
model(request)通过所选模型管线执行一次流式请求,返回助手回复;不会接纳输入、追加请求上下文、执行请求变换或调用工具。操作失败时抛出 DriverModelError,保留 round.messages、已报告的 round.usage 及结构化 failure 证据。
modelWithTools(request, batchID)显式允许安全的前缀工具在模型流式输出期间启动,并在请求前占用唯一批次 ID。成功返回的非空 tool_batch_id 须通过 tools() 按原顺序提交全部返回调用以等待结果,之后才能接纳输入、提交其他效果、替换上下文或完成回合。
tools(batchID, messages, calls)通过所选执行器和持久化工具账本执行明确的批次;批次 ID 在同一执行内唯一,调用 ID 在同一模型轮次内唯一;前面批次结算后,新的模型操作可以重用提供者的调用 ID。驱动可直接选取工具,执行前会补齐缺失的助手调用声明。待执行调用可以分批提交;返回值仅包含工具结果消息。
complete(content, messages?, completion?)返回终态回执,保留完成原因、停止原因和截断元数据。模型与工具消息已被记录;可选消息用于追加驱动自行生成的内容。
checkpoint(checkpoint)保存与该驱动身份及 checkpoint 契约精确匹配的状态;外部效果开始前应先保存执行意图。

操作归属于当前执行及提供者 generation,按顺序执行,取消或完成后拒绝新的效果调用。模型事件进入会话事件流,工具完成清理后才释放其执行资源。用户引导输入持久化失败时,不会将该输入送往模型。取消不会撤销外部效果;恢复中断的工具批次前,必须先通过持久化回执确认结果。

原生驱动模块也可使用相同的独立回合操作。回调依赖须显式声明,调用通过服务注册表并携带提供者的执行身份;原生模块调用未声明的回调同样会被拒绝。模型请求与上下文操作保留客户端消息 ID、历史序号、引导输入标记及工具调用 ID;这些字段用于关联记录,不授予调用权限。

driver.turn 4.0 增加默认算法所用的资源,并将调用 ID 的唯一范围设为单个模型轮次。SDK 与依赖的服务主版本须同步更新,旧主版本会被拒绝。输入接纳、请求上下文和请求变换仍是显式操作:在循环输入边界调用 admitInput(),执行驱动的压缩策略,组装 requestContext() 段,再在 model() 前调用 transformRequest()。重试已准备好的请求不会消费更多输入或重复变换。checkpoint 契约与默认驱动状态格式仍为版本 1,已有会话身份和历史保持可恢复。

下一步生效的模型选择是主版本 4 内的显式可选能力。所选引擎同时声明 host-managed-continuation 与 step-model-selection;名字不授予支持能力,旧的替代引擎明确拒绝意图。API 接纳和持久化意图的消费/恢复都校验同一保留 generation 的引擎与驱动描述。驱动声明 step_model_selection,在每个模型步骤前接纳输入,再重新读取 read() 与 settings(),之后才组装提示词、工具定义、媒体策略和预算。须将 read().model_selection_revision 原样保留到 transformRequest() 和 model() / modelWithTools();过期 revision 或不匹配的模型、提供者会在发送前被拒绝。默认驱动实现此边界;未声明能力的驱动保留整轮绑定行为,并明确拒绝下一步意图。

对于正在运行且引擎、驱动具备上述能力的持久化会话,可调用 config/model/update,参数为 { "thread_id": "…", "provider": "configured-provider", "model": "configured-model", "apply_at": "next_step" }。返回的 model_selection 确认持久化 revision,不代表已改变正在发送的请求。省略 apply_at 时仍采用原有空闲时选择行为。持久化下一步意图使用独立的可选操作 session.domain@1.SetNextStepModelSelection(id, intent),而非 SetRuntimeSelection。根据最近一次 Find 的结果传入 { ExpectedRevision, ExpectedEngineID, Selection };Selection 包含模型字段以及未改变的 PermissionMode / ApproveForMe。提供者须原子比较已保存的 revision 和引擎,不匹配时以 session_model_selection_changed 拒绝,并拒绝权限修改;提交只改变模型字段、设置 model_selection_apply_at: "next_step",将 revision 精确递增一次。相同 revision 的并发请求只能有一个成功。实现此操作的提供者也须在旧模型选择写入(SetRuntimeSelection / SetModelSelection)中递增同一 revision 并清空待生效边界。恢复读取持久化的成功结果。

未声明此操作的旧会话提供者在任何写入前以 method_not_found 拒绝。消费者不得回退到 SetRuntimeSelection,也不得因确认失败而回滚覆盖并发变更。原有 SetRuntimeSelection 契约仍绑定整个 turn。新操作若返回格式错误或不一致的确认,应视为 session_outcome_unknown,先检查持久化状态再重试。存储确认不等于运行时授权:消费和重启都会在发送前重新校验引擎和驱动能力。

已准备的步骤及其重试固定使用同一模型、提供者、提示词、媒体能力策略、选项、工具权限和用量链路。成功步骤后的下一次接纳读取最新意图,在预算检查和发送前重新组装模型输入。选择不会替换活动执行器、修改权限或执行环境,也不会采纳新插件 generation。只接受已配置、已启用且兼容保留工具面的模型;新增连接或替代 generation 须等到空闲回合边界。取消后不会启动下一步骤。重启保留意图并重新校验;所选提供者不可用时明确失败,不静默回退。启用模型输入回执时会记录选择 revision。

只有显式选择下一步切换的执行才新增 model_step 事件与逐步元数据;普通轮次保持原有事件顺序和历史结构。model_step 事件报告实际发送的配置模型/API 模型、提供者、操作 ID、revision 和上下文上限。线程的 running_model* 字段与目标选择分开;turn/usage 携带当前步骤身份,计数仍为回合累计值。持久化用量和 trace 保留 model_steps 归属,混合模型回合不再伪标为单一模型。用量查询按操作展开且不重复计算汇总,回合恢复仍使用一条汇总记录。

续接字段在主版本 4 内增量提供。settings().resume_sampling 表示用户明确要求在先前已完成回复之后继续采样,驱动不能直接将该回复作为新执行的终态结果返回。失败的模型轮次可携带 completed_messages,表示提供者已确认完整、可用于后续请求的消息项。其他部分输出仍在历史中可见,但会标记为不完整流片段,并从续接模型输入中排除。

模型操作失败返回包含 failure、部分助手消息及已报告用量的结果。SDK 将其转换为 DriverModelError;原生驱动会同时收到 FailureError 和部分 ModelRound。失败证据包含来源、类别、提供者代码、HTTP 状态、重试延迟、上下文溢出分类,以及可用时的恢复执行器停止原因和尝试次数。底层原因可重试不代表获得了新的重试预算。传输、调用权限及请求格式错误仍返回服务错误。直接使用协议的消费者须检查 failure;回复包含部分输出不代表操作成功。失败回复中的可见文本和图片会保留在历史中,不完整的工具声明和提供者重放状态只保留在失败回复中。

round.preempted 表示成功的模型轮次被新接纳的用户输入提前结束。驱动应保留其中的可见部分消息,并在同一回合继续处理该输入,不要把这次取消当作传输失败。

工具可能在模型回复前启动,因此应先保存执行意图。即使模型失败,round.tool_work_started 仍会报告已经启动的工作。失败的模型操作会取消工具并等待其退出,批次 ID 不能重复使用;调用 ID 保留到下一次已接纳的模型操作。成功的流式回执会等待原执行结果,不会重新发起工具调用。执行清理还会等待被最终回复遗漏的调用及嵌套调用退出,再释放工具提供者。工具启动后,模型流不能自动重放;未配置持久化账本时也遵循这一规则。

配置回执存储时,两种模型操作都会在发送前提交模型输入回执。回执以模型操作 ID 关联所选驱动及执行身份、最终消息和持久化输入地址、工具与请求设置。回执写入失败会阻止模型及流式工具执行。写入期间取消可能留下已提交但未发送的回执,因此仅有回执不能证明提供者已经接收或完成请求。原生与进程驱动遵循同一屏障。

替换活动上下文会保留原始历史。归档成功但 checkpoint 提交失败时,归档保持持久化,原活动上下文仍有效;重试前应重新读取 revision。提交后的取消不会撤销已提交上下文。缺少归档或原子 checkpoint 服务时,持久化替换不可用。嵌入调用方仅可在未配置持久化上下文写入者时显式传入 { volatile: true };该模式替换执行内存,有归档能力时仍可保留原始历史,但不承诺上下文 checkpoint。资源服务不会隐式选择此模式。

取证工作流示例 仅依赖公开 SDK:先规划一个工具批次,提交仅包含当前任务和证据的活动上下文,再禁用工具进行综合回答。此示例要求支持持久化上下文替换。checkpoint 记录当前阶段;恢复使用已结算的工具回执,不会重放效果。在检出目录运行 node scripts/workflow-driver-e2e.mjs,可在独立目录安装打包后的 SDK,并验证真实文件证据、原始历史保留、更改默认 profile 后的已提交上下文恢复、规划历史尚未写入时的中断、驱动自主处理上下文溢出,以及移除提供者。该检查还会运行独立 SDK 驱动,在模型流结束前读取真实文件,并仅等待一次对应的持久化结果。同一验证会排除原生默认模块,让工作流接管 driver.default;将示例的运行时环境变量设为 WUU_WORKFLOW_PROFILE=default 即可声明该服务。可重复核对的日志和安装包保留在 .tmp/e2e/workflow-driver/。

默认 agent.DefaultDriver 拥有多轮调用、压缩、溢出恢复和终止策略;模型、工具、上下文、观测和 checkpoint 效果均通过相同的公共资源执行。原有整段循环网关及宿主持有的循环入口已删除,嵌入调用方须显式选择提供者。工作流示例会继续保存的任务上下文,不会将新的用户任务合并到中断的工作流中。目前驱动 profile 通过 debug app-server 的 --driver 选项选择,桌面尚无相应选择控件。

核心服务覆盖存储、设置、会话、历史、工作区、产物和执行操作。功能服务也使用相同的路由机制。服务方法的 schema 标识描述其契约,不授予直接调用其他插件私有进程或读取私有存储的权限。

工具和能力分派会创建有明确边界的执行作用域,进度、子工具调用、提问和产物都附属于活动执行。取消会关闭作用域,不等待插件确认,迟到的更新也不能重新激活它。插件收到中止信号后,应将其传递给可取消的操作。服务注册表关闭时拒绝新工作,等待已有调用结束,并在关闭上下文到期时取消剩余调用。

原生与进程服务提供者使用相同的声明、方法检查和回调作用域。原生消费者也必须声明依赖,并加入同一生命周期图。名为 kernel 的调用者没有特殊权限。驱动实例的执行 ID 通过领域参数传递,不能用作服务执行 ID。service.invoke.execution_id 属于提供者回调;消费者从活动执行中调用时,caller_scope 携带经过验证的调用者身份、种类(tool 或 callback)及会话和工作区归属。SDK 通过 execution.caller 暴露这些信息和独立的 signal。该信号跨越单次服务响应存活,在调用者结束或提供者停止时中止,适合管理跨越 start/read/cancel 调用的模型流等资源。只需存活到本次回调结束的工作使用 execution.signal。嵌套调用使用提供者自己的执行 ID;调用者归属信息不授予冒用调用者身份或工具权限的能力。

方法可声明 cleanup: true,在停止接收自主任务后释放资源。清理方法不得获取新资源或启动工作,也不会收到调用者作用域。注册表快照使用提供者 ID 和绑定来源标识实现,原 kernel 标志已移除。这些契约变化不改变产品 CalVer,也不修改用户存储数据。

能力在指定位置参与组合,例如添加提示上下文或观察轮次完成,不是对 agent 循环或 provider 请求的任意拦截。支持的能力和错误策略见编写插件。

桌面宿主加载符合条件的包的 ESM 入口,调用 activate(api)。注册归属于该包的 generation。宿主先验证声明的贡献和视图目标,再发布候选并清理旧桌面 generation。候选注册失败时,只清理候选,不替换已有的桌面 generation。

每个候选的异步暂存限时为 30 秒,覆盖桌面模块请求、ESM 导入和 activate(api)。超时、卸载或更新的清单同步会结束待完成的激活,不再等待插件返回;宿主清理部分注册,并拒绝迟到的注册。替换超时仍保留上一个正常工作的 generation。

api.signal 表示该 generation 的生命周期。插件应将它传给可取消的操作,并在等待前注册资源清理。暂存失败、被更新的操作取代或已发布的 generation 被释放时,信号会中止;成功激活会清除启动期限。这属于协作式取消,不能打断同步 JavaScript、停止已开始的导入或撤销外部副作用。

必需 provider 必须成功完成桌面激活,消费者才会启动;独立分支可并发初始化。替换 provider 也会重新加载未改动的消费者;移除或禁用必需 provider 会阻止消费者。清理时先释放消费者,再释放 provider。替换失败可以保留该包的旧桌面 generation,但消费者不会针对这个未成功暂存的必需替换激活。可选依赖启动失败不阻止消费者。

这个桌面事务与 Go 运行时事务分开。不能假设包安装、运行时激活和所有 renderer 窗口具有统一的原子回滚。应根据失败位置查看运行时或桌面诊断。

不同 UI 扩展点承担不同职责:

扩展点用途
View 与放置规则在宿主管理的布局区域中显示插件内容
Slot在固定插入点添加小型贡献
UI contribution向明确归属的 Slot 提供内容或包装
Presenter渲染版本化语义快照,调用宿主管理的动作
主题与限定作用域的样式在公开 token 和样式契约内调整外观

Presenter 接收快照,不获得可修改的宿主 store。即使动作被列为可用,调用时仍会检查当前可写状态和参数。错误边界与默认渲染可在受支持区域隔离普通贡献错误,但不是防范恶意代码的安全沙箱。目标和组合规则见桌面 UI 插件。

支持的协议/生命周期边界、弃用规则及冻结产物回归见插件兼容性与升级。

插件是以用户权限运行的受信代码。manifest 权限和服务声明描述契约,不会隔离进程与机器。工具权限检查和文件系统进程约束分别保护对应的宿主执行路径,不会把任意插件代码变成可安全运行的不受信代码。

禁用或移除提交成功后,无需等待空闲轮次,就会撤销所有仍被引用的旧 generation 中该插件的行为:取消原生执行,阻止所属 MCP 服务重连,并移除后续旧式 hook 和请求转换。已经运行的旧式 hook 保留调用方生命周期;既有会话内容和已完成的外部操作不会被抹除。同 ID 更新则允许活动工作使用原实现完成。已被替代的空闲运行时无需等待下一条用户消息就会释放;未完成的会话工作者和受管进程仍可保留旧实现。拥有激活期后台行为的插件需要容忍工作结束前的新旧重叠。通常保留存储和设置,也不会撤销已经完成的外部操作。安全模式和默认 UI 恢复用于扩展失败后的恢复。Wuu 不审计或认证第三方包,详见安全模型。

兼容性分为多个独立层面:产品 CalVer、包 schema、运行时能力协议、服务主版本和桌面快照版本。应使用与目标宿主匹配的 SDK。wuu plugin dev ./extension.ts 也支持直接加载一个默认导出运行时插件的 TypeScript 文件,无需 manifest、构建、包安装或安装 SDK 依赖。宿主把源文件和内嵌 SDK 快照到私有候选中,需要 Node.js 22.13 或更新版本。单文件仅支持导入 Node 内置模块及 @wuu/plugin-sdk;其他依赖请使用包目录,详见快速开始。构建、包验证、运行时协商测试,以及实际功能或 UI 检查,分别验证这条路径的不同部分。