mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 08:58:03 +00:00
记忆系统在核心 1.1.0 支持了二进制多媒体节点,但那条链路只对**内核自己**开放: 插件把 Triple / Doc 交进来,媒体一律无处安放,且**不报错**。本版补上公开接口 侧缺失的表达能力。 ## 一、类型与接口(全部新增,无签名变更) - `Triple` += `SentenceText`、`MediaDigests` - `Doc` += `MediaDigests`、`Attachments`;新增 `MediaAttachment` - `TextEvent` += `Attachments` - `DocMemoryAPI` += `InsertWithMedia` - `IOInjector` += `InjectInputMedia` / `InjectInputMediaSync` / `InjectInterruptMedia` - `PluginSDK` 补上一直缺失的 `SetToolBlocks` 包装(接口里有、便捷方法里没有, 插件只能自己去拿 injector) `MediaAttachment` 一个类型服务两个方向:给 `Data`+`MIME` 是新内容(内核按字节 去重),只给 `Digest` 是引用已有内容。读路径**只回元数据不回字节**——一次检索 可能命中几十份媒体,把字节全塞回来会撑爆跨进程消息。 媒体注入为什么不能搭 `SetToolBlocks` 的车:那个方法只在工具处理函数内部可用, 且媒体要等**下一条** tool message 才到模型手上。插件主动发起一轮带媒体的对话、 以及中断注入,需要各自的签名,且媒体在**本轮**就随消息发出。 `Triple.MediaDigests` 非空而 `SentenceText` 为空时,内核会用媒体标记本身充当句子 ——媒体引用挂在句子上,没有句子就无处挂起。插件只需填 digest,标记由内核拼: 要求调用方知道格式,等于让一个拼写错误静默切断引用绑定而全链路无人报错。 ## 二、修掉两处并发竞态 `sdk/stress_test.go` 的 `-race` 实测报 11 处 DATA RACE,收敛到两个字段: 1. **`PluginSDK` 的 API 字段无锁**。写方是内核(加载/重载插件时依次注入 injector、memory、doc、llm…),读方是插件在 `Start()` 里起的后台 goroutine ——轮询、监听、定时器都要拿 injector 往管道注消息。生产表现是插件重载瞬间 偶发崩溃:读到半个接口值就 nil 解引用。 2. **`autoRestart` 标志无锁**。`SetAutoRestart` 的文档用法本身就是「外部连接建好 后再决定能否自动重启」,而连接建立通常在后台 goroutine;内核 registry 在另一个 goroutine 读 `AutoRestart()` 决定崩溃后重启策略。这对读写天然跨 goroutine。 加 `apiMu sync.RWMutex`。关键约定写进注释:**只在持锁期间取字段值,取完立刻 释放再调用**。持锁调用会把 `InjectInputSync`(阻塞到 agent 回复,可达数分钟) 与 `SetIOInjector` 串到一起,让插件重载卡死。 ## 三、压测(sdk/stress_test.go,13 例) SDK 是被多个 goroutine 同时使用的共享对象,单线程单测全绿不代表并发路径成立。 断言的是不变量而非吞吐: - 媒体注入高并发不丢不串——每次调用带唯一 tag,逐条校验文本与图片 URL 配对。 「不串」是重点:若实现里出现任何共享中间状态(把 blocks 暂存到字段再读出), 高并发下会出现 A 的文本配 B 的图,而两者单独看都「成功」了; - injector 热替换(含替换成 nil,即内核卸载 API 的真实状态); - stop / onRemove handler 恰好一次——契约是「执行后清空,幂等」,执行两次的后果 从重复写文件到 close 已关闭 channel 直接 panic; - `StageContext` 并发读改写无 lost update(媒体链路让 Extra 成为新热点, 而 map 并发写在 Go 里是直接 fatal,recover 接不住); - `OwnTools` scope 不跨插件泄漏; - 媒体类型 JSON 往返字节级一致(9 种长度,含 0/1/2/3 与 base64 分组边界) ——`[]byte` 在 JSON 里是 base64,往返不一致意味着图片静默损坏, 要到 CAS 校验 digest 时才发现,那时已无从追查; - `omitempty` 真的生效(读路径不能出现 `"data"` 键); - nil 依赖全部静默降级不 panic。 ## 四、工具链同步 - `proc_main.go.tmpl`:`procIO` 三个媒体方法、`procDocMemory.InsertWithMedia`。 模板不跟上的后果是**每个外部插件都编不过**(接口未实现),是硬失败; - `proc_runtime_test.go`:方法清单补 `io.injectMedia*` 与 `doc.insertWithMedia`。 漏接线时插件调 `InjectInputMedia` 会静默无效果——模板不发这个 RPC,内核也就 收不到,两边都不报错; - `yaegi/mocksdk`:与公开 SDK 对齐。它此前漂移严重且**没有任何代码对着它编译**, 所以漂移不会被编译器抓到:`Triple` 用的是 `Predicate`,而公开 SDK 一直叫 `Relation` —— 插件在 yaegi 调试期写 `Relation:` 报未知字段,写 `Predicate:` 则 编成 plugin.bin 时报错,两边都不对。 - README 中英双语补媒体接口文档与用法示例。 ## 兼容性 存量插件不需要改一行也不需要重编:新增方法由**插件调用、内核实现**,不调就不 受影响。17 个 example 插件源码零改动通过类型检查;用 SDK 0.9.2 编的旧 plugin.bin 在新内核上直接建链通过(握手校验的是 ProtocolVersion=1,不是 SDK 版本)。 媒体接口需要核心 1.1.1+(更早的核心没有对应 RPC,调用返回 unknown method)。 `CoreVersion` 保持 1.0.0:它是「SDK 能在其上运行」的下限,媒体是可选能力。
118 lines
4.6 KiB
Go
118 lines
4.6 KiB
Go
package sdk
|
||
|
||
// MemoryAPI provides access to the graph memory (entity-relation store).
|
||
type MemoryAPI interface {
|
||
Recall(query []string, depth int) ([]Entity, []Relation, error)
|
||
Commit(triples []Triple) error
|
||
Introspect() (map[string]interface{}, error)
|
||
MergeEntities(source, target string) (int, error)
|
||
Purge(criteria map[string]string, mode string) (int, error)
|
||
}
|
||
|
||
// Entity represents a named entity in the knowledge graph.
|
||
type Entity struct {
|
||
Name string `json:"name"`
|
||
Type string `json:"type"`
|
||
MentionCount int `json:"mention_count"`
|
||
}
|
||
|
||
// Relation represents a relationship between two entities.
|
||
type Relation struct {
|
||
SourceName string `json:"source_name"`
|
||
TargetName string `json:"target_name"`
|
||
RelationType string `json:"relation_type"`
|
||
Confidence float64 `json:"confidence,omitempty"`
|
||
}
|
||
|
||
// Triple represents a subject-relation-object triple for the knowledge graph.
|
||
//
|
||
// SentenceText 是这条三元组的原句,会写进 sentences 表;媒体引用挂在句子上,
|
||
// 所以 MediaDigests 非空时内核会保证句子存在(不给就自动合成一句)。
|
||
type Triple struct {
|
||
Subject string `json:"subject"`
|
||
Relation string `json:"relation"`
|
||
Object string `json:"object"`
|
||
Confidence float64 `json:"confidence,omitempty"`
|
||
SubjectType string `json:"subject_type,omitempty"`
|
||
ObjectType string `json:"object_type,omitempty"`
|
||
SentenceText string `json:"sentence_text,omitempty"`
|
||
MediaDigests []string `json:"media_digests,omitempty"`
|
||
}
|
||
|
||
// TextMemoryAPI provides access to chronological text event storage.
|
||
type TextMemoryAPI interface {
|
||
Append(evt TextEvent) error
|
||
}
|
||
|
||
// TextEvent represents a single text memory event.
|
||
// MediaAttachment 描述一份与记忆关联的媒体。
|
||
//
|
||
// 两个方向共用一个类型:
|
||
// - 写入(InsertWithMedia):给 Data + MIME 就是新内容;只给 Digest 则是引用已有内容。
|
||
// - 读出(Query):内核只填 Digest/MIME/Description,**不回 Data**——
|
||
// 一次检索可能命中几十张图,把字节全塞回插件会把 ABI 消息撑爆。
|
||
// 需要字节时拿 Digest 单独取。
|
||
type MediaAttachment struct {
|
||
Digest string `json:"digest,omitempty"`
|
||
MIME string `json:"mime,omitempty"`
|
||
Data []byte `json:"data,omitempty"`
|
||
Name string `json:"name,omitempty"`
|
||
Description string `json:"description,omitempty"`
|
||
}
|
||
|
||
type TextEvent struct {
|
||
Role string `json:"role"`
|
||
Content string `json:"content"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
Channel string `json:"channel,omitempty"`
|
||
Attachments []MediaAttachment `json:"attachments,omitempty"`
|
||
}
|
||
|
||
// DocMemoryAPI provides access to the document vector store.
|
||
type DocMemoryAPI interface {
|
||
Query(text string, topK int) []*Doc
|
||
Insert(doc *Doc) error
|
||
// InsertWithMedia 写入文档并关联媒体。attachments 里带 Data 的会落进
|
||
// 内容寻址存储(相同字节只存一份),只带 Digest 的直接引用已有内容。
|
||
// 插件无需自己拼标记:内核会把 `[mime <短 digest>] <描述>` 补进 Content,
|
||
// 让向量检索和后续蒸馏都能看到这份媒体。
|
||
InsertWithMedia(doc *Doc, attachments []MediaAttachment) error
|
||
Remove(id string)
|
||
Stats() map[string]interface{}
|
||
}
|
||
|
||
// Doc represents a document in the document store.
|
||
//
|
||
// MediaDigests / Attachments 在 Query 返回时由内核填充(仅元数据,不带字节)。
|
||
type Doc struct {
|
||
ID string `json:"id"`
|
||
Title string `json:"title"`
|
||
Content string `json:"content"`
|
||
Score float64 `json:"score,omitempty"`
|
||
MediaDigests []string `json:"media_digests,omitempty"`
|
||
Attachments []MediaAttachment `json:"attachments,omitempty"`
|
||
}
|
||
|
||
// SocialAPI provides read-only access to the social graph (person profiles and relationships).
|
||
// External plugins can query person traits and social networks but cannot modify them.
|
||
type SocialAPI interface {
|
||
GetPerson(name string) (*PersonProfile, error)
|
||
GetTrait(name, trait string) (string, bool)
|
||
GetRelations(name string) ([]SocialRelation, error)
|
||
GetNetwork(name string, depth int) ([]*PersonProfile, error)
|
||
ListPersons() ([]string, error)
|
||
}
|
||
|
||
// PersonProfile represents a person's complete profile (traits + social relations).
|
||
type PersonProfile struct {
|
||
Name string `json:"name"`
|
||
Traits map[string]string `json:"traits,omitempty"`
|
||
Relations []SocialRelation `json:"relations,omitempty"`
|
||
}
|
||
|
||
// SocialRelation represents a social relationship between two persons.
|
||
type SocialRelation struct {
|
||
Person string `json:"person"`
|
||
Relation string `json:"relation"`
|
||
}
|