refactor(plugindev): 模板适配 funccall 调用帧

工具调用/清洗由内核发起,内核标定一块内存帧交给插件(callee),
插件在帧内工作;只有结果超出内核预留预算时才向内核申请扩容块。

- 新增 frameInput/frameOutput/arenaPut:读写调用帧、按需扩容
- tool.invoke:从 frame[0,args_len) 读参数;结果优先写帧结果区,
  放不下才 arena.alloc 扩容并打 sharedRefFlagExpand
- cleaner.invoke:同一帧模型(frame + input_len)
- SharedRef.Flags 语义位与内核对齐(JSON / EXPAND)
- 防漂移测试更新为 frame/args_len/result_ref
This commit is contained in:
JianFeeeee
2026-09-10 18:54:40 +08:00
parent 71e3325439
commit 5175e7d6e0
2 changed files with 127 additions and 23 deletions

View File

@ -147,6 +147,20 @@ func TestProcTemplate_HandlesAllKernelCalls(t *testing.T) {
}
}
// 工具调用的 payload 必须走内核标定的**调用帧**funccall 模型)。
//
// 共享内存是内核内部实现(插件作者只看到普通 map但模板必须在传输层
// 正确读写 frame / args_len / result_ref。漏接线的后果很隐蔽参数被静默
// 丢弃、结果只走内联,性能退化而不报错。
func TestProcTemplate_ToolInvokeUsesSharedRef(t *testing.T) {
src := loadProcTemplate(t)
for _, want := range []string{"frame", "args_len", "result_ref"} {
if !strings.Contains(src, want) {
t.Errorf("模板的 tool.invoke 必须处理 %qpayload 走内核标定的调用帧)", want)
}
}
}
// 模板必须通过 arena.alloc / arena.free 向内核申请与归还共享内存。
//
// 共享内存是内核独占管理的**内部实现**:插件不能自己维护分配游标。

View File

@ -112,6 +112,12 @@ func (r SharedRef) Slice(data []byte) []byte {
return data[r.Offset : r.Offset+r.Length]
}
// SharedRef.Flags 语义位(须与内核 internal/plugin/proc/arena.go 一致)。
const (
sharedRefFlagJSON = 1 << 0 // 载荷是 JSON
sharedRefFlagExpand = 1 << 1 // 引用指向插件申请的扩容块
)
// region 是内核传入的统一共享区域 mmaphandshake 时设置)。
var region []byte
@ -144,6 +150,62 @@ func arenaFree(ref SharedRef) {
callCoreVoid("arena.free", map[string]interface{}{"ref": ref})
}
// ---- 调用帧funccall 模型)辅助 ----
//
// 工具调用/清洗由内核发起:内核标定一块内存帧交给插件,插件在帧内工作,
// 只有结果超出内核预留的预算时才向内核申请扩容块。
// frameInput 返回帧内的输入段(内核写入的参数/输入文本)。
func frameInput(frame SharedRef, inputLen uint32) []byte {
if frame.IsZero() || int(inputLen) > len(frame.Slice(region)) {
return nil
}
return frame.Slice(region)[:inputLen]
}
// frameOutput 尝试把 payload 写进帧的结果区(帧内 [inputLen, frame.Length))。
// 放不下时返回错误,由调用方决定是否申请扩容块。
func frameOutput(frame SharedRef, inputLen uint32, payload []byte, jsonFlag bool) (SharedRef, error) {
if frame.IsZero() {
return SharedRef{}, fmt.Errorf("无调用帧")
}
area := frame.Slice(region)
start := int(inputLen)
if start > len(area) || len(payload) > len(area)-start {
return SharedRef{}, fmt.Errorf("帧内空间不足(需 %d剩 %d", len(payload), len(area)-start)
}
copy(region[frame.Offset+uint32(start):], payload)
ref := SharedRef{
Offset: frame.Offset + uint32(start),
Length: uint32(len(payload)),
Generation: frame.Generation,
}
if jsonFlag {
ref.Flags |= sharedRefFlagJSON
}
return ref, nil
}
// arenaPut 申请一块扩容块并写入 payload引用上打 sharedRefFlagExpand
// 告知内核该块需单独归还(插件只申请,回收由内核做)。
func arenaPut(payload []byte, jsonFlag bool) (SharedRef, error) {
ref, err := arenaAlloc(uint32(len(payload)))
if err != nil {
return SharedRef{}, err
}
if len(payload) > int(ref.Length) {
arenaFree(ref)
return SharedRef{}, fmt.Errorf("扩容块容量不足(需 %d得 %d", len(payload), ref.Length)
}
copy(region[ref.Offset:ref.Offset+uint32(len(payload))], payload)
ref.Length = uint32(len(payload))
ref.Flags |= sharedRefFlagExpand
if jsonFlag {
ref.Flags |= sharedRefFlagJSON
}
return ref, nil
}
// inlinePayloadLimit 是走内联 JSON 的上限。
//
// 小 payload 走内联省两次 RPC申请 + 归还);大 payload 走共享内存,
@ -1082,10 +1144,27 @@ func handleKernelRequest(req *rpcRequest) {
case "tool.invoke":
var p struct {
Name string `json:"name"`
Args map[string]interface{} `json:"args"`
Name string `json:"name"`
Args map[string]interface{} `json:"args"`
Frame SharedRef `json:"frame"`
ArgsLen uint32 `json:"args_len"`
}
json.Unmarshal(req.Params, &p)
// 参数:内核标定帧的前段。只有直连 RPC 的调用方(无帧)才走
// 内联 Args——生产路径永远走帧。
args := p.Args
if !p.Frame.IsZero() {
if blob := frameInput(p.Frame, p.ArgsLen); len(blob) > 0 {
var decoded map[string]interface{}
if err := json.Unmarshal(blob, &decoded); err != nil {
respondErr(req.ID, fmt.Errorf("解析共享参数: %w", err))
return
}
args = decoded
}
}
handlerMu.RLock()
h, ok := toolHandlers[p.Name]
handlerMu.RUnlock()
@ -1093,20 +1172,34 @@ func handleKernelRequest(req *rpcRequest) {
respondErr(req.ID, fmt.Errorf("未注册的工具: %s", p.Name))
return
}
res, err := h(p.Args)
res, err := h(args)
if err != nil {
respondErr(req.ID, err)
return
}
respond(req.ID, map[string]interface{}{"result": res})
blob, err := json.Marshal(res)
if err != nil {
respondErr(req.ID, fmt.Errorf("序列化结果: %w", err))
return
}
// 结果优先写进内核标定的帧;放不下才申请扩容块。
outRef, err := frameOutput(p.Frame, p.ArgsLen, blob, true)
if err != nil {
outRef, err = arenaPut(blob, true)
if err != nil {
respondErr(req.ID, fmt.Errorf("结果扩容失败: %w", err))
return
}
}
respond(req.ID, map[string]interface{}{"result_ref": outRef})
case "cleaner.invoke":
var p struct {
Scope string `json:"scope"`
Name string `json:"name"`
Text string `json:"text"`
TextRef SharedRef `json:"text_ref"`
RespRef SharedRef `json:"resp_ref"`
Scope string `json:"scope"`
Name string `json:"name"`
Frame SharedRef `json:"frame"`
InputLen uint32 `json:"input_len"`
}
if err := json.Unmarshal(req.Params, &p); err != nil {
respondErr(req.ID, fmt.Errorf("解析 Cleaner 参数: %w", err))
@ -1127,23 +1220,20 @@ func handleKernelRequest(req *rpcRequest) {
respondErr(req.ID, fmt.Errorf("%s %s 未注册 Cleaner", p.Scope, p.Name))
return
}
// 读输入:优先内核写入的共享槽,否则内联。
input := p.Text
if !p.TextRef.IsZero() {
input = string(p.TextRef.Slice(region))
}
input := string(frameInput(p.Frame, p.InputLen))
output := cleaner(input)
// 结果:内核预分配了响应槽且放得下就写槽,否则内联
// 插件不申请任何槽(“谁分配谁归还”全部在内核侧)。
if !p.RespRef.IsZero() && len(output) <= int(p.RespRef.Length) {
copy(region[p.RespRef.Offset:p.RespRef.Offset+uint32(len(output))], output)
outRef := p.RespRef
outRef.Length = uint32(len(output))
respond(req.ID, map[string]interface{}{"text_ref": outRef})
return
// 结果优先写进内核标定的帧;放不下才申请扩容块
outRef, err := frameOutput(p.Frame, p.InputLen, []byte(output), false)
if err != nil {
outRef, err = arenaPut([]byte(output), false)
if err != nil {
respondErr(req.ID, fmt.Errorf("结果扩容失败: %w", err))
return
}
}
respond(req.ID, map[string]interface{}{"text": output})
respond(req.ID, map[string]interface{}{"text_ref": outRef})
case "stage.invoke":
handleStageInvoke(req)