Files
HomeAgent/internal/sdk/plugin.go
JianFeeeee d1959cbe80 feat(core): 注入行为的记忆/裁剪标志位落地 + jieba 词库内嵌 + Windows 改走 WSL
配套 SDK 提交:homeagent-sdk ba49dfd(公开 API 纯追加,无签名变更)。
本仓第三方的库镜像同步至该版本,以保证全新 clone 能编译。

## 1. 注入标志位(内核侧)

- 7 条注入路径(排队/中断/同步 × 纯文本/带媒体 + 旧 NoMem 变体)解析并转发
  no_memory / context_policy / cleaner_name;策略在入口**校验**,
  非法值报错而不是静默降级成 none(降级会让调用方以为自己声明的裁剪在生效)。
- 新增 validateContextPolicy(与 tool.register 同一套规则)与 pubSdkInjectOpts。
- input.register 不再手写字段白名单重建 ChannelDef,改为整体传递 + 补 ContextPolicy。
- io 层:applyInjectOpts 把标志位写进事件 payload,仅非零时写
  (零值与旧 payload 逐字节一致,事件订阅方与旧内核都不受影响)。
- ioAdapter / procCore / internal-sdk 别名补齐六个 *Opts 实现。

## 2. 修掉「输入无条件裁剪」这个真缺陷

eventloop 此前对**每条非中断输入**都调 `context.Prune(...)`:破坏性(低相关事件被
归档移出上下文)且无法从调用点看出是谁触发的。改为 pruneOnInput/pruneDeclared:

  优先级:注入点声明(payload.context_policy)> 通道声明(ChannelDef.ContextPolicy)
          > 默认**不裁剪**

查询向量仍取清洗后的内容;新增 cleanInputFor 解析清洗文本,优先级为
注入点声明的 cleaner(cleaner_name)> 按 source 查到的通道 cleaner > 原文,
名字查不到时**记日志再回退**(注入是 fire-and-forget,插件看不到错误,
至少要在内核日志留下「你声明的清洗没生效」的痕迹)。

## 3. jieba 词库内嵌(修「猜 GOMODCACHE → 静默失效」)

原 jiebaDictDir() 去猜 GOMODCACHE/GOPATH/~/go/pkg/mod,部署机上通常没有 Go 模块
缓存 → GetJieba() 返回 nil → 分词/关键词提取/NLP 依存解析(进而 doc→graph 三元组
抽取)/静态词向量 tokenizer **一律静默返回空列表**,只有一行日志。本机看起来正常
只因开发机与生产机重合、恰好有那份缓存。

现在词库随二进制分发:internal/memory/jiebadict/ 5 文件约 11.6MB + go:embed,
按**内容哈希**命名缓存目录落盘(词库升级不复用旧文件),已齐全则跳过写入。
模块缓存降为兜底。homed 体积 32MB。

顺带确认(并有测试佐证):gojieba 的 Tag() 不需要 pos_dict/ 目录——
cppjieba 的 PosTagger 从主词典每行的词性列取 tag。

## 4. homed 放弃 Windows 原生,改走 WSL2

插件体系依赖「继承的 fd」+「统一共享内存区的段内偏移解引用」,Windows 既无 fd
继承语义,其句柄模型也无法表达后者;强行适配等于再维护一套平台专属 ABI
(C ABI 时代三套 ABI 并存曾导致改写型插件在某平台静默失效)。

- cmd/homed/platform_{windows,other}.go:原生 Windows 启动即拒绝并打印 WSL2 指引。
- internal/plugin/proc/shmalloc_windows.go:allocShm 直接返回「请用 WSL2」,
  **不返回半可用的段**(与 shmalloc_other.go 同风格:未支持平台显式报错);
  procEnvForShm 返回 nil。顺手修掉两处长期编译错误
  (cryptorand→rand、h.evData→h.unified.evtData),使 GOOS=windows 至少能编译。
  注:homed 本就编不出 Windows——internal/memory 依赖 cgo-only 的 gojieba。
- deploy/packaging/installer.nsi:不再安装 homed.exe/initconfig.exe,改为携带
  **linux payload** 并调用新的 install-via-wsl.ps1;退出码 20/21 表示
  「需先装 WSL/发行版」,走指引而非报错。
- deploy/packaging/windows/install-via-wsl.ps1(新):检测 WSL → 引导安装 →
  确保 WSL2 → 送包进发行版 → 在 WSL 内按 Linux 方式安装。**复用 Linux 包与
  linux/setup.sh**,不另写一套安装逻辑;落点与 deb 布局统一
  (/usr/bin/homed + /usr/lib/homeagent/setup.sh)。
- deploy/packaging/linux/setup.sh:API Key 允许 HOMEAGENT_API_KEY 覆盖
  (否则安装器界面显示一份、config.db 里另一份 → 登录不上)。
- deploy/packaging/build.sh:windows 目标只构建 waiter + gui,并新增
  stage_linux_payload 把 Linux 包暂存给安装器;homed/initconfig 在 windows
  目标下明确拒绝。

## 5. 插件调用点统一写明意图

- webui 的 OpenAI 兼容端点(固定提示词模板)→ InjectTextSyncNoMemory。
- agentcli 的 5 处纯状态通知(已启动/超时/执行结束/进程退出/读取结束)→ NoMemory;
  **带输出**的 2 处(定时反馈、有新输出)刻意保留记忆并注明理由。
- timer 的定时提醒 → NoMemory(中断本来也隐含 NoMemory,这里是写明意图)。

## 6. 版本

meta.Version 仍为 1.2.0(main 是下一个未发布中版本);
SDKCompatibleVersion 1.1.0 → **1.2.0**(本内核已实现 SDK 1.2.0 全部新增方法)。

## 测试

- core:默认不裁剪(无声明/none/空)、通道 opt-in、注入点双向覆盖通道、
  nil context/io 安全、cleaner 优先级与未知名回退。
- io:零值 opts 与历史 payload 逐键相同;text/中断/媒体三类注入标志位都落到
  payload;旧方法仍生效。
- proc:validateContextPolicy 只接受 ""/none/prune,报错含位置与实际值;
  **跨进程** e2e——testdata 插件经 io.injectText 送出三个标志位,断言它们穿过 RPC
  到达内核。
- memory:模块缓存不可见时内嵌词库仍可用(分词与 POS 内容词均非空)、
  落盘幂等、内容哈希稳定。

验证:go build ./... / go vet ./... / go vet -tags onnxruntime ./...
      go test -short ./internal/memory/... ./internal/nlp/... ./internal/plugin/...
      ./internal/agent/{core,io}/... ./pkg/...
2026-09-11 20:31:50 +08:00

477 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
import (
"log"
"sync"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// SDKVersion 是对外 SDK 版本号,与核心 meta.Version 保持一致。
var SDKVersion = pubsdk.SDKVersion
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
type ToolHandler = pubsdk.ToolHandler
type StageHandler = pubsdk.StageHandler
type Stage = pubsdk.Stage
const (
StageOnInput = pubsdk.StageOnInput
StagePreAction = pubsdk.StagePreAction
StagePostAction = pubsdk.StagePostAction
StageBeforeToolcall = pubsdk.StageBeforeToolcall
StageAfterToolcall = pubsdk.StageAfterToolcall
StageBeforeOutput = pubsdk.StageBeforeOutput
StageAfterOutput = pubsdk.StageAfterOutput
)
type StageContext = pubsdk.StageContext
type MemItem = pubsdk.MemItem
type ToolCall = pubsdk.ToolCall
type ToolResult = pubsdk.ToolResult
type ToolDef = pubsdk.ToolDef
type IOInjector = pubsdk.IOInjector
type ToolRegistrar = pubsdk.ToolRegistrar
type StageRegistrar = pubsdk.StageRegistrar
type StageScope = pubsdk.StageScope
const (
StageScopeGlobal = pubsdk.StageScopeGlobal
StageScopeOwnTools = pubsdk.StageScopeOwnTools
)
type APIRegistrar = pubsdk.APIRegistrar
type OutputChannelRegistrar = pubsdk.OutputChannelRegistrar
type InputChannelRegistrar = pubsdk.InputChannelRegistrar
type ChannelDef = pubsdk.ChannelDef
// InjectOptions / 上下文策略常量:内置插件与外部插件必须用同一套类型与取值,
// 否则内核要认两份,而漏认会静默丢失标志位。
type InjectOptions = pubsdk.InjectOptions
const (
ContextPolicyNone = pubsdk.ContextPolicyNone
ContextPolicyPrune = pubsdk.ContextPolicyPrune
)
type DisabledPluginInfo struct {
Name string `json:"name"`
DisabledAt string `json:"disabled_at"`
DisabledBy string `json:"disabled_by"`
}
// PluginMeta is the display-name metadata for a plugin (from plg.json / RegisterPluginMeta).
type PluginMeta struct {
NameZh string `json:"name_zh"`
NameEn string `json:"name_en"`
}
// PluginRuntimeInfo 是插件的**运行期**状态,与 plugin.json 里的静态元数据相对。
//
// 为何需要:子进程插件的进程可能已经死了而注册表里还有条目(或反过来,
// 崩溃摘除后注册表已无条目但目录还在)。此前 plugin_list / GET /plugins
// 只读 plugin.json无论插件死活都返回同一份内容——WebUI 与模型都看不出
// 「已安装」与「正在运行」的区别,插件被 kill 后只表现为工具静默失败。
type PluginRuntimeInfo struct {
Name string `json:"name"`
// Loaded 表示注册表中存在该插件实例。
Loaded bool `json:"loaded"`
// Disabled 表示插件被显式禁用(不该运行)。
Disabled bool `json:"disabled"`
// Builtin 表示编译期内置插件(无独立进程)。
Builtin bool `json:"builtin"`
// Channel 是加载通道proc子进程/ lua / builtin。
Channel string `json:"channel"`
// PID 是子进程插件的进程号;非子进程或已退出为 0。
PID int `json:"pid"`
// Alive 表示子进程仍存活;非子进程插件与 Loaded 同值。
Alive bool `json:"alive"`
// CrashCount 是最近窗口内的崩溃次数0 表示健康)。
CrashCount int `json:"crash_count"`
// AutoRestart 表示崩溃后内核是否会自动拉起。
AutoRestart bool `json:"auto_restart"`
// Tools 是该插件当前注册在内核里的工具名。
Tools []string `json:"tools,omitempty"`
}
type PluginManager interface {
ListLoadedPlugins() []string
ListDisabledPlugins() []DisabledPluginInfo
IsPluginDisabled(name string) bool
// IsBuiltinPlugin 判断插件是否为内置插件编译期工厂init() 自注册)。
// 内置插件只能禁用/启用,不能卸载。
IsBuiltinPlugin(name string) bool
DisablePlugin(name, by string) error
EnablePlugin(name string) error
// RemovePlugin 卸载插件先停止stop handlers + Stop再执行插件注册的
// onRemove 回调RegisterOnRemoveHandler最后从注册表移除并清理配置表。
// 目录删除由调用方负责。
RemovePlugin(name string) error
// StopAndUnload 停止并从注册表移除插件但保留配置表,供更新/升级流程使用:
// 换产物不动配置,重装后配置原样生效。不触发 onRemove 回调。
StopAndUnload(name string) error
ReloadPlugins() (string, error)
// ReloadOne 重载单个插件(停止后重新加载,处理 dlclose/dynamic 句柄)。
ReloadOne(name string) error
PluginMetas() map[string]PluginMeta
PluginDir() string
// PluginRuntime 返回单个插件的运行期状态(进程存活 / PID / 崩溃计数)。
// 未安装的插件返回零值 + false。
PluginRuntime(name string) (PluginRuntimeInfo, bool)
// ListPluginRuntimes 返回全部已加载插件的运行期状态。
ListPluginRuntimes() []PluginRuntimeInfo
}
type PluginSDK struct {
*pubsdk.PluginSDK
settings SettingsAPI
memory MemoryAPI
textMem TextMemoryAPI
docMem DocMemoryAPI
know KnowledgeAPI
llm LLMAPI
iom *agentIO.IOManager
eventBus *events.Bus
logger *log.Logger
pluginMgr PluginManager
status StatusAPI
supervisor SupervisorAPI
adapter AdapterAPI
tracker TrackerAPI
config ConfigAPI
tool ToolAPI
indexer IndexerAPI
selftestMu sync.Mutex
selftest *VirtualInstance
}
func (s *PluginSDK) PluginMgr() PluginManager { return s.pluginMgr }
// 以下访问器遮蔽公共 SDK 的同名方法,返回内置插件可用的全量接口。
func (s *PluginSDK) Settings() SettingsAPI { return s.settings }
func (s *PluginSDK) Memory() MemoryAPI { return s.memory }
func (s *PluginSDK) TextMemory() TextMemoryAPI { return s.textMem }
func (s *PluginSDK) DocMemory() DocMemoryAPI { return s.docMem }
func (s *PluginSDK) Knowledge() KnowledgeAPI { return s.know }
func (s *PluginSDK) LLM() LLMAPI { return s.llm }
// ioAdapter 桥接 IOManager 到公共 SDK 的 IOInjector 接口,
// 确保外部插件通过 s.InjectText() 等方法的调用能被路由到内核 IO 层。
type ioAdapter struct{ iom *agentIO.IOManager }
func (a ioAdapter) InjectInterruptText(source, channel, text string) {
a.InjectInterruptTextOpts(source, channel, text, pubsdk.InjectOptions{})
}
// InjectInterruptTextOpts 注入可抢占当前处理的中断文本,并声明记忆/裁剪行为。
func (a ioAdapter) InjectInterruptTextOpts(source, channel, text string, opts pubsdk.InjectOptions) {
if a.iom != nil {
a.iom.InjectInterruptTextOpts(source, channel, text, opts)
}
}
// InjectInputSync 同步注入输入并等待回复(阻塞直至 agent 处理完成),返回回复文本。
func (a ioAdapter) InjectInputSync(source, channel, text string) string {
return a.InjectInputSyncOpts(source, channel, text, pubsdk.InjectOptions{})
}
// InjectInputSyncOpts 同步注入输入并声明记忆/裁剪行为。
func (a ioAdapter) InjectInputSyncOpts(source, channel, text string, opts pubsdk.InjectOptions) string {
if a.iom == nil {
return ""
}
out := a.iom.InjectInputSyncToOpts(source, channel, "text", map[string]interface{}{
"content": text,
}, opts)
if out == nil {
return ""
}
reply, _ := out.Payload["content"].(string)
return reply
}
func (a ioAdapter) InjectText(source, channel, text string) {
a.InjectTextOpts(source, channel, text, pubsdk.InjectOptions{})
}
// InjectTextOpts 注入排队文本,并声明记忆/裁剪行为。
func (a ioAdapter) InjectTextOpts(source, channel, text string, opts pubsdk.InjectOptions) {
if a.iom != nil {
a.iom.InjectTextOpts(source, channel, text, opts)
}
}
func (a ioAdapter) InjectTextNoMemory(source, channel, text string) {
a.InjectTextOpts(source, channel, text, pubsdk.InjectOptions{NoMemory: true})
}
// InjectInputMedia 注入带媒体内容块的输入。
//
// blocks 放在 payload 的 media_blocks 里,由 eventloop 取出转进
// stageCtx.Extra——与用户直接发图走的是同一条通道因此自动获得
// CAS 落盘与媒体记忆绑定。与 SetToolBlocks 的区别:后者只能在工具
// 调用内部用,且媒体要等到下一条 tool message 才到模型手上。
func (a ioAdapter) InjectInputMedia(source, channel, text string, blocks []pubsdk.ContentBlock) {
a.InjectInputMediaOpts(source, channel, text, blocks, pubsdk.InjectOptions{})
}
// InjectInputMediaOpts 注入带媒体块的输入,并声明记忆/裁剪行为。
func (a ioAdapter) InjectInputMediaOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions) {
if a.iom != nil {
a.iom.InjectInputMediaOpts(source, channel, text, blocks, opts)
}
}
// InjectInputMediaSync 注入带媒体内容块的输入并同步等待回复。
func (a ioAdapter) InjectInputMediaSync(source, channel, text string, blocks []pubsdk.ContentBlock) string {
return a.InjectInputMediaSyncOpts(source, channel, text, blocks, pubsdk.InjectOptions{})
}
// InjectInputMediaSyncOpts 注入带媒体块的输入并同步等待回复,同时声明记忆/裁剪行为。
func (a ioAdapter) InjectInputMediaSyncOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions) string {
if a.iom == nil {
return ""
}
out := a.iom.InjectInputMediaSyncOpts(source, channel, text, blocks, opts)
if out == nil {
return ""
}
reply, _ := out.Payload["content"].(string)
return reply
}
// InjectInterruptMedia 注入带媒体内容块的中断,可抢占当前 LLM 处理。
func (a ioAdapter) InjectInterruptMedia(source, channel, text string, blocks []pubsdk.ContentBlock) {
a.InjectInterruptMediaOpts(source, channel, text, blocks, pubsdk.InjectOptions{})
}
// InjectInterruptMediaOpts 注入带媒体块的中断,并声明记忆/裁剪行为。
func (a ioAdapter) InjectInterruptMediaOpts(source, channel, text string, blocks []pubsdk.ContentBlock, opts pubsdk.InjectOptions) {
if a.iom != nil {
a.iom.InjectInterruptMediaOpts(source, channel, text, blocks, opts)
}
}
// ContentBlock / ImageURL / AudioURL 是多模态内容块在插件边界上的类型。
//
// 别名到公共 SDK 而非另建一套内置插件webui/multimodal 等)与外部插件必须
// 用同一套结构,否则 resolveInput 的类型分支要认第三种类型,而漏认的后果是
// 媒体被静默丢弃。
type ContentBlock = pubsdk.ContentBlock
type ImageURL = pubsdk.ImageURL
type AudioURL = pubsdk.AudioURL
// SDKConfig holds all dependencies for creating a PluginSDK.
type SDKConfig struct {
IOManager *agentIO.IOManager
EventBus *events.Bus
Memory MemoryAPI
TextMemory TextMemoryAPI
DocMemory DocMemoryAPI
Knowledge KnowledgeAPI
LLM LLMAPI
Settings SettingsAPI
RegTool ToolRegistrar
RegStage StageRegistrar
RegAPI APIRegistrar
RegOutput OutputChannelRegistrar
RegInput InputChannelRegistrar
PluginMgr PluginManager
Status StatusAPI
Supervisor SupervisorAPI
Adapter AdapterAPI
Tracker TrackerAPI
Config ConfigAPI
Tool ToolAPI
Indexer IndexerAPI
}
func New(name string, cfg SDKConfig) *PluginSDK {
base := pubsdk.New(name, cfg.Settings, cfg.RegTool, cfg.RegStage, cfg.RegAPI, cfg.RegOutput)
if cfg.IOManager != nil {
base.SetIOInjector(ioAdapter{iom: cfg.IOManager})
}
if cfg.RegInput != nil {
base.SetInputChannelRegistrar(cfg.RegInput)
}
base.SetMemoryAPI(cfg.Memory)
base.SetTextMemoryAPI(cfg.TextMemory)
base.SetDocMemoryAPI(cfg.DocMemory)
base.SetKnowledgeAPI(cfg.Knowledge)
base.SetLLMAPI(cfg.LLM)
return &PluginSDK{
PluginSDK: base,
settings: cfg.Settings,
memory: cfg.Memory,
textMem: cfg.TextMemory,
docMem: cfg.DocMemory,
know: cfg.Knowledge,
llm: cfg.LLM,
iom: cfg.IOManager,
eventBus: cfg.EventBus,
logger: log.Default(),
pluginMgr: cfg.PluginMgr,
status: cfg.Status,
supervisor: cfg.Supervisor,
adapter: cfg.Adapter,
tracker: cfg.Tracker,
config: cfg.Config,
tool: cfg.Tool,
indexer: cfg.Indexer,
}
}
// Selftest 返回一个隔离的虚拟自检实例healthcheck 等内置插件用),
// 完全独立于生产存储,不产生任何污染。首次调用创建,复用已存在实例;
// 每轮自检前调用 SelftestReset 重建以清空上轮测试数据。
func (s *PluginSDK) Selftest(scope string) (*VirtualInstance, error) {
s.selftestMu.Lock()
defer s.selftestMu.Unlock()
if s.selftest == nil {
vi, err := NewVirtualInstance(scope)
if err != nil {
return nil, err
}
s.selftest = vi
}
return s.selftest, nil
}
// SelftestReset 清理并重建隔离自检实例,用于每轮健康检查前重置状态。
func (s *PluginSDK) SelftestReset(scope string) error {
s.selftestMu.Lock()
defer s.selftestMu.Unlock()
if s.selftest == nil {
vi, err := NewVirtualInstance(scope)
if err != nil {
return err
}
s.selftest = vi
return nil
}
vi, err := s.selftest.Reset(scope)
if err != nil {
return err
}
s.selftest = vi
return nil
}
func (s *PluginSDK) Status() StatusAPI { return s.status }
func (s *PluginSDK) Supervisor() SupervisorAPI { return s.supervisor }
func (s *PluginSDK) Adapter() AdapterAPI { return s.adapter }
func (s *PluginSDK) Tracker() TrackerAPI { return s.tracker }
func (s *PluginSDK) Config() ConfigAPI { return s.config }
func (s *PluginSDK) Tool() ToolAPI { return s.tool }
func (s *PluginSDK) Indexer() IndexerAPI { return s.indexer }
func (s *PluginSDK) InjectInput(source, channel, eventType string, payload map[string]interface{}) {
if s.iom != nil {
s.iom.InjectInputTo(source, channel, eventType, payload)
}
}
func (s *PluginSDK) InjectInputSync(source, channel, eventType string, payload map[string]interface{}) *agentIO.OutputEvent {
if s.iom != nil {
return s.iom.InjectInputSyncTo(source, channel, eventType, payload)
}
return nil
}
func (s *PluginSDK) InjectInterrupt(source, channel, eventType string, payload map[string]interface{}) {
if s.iom != nil {
if payload == nil {
payload = map[string]interface{}{}
}
payload["type"] = eventType
s.iom.InjectInterrupt(source, channel, payload)
}
}
func (s *PluginSDK) InjectTextSync(source, channel, text string) *agentIO.OutputEvent {
return s.InjectInputSync(source, channel, "text", map[string]interface{}{"content": text})
}
func (s *PluginSDK) InjectTextSyncNoMemory(source, channel, text string) *agentIO.OutputEvent {
return s.InjectInputSync(source, channel, "text", map[string]interface{}{"content": text, "no_memory": true})
}
func (s *PluginSDK) OutputChan() <-chan *agentIO.OutputEvent {
if s.iom != nil {
return s.iom.OutputChan()
}
return nil
}
func (s *PluginSDK) RegisterChannel(name string, dev agentIO.Device) error {
if s.iom != nil {
return s.iom.RegisterDevice(dev)
}
return nil
}
func (s *PluginSDK) UnregisterChannel(name string) {
if s.iom != nil {
s.iom.UnregisterDevice(name)
}
}
func (s *PluginSDK) ListChannels() []agentIO.ChannelInfo {
if s.iom != nil {
return s.iom.ListChannels()
}
return nil
}
func (s *PluginSDK) Publish(evt *events.Event) {
if s.eventBus != nil {
s.eventBus.Publish(evt)
}
}
func (s *PluginSDK) Subscribe(eventType events.EventType, handler events.Handler) func() {
if s.eventBus != nil {
return s.eventBus.Subscribe(eventType, handler)
}
return func() {}
}
// SetToolBlocks 桥接到 IOManager插件工具注入多模态块process.go 消费。
func (a ioAdapter) SetToolBlocks(blocks []pubsdk.ContentBlock) {
if a.iom == nil {
return
}
ifaces := make([]interface{}, len(blocks))
for i, b := range blocks {
ifaces[i] = b
}
a.iom.SetToolBlocks(ifaces)
}
// SetToolBlocks 注入多模态内容块(图片/音频),内核在下一条 tool message
// 的 content 数组里带上这些块,让模型在后续轮次看到图/听到音频。
func (s *PluginSDK) SetToolBlocks(blocks []pubsdk.ContentBlock) {
if s.iom != nil {
ifaces := make([]interface{}, len(blocks))
for i, b := range blocks {
ifaces[i] = b
}
s.iom.SetToolBlocks(ifaces)
}
}