From 2a4315cfe4467a77ed425956192657a864281c9b Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 10 Sep 2026 21:47:23 +0800 Subject: [PATCH] =?UTF-8?q?docs(plan):=20=E4=BF=AE=E6=AD=A3=20=C2=A713.13?= =?UTF-8?q?=20=E5=88=A4=E6=8D=AE=E2=80=94=E2=80=94=E6=8C=89=E3=80=8C?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E8=83=BD=E5=90=A6=E5=B0=B1=E5=9C=B0=E6=94=B9?= =?UTF-8?q?=E5=86=99=E3=80=8D=E8=80=8C=E9=9D=9E=20payload=20=E5=A4=A7?= =?UTF-8?q?=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前把共享内存的理由写浅了(当成“省管道 / 避免大 payload”)。真实目的是 找回 .so 时代的能力:插件回调(Cleaner / Stage / 工具)与内核同进程时可以 直接就地改写参数与结果;多进程化后若靠 RPC 来回发消息,回调就只能 “读一份、回发一份”,丢失就地改写语义。共享内存是把内容放进段里、回调就地 改、只回描述符。 因此判据应是「插件回调要能就地改写的内容有没有留在段里」,不是 payload 大小。 按新判据核实:回调主线已达成—— - Stage:invokeStage 只发 {Stage, Seq},插件就地改写,应答只回 DirtyFields 计数 - Cleaner:发 {Scope,Name,Frame,InputLen},回 TextRef(16B 描述符),内容不随 RPC 走 - tool.invoke:内核标定 Frame,结果 ResultRef,after_toolcall 可在段内再改 未入内存的重新排序,第一条换成 io.setToolBlocks:它直接破坏“结果媒体能被 after_toolcall 就地改写”——插件只能推一份 base64 拷贝过去(ai_image 大图)。 --- plan.md | 51 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/plan.md b/plan.md index f5b4cdf..3c26144 100644 --- a/plan.md +++ b/plan.md @@ -1368,42 +1368,51 @@ SSE Last-Event-ID → 超时 → api 状态码 → renderAll 增量 → XSS 消 - [ ] 图查询返回媒体节点 - [ ] git commit -m "feat(l3): native multimodal nodes/edges" -### 13.13 剩余内联大 payload 路径(「全量数据交互入共享内存」的尾巴) +### 13.13 剩余内联 payload 路径(「全量数据交互入共享内存」的尾巴) **目标**:所有**数据面**交换都走共享内存,RPC 只传偏移描述符(SharedRef)。 共享内存是跨进程的内部实现,不对插件开发者暴露(SDK 公开 API 仍是 string / map / slice)。 -**已经走共享内存的**: +**为什么必须入共享内存(不能只图省管道)**: + +.so 方案下插件回调(Cleaner / Stage / 工具)与内核同进程,可以直接就地 +改写参数与结果;多进程化后如果靠 RPC 把消息来回发,回调就只能“读一份、 +回发一份”,丢失就地改写语义。共享内存就是为了把 .so 时代的能力找回来: +内核把内容放进段里 → 插件回调**就地改** → 只回一个描述符。 + +**因此判据不是「payload 大不大」,而是「插件回调要能就地改写的内容有没 +留在段里」**。控制面小报文(plugin.init.Config、tool.register 的 def、 +settings.*、lifecycle.*、arena.alloc/free 自身)不属于此列:它们不被任何 +回调改写,搬进段里反而多两次 RPC。 + +**回调就地改写——已达成(实测核实)**: | 通道 | 机制 | | --- | --- | -| StageContext | 区内 segment + `WriteAll/ReadInto` | +| StageContext | 区内 segment;`invokeStage` 只发 `{Stage, Seq}`,插件就地改写,应答只回 `DirtyFields` 计数 | | 事件环 | 区内 segment + eventfd 通知 | -| tool.invoke 参数/结果 | `Frame` / `ResultRef`(§13.3) | -| cleaner.invoke 输入/结果 | `Frame` / `TextRef`(§13.4) | +| Cleaner | `Frame`/`InputLen` 入,`TextRef`(16B 描述符)回,内容不随 RPC 走 | +| tool.invoke 参数/结果 | 内核标定 `Frame`,结果 `ResultRef`;`after_toolcall` 可在段内再改 | | output.invoke 参数 | `Frame`(§13.6) | | io.injectText 系列 | 插件侧 `putInArena` → `text_ref`(§13.5) | -**尚未入内存(按风险排序,都是数据面)**: +**尚未入内存**(按“是否破坏回调语义”排序): -1. **媒体块:`io.injectMedia` / `injectMediaSync` / `injectInterruptMedia` - / `io.setToolBlocks`**——`blocks` 内联在 RPC JSON 里,而 - `ImageURL.URL` / `AudioURL.URL` 对本地生成的图/音频是 **base64 data URL**。 - 本地大图 base64 后可达数 MB,是目前最大的一条内联路径。 - 待做:blocks 序列化后 `putInArena`,传 `blocks_ref`;内核侧读回。 -2. **`doc.insert` / `doc.insertWithMedia`**——文档全文内联。文档可达几十 KB~ - 数 MB。待做:同 1,`doc_ref`。 -3. **`knowledge.add(name, content)`**——知识正文内联,同上。 -4. **反向工具结果(kernel → 插件)**:目前只有正向(插件→内核)有 - `ResultRef`;插件反向调内核读大结果时仍是内联。 - -不需入内存的:控制面小报文(`plugin.init.Config`、`tool.register` 的 def、 -`settings.*`、`lifecycle.*`、`arena.alloc/free` 自身)——它们本身就只有 -几十~几百字节,搬进共享内存反而多两次 RPC。 +1. **媒体块:`io.setToolBlocks`**——工具内的媒体注入仍把 `blocks` 内联在 + RPC JSON 里,而 `ImageURL.URL` / `AudioURL.URL` 对本地生成的图/音频是 + **base64 data URL**(如 ai_image 生成的大图)。这条直接破坏“结果媒体 + 能被 after_toolcall 就地改写”的能力:插件只能推一份拷贝过去。 + 待做:blocks 序列化后 `putInArena`,传 `blocks_ref`,内核读回。 +2. **`io.injectMedia` / `injectMediaSync` / `injectInterruptMedia`**——同上, + 插件主动发起带媒体的一轮对话。 +3. **`doc.insert` / `doc.insertWithMedia`**——文档全文内联,且 `doc` 是可被 + 插件回调改写的内容。 +4. **`knowledge.add(name, content)`**——知识正文内联,同上。 +5. **反向结果**:插件反向调内核读大结果时仍内联(正向已有 `ResultRef`)。 **验证**: -- [ ] 媒体块走共享内存(本地大图注入不再爆管道) +- [ ] 媒体块走共享内存(插件推图不再靠拷贝,且可被回调就地改写) - [ ] 文档/知识正文走共享内存 - [ ] git commit -m "feat(shm): remaining data-plane payloads via shared refs"