Skip to content

fix(agent-core-v2): fold UserPromptSubmit hook output into the prompt message - #4065

Closed
sailist wants to merge 3 commits into
MoonshotAI:mainfrom
sailist:feat-273-09-28-hook-result-prompt-part
Closed

sailist wants to merge 3 commits into
MoonshotAI:mainfrom
sailist:feat-273-09-28-hook-result-prompt-part

Conversation

@sailist

@sailist sailist commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Requirement or Bug

Resolve #4032

Bug Reproduction Steps

See linked issue(2.1.0/2.1.1 可复现:配置 UserPromptSubmit 钩子的会话里,轮次整体错位一位,最后一条消息看起来永远没有回复,刷新后恢复)。

Root Cause

引擎在 UserPromptSubmit 钩子返回文本后,把它作为一条独立的 user 角色消息(origin: hook_result)追加到真实用户消息之前,上下文序列变成 [hook 消息][用户消息][回复],user 单元翻倍。渲染层的轮次配对依赖「一个 user 单元配一段 assistant 输出」的不变量,任何不消费 origin 的配对路径都会因多出的 user 单元而整体错位一位。排查确认数据层(wire、fold、live store、投影)完整且对齐,错位只来自不消费 hook origin 的渲染路径。

这是根本修复而非绕过:让「一条 prompt 一条 user 消息」在结构上成立——钩子注入文本以带 meta 标记的 content part 并入 user prompt 消息,不再产生第二条 user 消息,轮次配对在结构上不存在错位可能,不再依赖每条渲染路径自觉遵守 origin 约定。

Code Changes

三个提交,按主题分五块:

1. content part 增加 meta 能力(agent-core-v2 + transcript)

// packages/agent-core-v2/src/human/llm/message.ts
interface TextPart {
  type: 'text';
  text: string;
  meta?: { source?: string; contentType?: string };  // 新增,仅 text 类型
}

wire schema(historySchema.ts)以 looseObject 形态接受 meta(前向兼容);packages/transcript 的 HistoryContentPart text 变体同步(browser-safe 纯类型)。meta 不下发 LLM provider(adapter 只读 type/text)。

2. 钩子注入合并进 prompt 消息(gate 单次落盘)

 runPromptGate
   onBeforeSubmitPrompt
-    hook 服务: context.append(独立 hook_result user 消息)   # user 单元翻倍
+    hook 服务: ctx.hookParts.push(meta 标记的 text part)
   组装最终消息
-    content = 原始 prompt
+    content = [...hook parts, ...原始 prompt]               # 一次 append 落盘
  • PromptSubmitContext 增加 hookParts 可变插槽(钩子的唯一输出通道,与既有 block 字段同模式);hook part 固定形状 { type:'text', text:'<hook_result …>…</hook_result>', meta:{ contentType:'text/xml', source:'user prompt submit hook' } },多钩子按返回顺序各成一个 part。
  • 附带修复:stdout 为 JSON 且无 message 字段(如 {"continue":true})不再注入任何内容(此前原文注入,每轮都有噪声);纯文本 stdout 仍兜底注入。

3. Turn 层记录保留完整数据(vis/replay 可见)

turn.prompt wire 记录与 TurnStarted.prompt 均含 hook 内容。

4. 展示层可见渲染

  • transcript 冷 fold:hook part 映射为 hook marker(载荷与 live hook.result marker 同形),轮 prompt 提取/steer 匹配仍按剥离后内容加工,不产生 0 步轮组;
  • transcript live 投影:轮标题剥掉前导 <hook_result> wrapper(注入已由 live hook marker 承载);
  • TUI replay:meta hook part 渲染为与老 wire 相同的 hook 条目,prompt 文本干净;
  • 存量 wire 的独立 hook 消息按原逻辑继续处理,两制并存。

5. skill bundled 块统一 meta 化,消除位置切片

skill 块与 hook part 一样在构造时携带 meta: { source: 'skill activation', activationId }(fallback 补标时按 origin 顺序对应一并回填 activationId,新老 wire 的 skill part 均自描述)。原先全部 content.slice(skillActivations.length) 位置切片点——轮 prompt 文本、steer 合并与 echo、会话标题与 fork 标题、transcript fold、TUI replay——统一改为「先 annotate 再按 meta 过滤」:annotate 是集中的向后兼容层,老 wire 里无标记的前 N 个块(每个 bundled activation 一个)在读取时补标,之后与有标记的新 wire 走同一条过滤路径。prompt 内容不再依赖块位置,loop 也无需识别 hook part。

Behavior Changes and Affected Users

Behavior Before After Who relies on the old behavior Escape hatch
UserPromptSubmit 注入文本的上下文形态 独立 user 消息(origin: hook_result),位于真实 prompt 之前 并入 prompt 消息 content[0],带 meta 标记 无合理依赖(旧形态正是错位根因) 无(本 PR 即修复)
Hook stdout 为无 message 的 JSON(如 {"continue":true}) 原文注入上下文(噪声) 不注入任何内容 依赖裸 JSON stdout 注入向模型传数据的 hook 脚本 改用 {"message": "..."}
Hook stdout 为纯文本(非 JSON) 兜底注入 仍注入(现为 prompt content part) — —
turn.prompt 记录 / turn.started 事件的 prompt 不含 hook 内容 含完整 hook 内容(持久化、进上下文) 消费 turn.started.prompt 的客户端:ACP 客户端(如 Zed)、desktop、klient SDK 用户,将看到含 hook 文本的 prompt kap-server live 投影已为轮标题剥 wrapper;各客户端可自行决定展示
skill bundled 块的 wire 形态 无标记 text part,靠 origin.skillActivations.length 位置识别 构造时携带 meta: { source: 'skill activation', activationId }(含 steer 合并消息;老 wire 读取时由 fallback 回填) 按「数量 + 位置」解析 skill 块的外部消费者(如直接读 wire 的脚本) 老 wire 由读取侧集中 fallback 补标,行为不变
会话标题 / fork 标题提取 仅排除 skill 块(hook 文本可能混入标题) 同时排除 skill 块与 hook part — —
LLM 可见内容 hook 文本在独立消息中,位于 prompt 前 hook 文本在同一 user 消息 content[0],位于原始内容前 无(注入文本总量不变,位置语义不变) —
存量会话(含独立 hook_result 消息、无 meta 的 skill 块的旧 wire) 按原逻辑恢复/渲染 不变(两制并存,读取侧集中 fallback) 老会话 resume、搜索索引 无需
resume/replay 后 hook 注入的展示 老 wire 渲染为独立 hook 条目;新 wire 此前不可见 新 wire 在 transcript 显示为 hook marker、TUI 显示为 hook 条目 — —

受影响模块与测试覆盖:

  • 合并与记录(agent-core-v2 gate / 钩子服务):promptService.test.ts 的 gate 用例钉住单条 user 消息、content[0] meta 形状、turn.prompt/TurnStarted 均含 hook 内容、skill 块不进 prompt 文本;
  • 注入协议(runHook / userPrompt):runner.test.ts 钉住无 message JSON(含 {"continue":true})不注入、纯文本 stdout 产出带约定 meta 的 part;
  • skill meta 与 fallback(origin.ts / steer 合并):promptService.test.ts 的 steer/queue 用例与 machine.test.ts 钉住标记与未标记块两种形态的合并/echo 结果;
  • transcript fold/steer(transcript、kap-server):layers.test.ts 钉住 hook marker 可见、bundled 标记/未标记两形态展开一致、steer content-key 匹配不受影响;
  • TUI replay(apps/kimi-code):message-replay.test.ts 与 kimi-tui-message-flow.test.ts 钉住合并 wire 的 hook 条目可见、marked/unmarked skill 块渲染一致、prompt 文本干净;
  • 全量测试(m0-dev):898 文件 / 16655 用例全部通过;测试数净增为 0(均为扩展既有用例)。

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.(按仓库规则净增为 0:扩展既有用例覆盖全部新行为)
  • The behavior-change table above is complete, and every removed behavior or flipped default is named in the changeset and either has an escape hatch or was explicitly approved by a maintainer in this PR.(被移除的裸 JSON 注入行为及退路见上表;changeset 聚焦用户可感的错位修复)
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.(state-manifest.d.ts 已随类型同步重新生成)

…tent parts

UserPromptSubmit hook messages now merge into the user prompt message as
meta-marked text content parts (contentType text/xml, source 'user prompt
submit hook') at content position 0 instead of a standalone hook_result
user message, so one prompt always yields exactly one user message and
turn pairing can no longer shift. Structured hook JSON without a message
field (e.g. {"continue":true}) no longer injects anything; plain-text
stdout still falls through. turn.prompt records, TurnStarted prompts,
fold turn openings, and steer content keys all strip hook parts; existing
wires with standalone hook_result messages keep their current behavior.
… and render them in clients

Per the revised contract, turn.prompt records and TurnStarted prompts
keep hook content (the data is persisted and enters the context), and
the presentation layers own how it shows up: the cold transcript fold
maps hook parts to visible hook markers, the live transcript projection
strips hook_result wrappers from turn headers (the hook marker carries
the injection), and the TUI replay renders meta hook parts as the same
hook entries old wires produced, keeping prompt text clean. vis sees
the full data from the wire unchanged.
@changeset-bot

changeset-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a743499

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@a743499
npx https://pkg.pr.new/@moonshot-ai/kimi-code@a743499

commit: a743499

@sailist
sailist force-pushed the feat-273-09-28-hook-result-prompt-part branch from 095878c to 612a4c4 Compare September 28, 2026 11:38
…onal slicing

Bundled skill blocks are now constructed with meta { source: 'skill
activation' }, matching the hook-part convention. Every former
content.slice(skillActivations.length) site — turn prompt text, steer
merge/echo, session and fork titles, transcript fold, TUI replay — now
annotates then filters by meta, so prompt content no longer depends on
block positions and the loop needs no hook-part knowledge. A shared
annotate step keeps legacy wires working: leading unmarked blocks (one
per bundled activation) are marked on read before filtering.
@sailist
sailist force-pushed the feat-273-09-28-hook-result-prompt-part branch from 612a4c4 to a743499 Compare September 28, 2026 11:42
@RealKai42

Copy link
Copy Markdown
Collaborator

@codex

@sailist

sailist commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded: resubmitting under a different account.

@sailist sailist closed this Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] UserPromptSubmit 钩子注入 message 导致 Web UI 消息时序错乱(第 n 条回复被渲染到第 n-1 条回复的位置)

2 participants