☰
Kun 回合级技能激活与 PPT 工作流续接强化:从 `load_skill` 到受管执行边界的完整加固
2026/10/12 2:07:40 网站建设 项目流程
  • 人工智能
  • AI Agent
  • 自主智能体
  • 桌面应用
  • MCP Clients

【免费下载链接】Kun

Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.

项目地址:https://gitcode.com/gh_mirrors/de/Kun
点击查看免费下载

本文以openspec/changes/harden-ppt-master-continuation/变更提案为主体,结合当前仓库的SkillRuntime、load_skill工具提供器、原生 Agent Loop 与 Agent SDK 运行时源码,系统讲解 Kun 如何解决"PPT 工作流确认被取消或延后到后续回合后技能失活"的问题:回合级(turn-scoped)手动技能激活、工具目录确定性刷新、PPT 激活期间的 shell 禁令,以及受管恢复指引与审批令牌边界。读完本文,你将掌握 Kun 技能运行时手动激活机制的数据结构、生命周期清理路径、工具目录冻结器的工作方式,以及这些机制如何共同守住"受管 PPT 执行不落入系统 Python / 通用 shell"的契约。

一、背景:PPT Master 为何会"断线"

Kun 是一个本地优先(local-first)的 AI Agent 工作区,同时支撑桌面 GUI 与 TUI。它的技能(Skill)系统允许模型在提示词中显式提及技能名(如$ppt-master)来触发技能激活,但每个模型步骤都会基于持久化的回合提示词(persisted turn prompt)独立地重新解析技能。

这一设计带来一个具体问题,记载于 proposal.md 的 Why 部分:

  1. 后续回合不再激活技能:当 PPT Master 的原生确认被取消,或用户通过后续聊天回合(如一句"好的")回答时,这个新回合的提示词不再命中ppt-master的触发条件,技能因此没有激活。
  2. load_skill不刷新工具目录:load_skill工具总是被广告给模型,但它原本只返回请求的指令文本,不会改变当前回合的活动技能集合,所以下一个模型步骤依然无法发现或执行受技能门控(skill-gated)的工具。
  3. 模型可能绕过受管运行器:当模型从历史记录里记住了某个 PPT 工具,但分发(dispatch)被拒绝时,通用指引会允许它改用bash之类的 shell 工具,从而绕过受管的 Python 虚拟环境和确认令牌契约——既破坏了确认令牌边界,又产生误导性的工具失败,还可能用不兼容的系统 Python 运行包。

变更提案的"Capabilities"节定义了新能力managed-skill-turn-continuation,其职责是:回合级的手动技能激活、刷新后的技能门控工具可用性,以及受管 PPT Master 工作流的安全恢复行为。

二、变更总览:四条核心承诺

proposal.md 的 What Changes 列出了四条承诺,也是全篇实现的验收标准:

承诺说明
显式激活一次明确的load_skill调用,使该技能在同一回合的剩余时间内保持激活,包括后续模型步骤的工具发现与执行
回合作用域手动激活只存活于当前回合;回合结束时清除;技能不会跨无关回合粘滞
有界恢复路径受管 PPT Master 工具不可用时,引导模型"只激活 PPT Master 一次",并禁止用 shell 替代
令牌唯一授权ppt_master_run的唯一授权来源是原生确认令牌;通用用户输入或散文式口头批准都不够

同时明确影响范围:Kun 技能运行时与load_skill工具提供器、原生 Agent Loop 回合上下文解析与回合生命周期清理、PPT Master 工具分发指引与受管工作流策略、相关集成测试;不改变HTTP/SSE schema、持久化线程格式或 PPT Master 包格式。

三、回合级手动激活的实现:SkillRuntime内部结构

3.1 有界的手动激活存储

设计决策 1(见 design.md 的 Decisions 部分)要求:手动激活按threadId + turnId组合键存储在SkillRuntime内部,而不是放在渲染器状态或线程元数据里(后者会扩大协议并带来跨进程同步问题)。

对应源码位于 skill-runtime-engine.ts:

/** * Explicit `load_skill` activations live only for one thread/turn. The map is * process-local and bounded so an abnormal turn that never reaches cleanup * cannot grow runtime state without limit. */ private readonly manualSkillIdsByTurn = new Map<string, Set<string>>()

键由 skill-runtime-support.ts 中的skillTurnKey(threadId, turnId)生成;存储上限由常量MAX_MANUAL_SKILL_TURN_ACTIVATIONS = 512(定义于 skill-runtime-contracts.ts)约束,作为异常进程终止时的纵深防御——即使某个回合从未走到清理逻辑,运行时的进程内状态也不会无限增长。

3.2loadSkillById:校验通过后才记录激活

load_skill工具的执行会调用SkillRuntime.loadSkillById。在 skill-runtime-engine.ts 中可以看到完整流程:

  1. 先做可见性校验与阻止校验:通过filterSkills(await this.skillsForWorkspace(workspace), allowedIds, blockedIds)拿到该工作区可见、且未被 allow-list/deny-list 过滤的技能集合;找不到技能时返回{ error: 'unknown skill id ...' }(不抛异常,作为普通工具结果返回给模型)。
  2. 按instructionBudgetBytes(默认 24,000 字节,见 skill-runtime-contracts.ts)对指令做字节预算截断。
  3. 只有校验全部通过,且调用方提供了非空的threadId与turnId时,才调用rememberManualActivation(turn.threadId, turn.turnId, skill.id)记录激活。

这正好对应 spec 中的"Blocked skill cannot be manually activated"场景:被执行配置(execution profile)阻止的技能,加载失败,后续模型步骤也不会广告它的工具。

rememberManualActivation(skill-runtime-engine.ts)会把技能 id 加入对应回合的Set,并在超过 512 上限时淘汰最老的键。

3.3resolveTurn:手动激活与提示词触发合并为确定性结果

SkillRuntime.resolveTurn(skill-runtime-engine.ts)是每次模型步骤解析技能的唯一入口。它先把提示词触发的匹配收集起来(matchSkills),然后遍历manualSkillIds(input.threadId, input.turnId),把尚未匹配上的手动激活技能追加进去:

matches.push({ skill, skillId: skill.id, reason: 'load_skill', score: 1_100 + skill.priority })

手动激活的评分(1,100)高于提示词显式提及(1,000)、命令触发(900)、正则模式(500)与文件类型(300),随后所有匹配按分数降序、同分按 id 字典序排序,再取前activeLimit(默认 3)个作为活动技能。这样一来,原生运行路径与 Agent SDK 桥共享同一个SkillRuntime,就都能消费同一份确定性解析结果——这正是 design.md 决策 1 强调的"两个运行时路径消费一个确定性结果"。

四、激活何时生效:下一个模型步骤,而非当前请求

设计决策 2 明确指出:当前模型请求的工具 schema 是不可变的,因此手动激活不会在load_skill调用期间立刻生效,而是在下一个模型步骤的工具列表重建时可见。

这一机制由两层配合完成:

第一层:回合上下文解析器传入身份。原生路径的 turn-context-resolver.ts 在调用skillRuntime.resolveTurn时显式传入threadId与turnId;Agent SDK 路径的loadTurnContext/执行回退同样传递这两个字段(见 design.md 决策 3)。

第二层:工具目录冻结器支持显式策略作用域。turn-tool-catalog.ts 中的TurnToolCatalogFreezer原本让一个回合内保持单一稳定工具目录,防止 MCP/提供器刷新中途篡改已广告的 schema 集合;但它在resolve(threadId, turnId, liveTools, scopeKey)中允许按 scopeKey 切换目录:

// A turn normally has one stable catalog. Deliberate policy transitions // such as `load_skill` can opt into a new scope so newly activated managed // tools remain usable without allowing an MCP/provider refresh to mutate // an already-advertised schema set. const key = JSON.stringify([threadId, turnId, scopeKey])

turn-tool-catalog.test.ts 中的测试用例 "adopts a new catalog for an explicit policy scope within the same turn" 验证了这一行为:先以'skills:base'作用域解析出['load_skill'],再以'skills:ppt-master'作用域解析出['load_skill', 'example_skill_run'],且判定为无漂移(pendingDrift.kind === 'none')。这保证了load_skill激活 PPT Master 后,ppt_master_confirm_design、ppt_master_read_guide、ppt_master_run三个门控工具能在下一个模型步骤被广告(对应 spec 的 "Follow-up turn loads PPT Master" 场景)。

五、生命周期清理:两个运行时路径的终态回收

设计决策 3 要求"原生AgentLoop清理与 Agent SDKfinishTurn调用共享的清理方法"。源码中两处实现完全对应:

  • 原生路径:agent-loop-turn-lifecycle.ts 在回合的finally块中,与modelRouting.clear、toolStormBreakers.delete等清理并列调用:
if (typeof this.opts.skillRuntime?.clearTurnActivation === 'function') { this.opts.skillRuntime.clearTurnActivation(threadId, turnId) }
  • Agent SDK 路径:agent-sdk-runtime-factory-lifecycle.ts 在finishTurn中做同样的调用。

clearTurnActivation本体在 skill-runtime-engine.ts,只是从 Map 中删除对应键:

clearTurnActivation(threadId: string, turnId: string): void { this.manualSkillIdsByTurn.delete(skillTurnKey(threadId, turnId)) }

Agent SDK 侧有专门测试验证这一契约:agent-sdk-runtime-factory-capability-skills.test.ts 的 "clears turn-scoped skill activation when an SDK turn finishes",断言clearTurnActivation以('thread_1', 'turn_1')被调用。

这支撑了 spec 的两个场景:

  • Unrelated turn remains inactive:一个回合手动激活 PPT Master,另一个未提及也未加载它的回合不会广告 PPT 工具——因为激活键含 threadId + turnId。
  • Terminal cleanup:回合完成、失败或被中止时,进程内手动激活状态被移除。

六、PPT 受管执行的恢复策略:禁止 shell 替代

6.1 能力注册表层面的 shell 禁令

设计决策 4 的关键表述是:"PPT shell 禁令在能力注册表(capability registry)强制执行"——当ppt-master处于活动状态时,bash和background_shell既不会被广告,也不可被解析,不依赖模型自觉遵守。这一策略被刻意设计为针对性的(targeted),而不是全局 shell 禁令或修改磁盘上的旧技能 manifest(后者需要重装/迁移行为,可能波及其他技能)。

spec 中对应的验收场景是 "Active PPT workflow tool catalog":ppt-master对某模型步骤激活时,只要受管 PPT 工具仍可用,bash与background_shell既不广告也不可执行。

值得说明当前仓库的落地状态:在 runtime-factory-config.ts 中,skillsConfigForRuntime将'ppt-master'无条件并入disabledIds,注释明确写着:

/** * PPT Master was a host-managed Skill. Keep an old package on disk inert after * the first-class PPT agent replaced it, without deleting user data. */

也就是说,在本文对应的变更落地之后,PPT Master 作为"host-managed Skill"已被 Kun 的 first-class PPT Agent(ppt_agent工具及其本地工具族)取代,旧包保留在磁盘上但不参与激活(project-skill-runtime.test.ts 验证了disabledIds: ['ppt-master']之后loadSkillById('ppt-master')返回unknown skill id)。这一机制的价值正在于:回合级激活、门控工具目录刷新、受管执行边界这些运行时能力是通用的,不绑定具体某个 PPT 包。

6.2 受管工具族的执行边界

虽然ppt_master_run这类工具名属于 PPT Master 时代(spec 中作为验收场景描述),当前仓库的 PPT 受管工具族(ppt_agent子代理与其本地工具)同样贯彻"非 shell、受管执行"原则:

  • ppt-agent-local-tools.ts 的注释明确:"Unlike a shell call, neither tool can execute an arbitrary command or escape the active workspace for input/output files."(它们只暴露捆绑的参考 Markdown 与一个固定的离线 WASM 导出器)。
  • 预览渲染走toolchain/scripts/export_images.py(ppt-agent-local-tools.ts),但通过execFile以固定脚本路径执行、带 5 分钟超时与 abortSignal 联动,而不是让模型自由拼装 shell 命令。
  • 导出走捆绑的离线 WASM 导出器scripts/local-export/export-pptd.mjs+pptd_wasm_bg.wasm(ppt-agent-export-tool.ts),同样不经过任意 shell。
  • 沙箱策略 sandbox-policy.ts 将bash与background_shell列为需要工作区审批的命令工具,与受管 PPT 边界的思路一脉相承。

6.3 拒绝调用的恢复指引

设计决策 5 规定:当某个ppt_master_*工具的分发被拒绝时,工具结果应引导模型:

  1. 对ppt-master调用一次load_skill;
  2. 只在下一个工具目录刷新之后重试;
  3. 若受管工具仍不可用则停止;
  4. 明确禁止bash与直接 Python 执行。

这对应 spec 的 "Rejected PPT tool call" 场景。恢复路径之所以"有界"(bounded),是因为它不会退化为直接脚本执行——load_skill成功后工具目录在下一模型步骤被刷新,模型拿到的是受管工具 schema,而不是自由 shell。

七、审批令牌:唯一且不可伪造的授权来源

spec 的最后一个需求 "Native PPT confirmation remains authoritative" 与 design.md 决策 5 的结尾一致:"approval token remains unforgeable because onlyppt_master_confirm_designrecords it"——只有原生确认工具才签发令牌。

两个验收场景:

  • Generic confirmation is insufficient:用户回答通用request_user_input提示,或发送批准性质的散文(如"好的"),而没有原生 PPT 审批令牌时,ppt_master_run拒绝创建或修改演示文稿文件。这正是第一章描述的断线场景的核心风险点:会话延续到后续回合并不构成授权。
  • Managed execution uses the installed environment:被有效批准的ppt_master_run动作执行时,调用的是受管 PPT Master 虚拟环境的 Python,而不是系统的python3。

结合当前仓库的 PPT Agent 实现可以看到同类约束的延续:ppt_agent子代理执行时携带pptWorkflowScope(host-minted managed PPT authority,见 agent-loop-options.ts),本地 PPT 工具只有在context.pptWorkflowScope !== undefined时才被广告(ppt-agent-local-tools.ts),并且assertPptWorkflowBinding会在每次执行时校验工作流绑定(workflowId/projectDir),从物理路径层面阻止越界读写(ppt-agent-physical-path.ts)。这与"令牌是唯一授权"的设计一脉相承:无论是旧的 PPT Master 还是新的 PPT Agent,受管执行边界都不能被散文式批准绕过。

八、回归测试覆盖

tasks.md 要求三组回归覆盖,仓库中均有对应落点:

任务仓库证据
3.1 SkillRuntime 与load_skill:同回合激活、阻止技能、回合隔离、清理project-skill-runtime.test.ts(disabledIds 阻止load_skill、工作区可见目录);agent-sdk-runtime-factory-capability-skills.test.ts(SDK finishTurn 清理);agent-sdk-runtime-factory-supervision-approval.test.ts(另一条清理路径)
3.2 能力/分发测试:PPT shell 阻止与受管恢复指引turn-tool-catalog.test.ts(同回合显式策略作用域目录刷新);sandbox-policy.ts 的命令工具集合定义
3.3 运行时覆盖:非触发回合先load_skill再在下一模型步骤执行门控工具与 3.1/3.2 的用例共同构成该路径的验证(回合内先load_skill→ scopeKey 切换 → 下一模型步骤工具目录包含技能门控工具)

验证任务(tasks.md 第 4 节)要求运行 Kun 技能、能力、loop、Agent SDK 与 PPT Master 测试,并执行npm run build:kun、根级类型检查与 diff 卫生检查——这些属于 CI 门禁动作,可参见仓库根目录的 package.json 中对应的脚本约定。

九、部署、迁移与风险权衡

9.1 部署与回滚

design.md 的 Migration Plan 强调:无需持久化数据迁移。部署时把运行时与渲染器变更一起发布,重启 Kun 使新工具策略生效,然后开启一个全新的 PPT 生成回合即可。回滚纯属代码级操作,现有线程与已安装的 PPT 包保持可读。

9.2 风险与缓解

design.md 的 Risks/Trade-offs 节记录了四个风险及其缓解:

  • load_skill后工具前缀变化→ 变更只发生在下一个模型步骤边界,且添加的是确定性 schema;遥测已按活动技能处理目录身份。
  • 共享SkillRuntime状态在子回合与主回合间泄漏→ 键同时含 thread 与 turn id;记录前先做阻止校验;清理是终态的;存储有界。
  • PPT 工作期间屏蔽 shell 可能移除旧提示词依赖的逃生通道→ 受管技能契约本就禁止 shell 替代;保留读/写/编辑工具与固定的ppt_master_run动作面。
  • 运行时崩溃留下进程内激活→ 有界存储 + 进程整体重置使其既非持久化也不具授权性;PPT 变更仍要求同回合的审批令牌。

Non-Goals 同样值得重申(design.md Goals/Non-Goals):不跨回合/线程/重启/无关消息持久化活动技能;不把散文或通用request_user_input回答当作 PPT 授权;不改变 PPT Master 脚本、受管虚拟环境或 HTTP/SSE 契约;不做全局技能粘滞或全局收窄 shell 访问。

十、结语:从一次断线修复到通用运行时契约

"强化 PPT Master 续接"表面上是一次针对 PPT 工作流的修复,其真正落地的是一组通用的运行时契约:

  1. load_skill从"读指令"升级为"回合级激活",且激活状态按threadId + turnId有界隔离(MAX_MANUAL_SKILL_TURN_ACTIVATIONS = 512);
  2. 工具目录在显式策略作用域下确定性刷新,TurnToolCatalogFreezer保证load_skill触发的目录切换不引入 MCP/提供器漂移;
  3. 原生 Agent Loop 与 Agent SDK 共享同一个清理方法,回合终态(完成/失败/中止)必然回收激活状态;
  4. 受管执行的授权边界永不退化:PPT 变更只认原生审批令牌,shell 与系统 Python 在任何情况下都不是合法的替代路径。

对开发者而言,这套机制的可借鉴之处在于:当 Agent 工具的"可用性"依赖回合内动态状态时,必须同时设计好状态的作用域(thread + turn)、生命周期清理(终态回收)、目录可见性(下一个模型步骤生效)、以及策略执行位置(能力注册层而非模型自觉)。在 Kun 仓库中,这一整套机制的源码落点集中在 skill-runtime-engine.ts、skill-tool-provider.ts、turn-tool-catalog.ts 与 agent-loop-turn-lifecycle.ts,是理解 Kun 技能与工具治理模型的良好切入点。

  • 人工智能
  • AI Agent
  • 自主智能体
  • 桌面应用
  • MCP Clients

【免费下载链接】Kun

Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.

项目地址:https://gitcode.com/gh_mirrors/de/Kun
点击查看免费下载
上一篇:三步解锁Wand游戏修改器专业版:本地增强工具完全指南
下一篇:WarcraftHelper终极指南:让经典魔兽争霸III在现代电脑上流畅运行

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询