sdk: embed non-toolchain SDK in third_party, add NoMemory/Cleaner support

- Embed sdk/, example/, meta/, go.mod from homeagent-sdk (no .git)
- Core .gitignore excludes SDK toolchain: bin/, tools/, package/
- RegisterInputChannel + ChannelDef(NoMemory, Cleaner) in SDK
- IOManager input channel registry with GetInputChannelDef
- eventloop: apply channel Cleaner/NoMemory to interrupt text
- context engine: channelDefLookup applied in textForVector
- document store: ChannelCleaner param for archive functions
- All callers/adapters updated with ChannelDef{} default
This commit is contained in:
root
2026-07-29 14:48:14 +08:00
parent 4218890aed
commit a899d777c3
29 changed files with 1265 additions and 94 deletions

3
third_party/homeagent-sdk/go.mod vendored Normal file
View File

@ -0,0 +1,3 @@
module gitcode.com/JianFeeeee/homeagent-sdk
go 1.21.0

87
third_party/homeagent-sdk/meta/meta.go vendored Normal file
View File

@ -0,0 +1,87 @@
// Package meta 收集 HomeAgent SDK 的全部元数据。
// 版本号应与核心 meta.Version 保持一致。
// ABI 版本与 Dispatch Method ID 应与核心仓 internal/meta/meta.go 保持一致。
package meta
var (
// Version 是 HomeAgent SDK 版本号。
// 通过 `-ldflags="-X gitcode.com/JianFeeeee/homeagent-sdk/meta.Version=vX.Y.Z"` 注入。
Version = "0.7.2"
// Commit 是构建时的 Git commit hash。
Commit = "unknown"
// BuildTime 是构建时间。
BuildTime = "unknown"
// SDKName 是 SDK 名称。
SDKName = "HomeAgent SDK"
// CoreModule 是核心仓的 Go module path供 plugindev 生成 go.mod 时使用。
CoreModule = "gitcode.com/JianFeeeee/HomeAgent"
// CoreVersion 是此 SDK 所兼容的最低核心版本。
CoreVersion = "0.7.2"
)
// FullVersion 返回完整的版本字符串。
func FullVersion() string {
return SDKName + " v" + Version + " (" + Commit + ")"
}
// ---- ABI 版本(与核心仓 internal/meta/meta.go 同步) ----
// 修改时需确保核心仓与 SDK 仓的值一致。
const (
ABIVersion = 1
ABIVersionMin = 1
)
// ---- Dispatch Method IDs与核心仓 internal/meta/meta.go 同步) ----
const (
CoreRegisterTool = 1
CoreRegisterStage = 2
CoreRegisterOutputCh = 3
CoreRegisterPluginAPI = 4
CoreInjectText = 5
CoreInjectInterruptText = 6
CoreInjectTextNoMemory = 7
CoreSetAutoRestart = 8
CoreMemoryRecall = 9
CoreMemoryCommit = 10
CoreMemoryIntrospect = 11
CoreMemoryMerge = 12
CoreMemoryPurge = 13
CoreDocQuery = 14
CoreKnowledgeSearch = 15
CoreSettingsGet = 16
CoreSettingsSet = 17
CoreSettingsRegisterDef = 18
CoreLLMListSources = 19
CoreLLMSetSource = 20
CoreSocialGetPerson = 21
CoreSocialGetNetwork = 22
CoreSubscribe = 23
CoreUnsubscribe = 24
CoreFreeString = 25
CoreSettingsGetCore = 26
CoreSettingsSetCore = 27
CoreSettingsListCore = 28
CoreSettingsGetPlugin = 29
CoreSettingsSetPlugin = 30
CoreSettingsListPlugin = 31
CoreDocInsert = 32
CoreDocRemove = 33
CoreDocStats = 34
CoreKnowledgeAdd = 35
CoreKnowledgeList = 36
CoreLLMCurrentSource = 37
CoreSocialGetTrait = 38
CoreSocialGetRelations = 39
CoreSocialListPersons = 40
CoreTextMemoryAppend = 41
CoreSettingsList = 42
CoreSettingsDefs = 43
CoreSettingsDump = 44
CoreSettingsPlugins = 45
)

View File

@ -0,0 +1,14 @@
package sdk
// KnowledgeAPI provides access to the knowledge store.
type KnowledgeAPI interface {
Search(query string, topK int) ([]*Knowledge, error)
Add(name, content string) error
List() ([]string, error)
}
// Knowledge represents a knowledge entry.
type Knowledge struct {
Name string `json:"name"`
Content string `json:"content"`
}

8
third_party/homeagent-sdk/sdk/llm.go vendored Normal file
View File

@ -0,0 +1,8 @@
package sdk
// LLMAPI provides access to the LLM provider manager.
type LLMAPI interface {
ListSources() []string
SetSource(name string) error
CurrentSource() string
}

87
third_party/homeagent-sdk/sdk/memory.go vendored Normal file
View File

@ -0,0 +1,87 @@
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.
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"`
}
// TextMemoryAPI provides access to chronological text event storage.
type TextMemoryAPI interface {
Append(evt TextEvent) error
}
// TextEvent represents a single text memory event.
type TextEvent struct {
Role string `json:"role"`
Content string `json:"content"`
Timestamp int64 `json:"timestamp"`
Channel string `json:"channel,omitempty"`
}
// DocMemoryAPI provides access to the document vector store.
type DocMemoryAPI interface {
Query(text string, topK int) []*Doc
Insert(doc *Doc) error
Remove(id string)
Stats() map[string]interface{}
}
// Doc represents a document in the document store.
type Doc struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Score float64 `json:"score,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"`
}

369
third_party/homeagent-sdk/sdk/plugin.go vendored Normal file
View File

@ -0,0 +1,369 @@
package sdk
import (
"sync"
"gitcode.com/JianFeeeee/homeagent-sdk/meta"
)
// SDKVersion 是对外暴露的 SDK 版本号。
var SDKVersion = meta.Version
// Plugin is the interface every plugin must implement.
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
// ToolHandler is a function that handles a tool call.
type ToolHandler func(args map[string]interface{}) (interface{}, error)
// StageHandler is a function that handles a pipeline stage event.
type StageHandler func(ctx *StageContext) error
// Stage represents a point in the message processing pipeline.
type Stage string
const (
StageOnInput Stage = "on_input"
StagePreAction Stage = "pre_action"
StagePostAction Stage = "post_action"
StageBeforeToolcall Stage = "before_toolcall"
StageAfterToolcall Stage = "after_toolcall"
StageBeforeOutput Stage = "before_output"
StageAfterOutput Stage = "after_output"
)
// ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
// NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提取/蒸馏),但原文保留在上下文中
// Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
type ChannelDef struct {
NoMemory bool
Cleaner func(string) string
}
// StageContext provides context for stage handlers.
type StageContext struct {
mu sync.RWMutex
RawMessage string
UserID string
GroupID string
ContextMsgs []map[string]interface{}
LLMText string
ReasoningContent string
TokenUsage map[string]int
ToolCalls []ToolCall
ToolResults []ToolResult
FinalText string
Response *string
Phase Stage
Memory []MemItem
NoMemory bool
Extra map[string]interface{}
Errors []string // 阶段处理过程中的错误信息
}
func (c *StageContext) RLock() { c.mu.RLock() }
func (c *StageContext) RUnlock() { c.mu.RUnlock() }
func (c *StageContext) Lock() { c.mu.Lock() }
func (c *StageContext) Unlock() { c.mu.Unlock() }
func (c *StageContext) IsResponded() bool { c.mu.RLock(); defer c.mu.RUnlock(); return c.Response != nil }
// MemItem represents a memory item in stage context.
type MemItem struct {
Role string `json:"role"`
Content string `json:"content"`
Score float64 `json:"score"`
}
// ToolCall represents a model's request to call a tool.
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Arguments map[string]interface{} `json:"arguments"`
}
// ToolResult represents the result of a tool call.
type ToolResult struct {
CallID string `json:"call_id"`
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Success bool `json:"success"`
Result interface{} `json:"result"`
}
// ToolDef describes a tool that the plugin exposes.
type ToolDef struct {
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
NoMemory bool `json:"no_memory,omitempty"` // 此工具输出不参与记忆计算,但原文保留
Cleaner func(string) string `json:"-"` // 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏时调用
}
// IOInjector provides methods for injecting input and interrupts into the agent pipeline.
// All methods accept (source, channel) where channel is the target output channel
// for routing the agent's response.
type IOInjector interface {
InjectInterruptText(source, channel, text string)
InjectText(source, channel, text string)
InjectTextNoMemory(source, channel, text string)
}
// EventType identifies the kind of system event.
type EventType string
const (
EventRawInput EventType = "raw_input"
EventAgentOutput EventType = "agent_output"
EventAgentLLMChain EventType = "agent_llm_chain"
EventToolCall EventType = "tool_call"
EventReasoning EventType = "reasoning"
EventStage EventType = "stage"
EventSystem EventType = "system"
)
// Event represents a system event published by the kernel.
type Event struct {
Type EventType `json:"type"`
Source string `json:"source"`
Payload map[string]interface{} `json:"payload"`
Timestamp int64 `json:"timestamp"`
}
// EventHandler processes a system event.
type EventHandler func(evt *Event)
// EventSubscriber allows plugins to subscribe to kernel events.
// This is a restricted interface: plugins can subscribe but the kernel
// controls which events are delivered.
type EventSubscriber interface {
Subscribe(eventType EventType, handler EventHandler) func()
}
// StageScope controls which events a stage handler receives.
type StageScope int
const (
// StageScopeGlobal receives all stage events (default).
StageScopeGlobal StageScope = 0
// StageScopeOwnTools only receives events for this plugin's own tool calls
// (before_toolcall / after_toolcall only). Other stages degrade to global.
StageScopeOwnTools StageScope = 1
)
// ToolRegistrar registers a tool dynamically.
type ToolRegistrar func(name string, def ToolDef, handler ToolHandler) error
// StageRegistrar registers a stage handler.
type StageRegistrar func(stage Stage, handler StageHandler)
// APIRegistrar registers a plugin API for external access.
type APIRegistrar func(name string) error
// InputChannelRegistrar registers an input channel with its memory behavior.
type InputChannelRegistrar func(name string, def ChannelDef) error
// OutputChannelRegistrar registers an output channel that the output_send tool can use.
type OutputChannelRegistrar func(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error
// Output capability flags
const (
CapText = 1
CapFile = 2
CapImage = 4
CapAudio = 8
CapStructured = 16
)
// PluginSDK is the main API surface provided to plugins at runtime.
// It wraps tool registration, settings, memory, knowledge, LLM, and IO injection.
type PluginSDK struct {
name string
regTool ToolRegistrar
regStage StageRegistrar
regAPI APIRegistrar
regOutput OutputChannelRegistrar
regInput InputChannelRegistrar
io IOInjector
mem MemoryAPI
textMem TextMemoryAPI
docMem DocMemoryAPI
know KnowledgeAPI
llm LLMAPI
sett SettingsAPI
social SocialAPI
events EventSubscriber
autoRestart bool
}
// New creates a PluginSDK with the given dependencies.
func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK {
return &PluginSDK{
name: name,
sett: sett,
regTool: regTool,
regStage: regStage,
regAPI: regAPI,
regOutput: regOutput,
autoRestart: true,
}
}
// PluginName returns the name of the plugin.
func (s *PluginSDK) PluginName() string { return s.name }
// Settings returns the settings API for reading/writing plugin configuration.
func (s *PluginSDK) Settings() SettingsAPI { return s.sett }
// Memory returns the graph memory API (may be nil if not available).
func (s *PluginSDK) Memory() MemoryAPI { return s.mem }
// TextMemory returns the text memory API (may be nil if not available).
func (s *PluginSDK) TextMemory() TextMemoryAPI { return s.textMem }
// DocMemory returns the document memory API (may be nil if not available).
func (s *PluginSDK) DocMemory() DocMemoryAPI { return s.docMem }
// Knowledge returns the knowledge store API (may be nil if not available).
func (s *PluginSDK) Knowledge() KnowledgeAPI { return s.know }
// LLM returns the LLM provider API (may be nil if not available).
func (s *PluginSDK) LLM() LLMAPI { return s.llm }
// Social returns the social graph API (may be nil if not available).
func (s *PluginSDK) Social() SocialAPI { return s.social }
// Events returns the event subscriber for listening to kernel events (may be nil if not available).
func (s *PluginSDK) Events() EventSubscriber { return s.events }
// RegisterTool registers a tool that the LLM can call.
func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) error {
if def.Plugin == "" {
def.Plugin = s.name
}
if s.regTool != nil {
return s.regTool(name, def, handler)
}
return nil
}
// RegisterStage registers a handler for a pipeline stage.
// scope: StageScopeGlobal (default) — receives all stage events.
// StageScopeOwnTools — only before_toolcall/after_toolcall for this plugin's tools.
func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler, scope ...StageScope) {
if s.regStage == nil {
return
}
sc := StageScopeGlobal
if len(scope) > 0 {
sc = scope[0]
}
if sc == StageScopeGlobal {
s.regStage(stage, handler)
return
}
// OwnTools scope — only for before_toolcall / after_toolcall
if stage != StageBeforeToolcall && stage != StageAfterToolcall {
s.regStage(stage, handler)
return
}
s.regStage(stage, func(ctx *StageContext) error {
ctx.RLock()
match := false
switch stage {
case StageBeforeToolcall:
match = len(ctx.ToolCalls) > 0 && ctx.ToolCalls[0].Plugin == s.name
case StageAfterToolcall:
match = len(ctx.ToolResults) > 0 && ctx.ToolResults[0].Plugin == s.name
}
ctx.RUnlock()
if !match {
return nil
}
return handler(ctx)
})
}
// RegisterPluginAPI registers this plugin's API for access by other plugins.
func (s *PluginSDK) RegisterPluginAPI(name string) error {
if s.regAPI != nil {
return s.regAPI(name)
}
return nil
}
// RegisterOutputChannel registers an output channel that the output_send tool can route to.
// name: channel name (e.g. "qq", "webui")
// caps: bitmask of supported output capabilities (CapText, CapFile, etc.)
// desc: description of the channel, expected meta format, and type enum
// def: 通道在记忆计算层的行为NoMemory/Cleaner
// handler: receives args map with keys: payload (string), type (string), meta (string|optional)
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error {
if s.regOutput != nil {
return s.regOutput(name, caps, desc, def, handler)
}
return nil
}
// RegisterInputChannel registers an input channel with its memory behavior.
// def.NoMemory: 此通道输入不参与记忆计算
// def.Cleaner: 计算层对输入文本清洗后(不改原文)再向量化/提关键词
func (s *PluginSDK) RegisterInputChannel(name string, def ChannelDef) error {
if s.regInput != nil {
return s.regInput(name, def)
}
return nil
}
// SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar) { s.regOutput = r }
// SetInputChannelRegistrar sets the input channel registrar (called by the core at startup).
func (s *PluginSDK) SetInputChannelRegistrar(r InputChannelRegistrar) { s.regInput = r }
// SetIOInjector sets the IO injector (called by the core at startup).
func (s *PluginSDK) SetIOInjector(io IOInjector) { s.io = io }
// SetMemoryAPI sets the memory API (called by the core at startup).
func (s *PluginSDK) SetMemoryAPI(mem MemoryAPI) { s.mem = mem }
func (s *PluginSDK) SetTextMemoryAPI(tm TextMemoryAPI) { s.textMem = tm }
func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI) { s.docMem = dm }
func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI) { s.know = kn }
func (s *PluginSDK) SetLLMAPI(llm LLMAPI) { s.llm = llm }
func (s *PluginSDK) SetSocialAPI(social SocialAPI) { s.social = social }
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber) { s.events = es }
// ---- IO Convenience Methods ----
// InjectInterruptText injects a text interrupt that can preempt current LLM processing.
func (s *PluginSDK) InjectInterruptText(source, channel, text string) {
if s.io != nil {
s.io.InjectInterruptText(source, channel, text)
}
}
// InjectText injects a text message into the agent pipeline.
func (s *PluginSDK) InjectText(source, channel, text string) {
if s.io != nil {
s.io.InjectText(source, channel, text)
}
}
// InjectTextNoMemory injects a text message without generating memory.
func (s *PluginSDK) InjectTextNoMemory(source, channel, text string) {
if s.io != nil {
s.io.InjectTextNoMemory(source, channel, text)
}
}
// SetAutoRestart 设置插件是否允许内核自动重启(崩溃后自动重载)。
// 默认 true。如果插件有无法恢复的状态如外部连接应设为 false。
func (s *PluginSDK) SetAutoRestart(enabled bool) { s.autoRestart = enabled }
// AutoRestart 返回插件是否允许自动重启。
func (s *PluginSDK) AutoRestart() bool { return s.autoRestart }

View File

@ -0,0 +1,253 @@
package sdk
import (
"testing"
)
func TestRegisterStageGlobalDefault(t *testing.T) {
called := false
regStage := func(stage Stage, handler StageHandler) {
called = true
}
s := &PluginSDK{regStage: regStage, name: "test"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil })
if !called {
t.Error("global scope: handler not registered")
}
}
func TestRegisterStageGlobalExplicit(t *testing.T) {
called := false
regStage := func(stage Stage, handler StageHandler) {
called = true
}
s := &PluginSDK{regStage: regStage, name: "test"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeGlobal)
if !called {
t.Error("global scope: handler not registered")
}
}
func TestRegisterStageOwnToolsMatch(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
ctx.ToolCalls = []ToolCall{{Plugin: "myplugin", Name: "my_tool"}}
ctx.ToolResults = nil
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
}
func TestRegisterStageOwnToolsSkipOtherPlugin(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
ctx.ToolCalls = []ToolCall{{Plugin: "other", Name: "other_tool"}}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 0 {
t.Error("handler should not be called for other plugin's tool")
}
}
func TestRegisterStageOwnToolsNonToolcallDegrades(t *testing.T) {
regStage := func(stage Stage, handler StageHandler) {
if stage != StagePreAction {
t.Errorf("expected StagePreAction, got %s", stage)
}
}
s := &PluginSDK{regStage: regStage, name: "test"}
s.RegisterStage(StagePreAction, func(ctx *StageContext) error { return nil }, StageScopeOwnTools)
}
func TestRegisterStageOwnToolsStageBeforeToolcallNoToolCalls(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 0 {
t.Error("handler should not be called when ToolCalls is empty")
}
}
func TestRegisterStageOwnToolsStageAfterToolcallMatch(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageAfterToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
ctx.ToolResults = []ToolResult{{Plugin: "myplugin", Name: "my_tool"}}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 1 {
t.Error("handler should be called for own plugin's tool result")
}
}
func TestRegisterStageOwnToolsStageAfterToolcallSkip(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageAfterToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
ctx := &StageContext{}
ctx.ToolResults = []ToolResult{{Plugin: "other", Name: "other_tool"}}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 0 {
t.Error("handler should not be called for other plugin's tool result")
}
}
func TestRegisterStageOwnToolsNilRegStage(t *testing.T) {
s := &PluginSDK{name: "test"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeOwnTools)
}
func TestToolDefCleaner(t *testing.T) {
called := false
def := ToolDef{
Name: "test_clean",
Description: "A test tool with cleaner",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
Cleaner: func(output string) string {
called = true
return "cleaned:" + output
},
}
if def.Cleaner == nil {
t.Fatal("Cleaner should not be nil")
}
result := def.Cleaner("raw output")
if !called {
t.Error("Cleaner was not called")
}
if result != "cleaned:raw output" {
t.Errorf("expected 'cleaned:raw output', got '%s'", result)
}
}
func TestToolDefNoMemory(t *testing.T) {
def := ToolDef{
Name: "test_nomem",
Description: "A test tool with NoMemory",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: true,
}
if !def.NoMemory {
t.Error("NoMemory should be true")
}
if def.Cleaner != nil {
t.Error("Cleaner should be nil when not set")
}
}
func TestToolDefNoMemoryDefaultFalse(t *testing.T) {
def := ToolDef{
Name: "test_default",
Description: "A test tool with defaults",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
}
if def.NoMemory {
t.Error("NoMemory should default to false")
}
}
func TestToolDefRegisterPreservesNoMemory(t *testing.T) {
var capturedDef ToolDef
regTool := func(name string, def ToolDef, handler ToolHandler) error {
capturedDef = def
return nil
}
s := &PluginSDK{regTool: regTool, name: "test"}
def := ToolDef{
Name: "test_tool",
Description: "test desc",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: true,
Cleaner: func(s string) string { return s },
}
s.RegisterTool("test_tool", def, func(args map[string]interface{}) (interface{}, error) {
return nil, nil
})
if !capturedDef.NoMemory {
t.Error("NoMemory should be preserved through RegisterTool")
}
if capturedDef.Cleaner == nil {
t.Error("Cleaner should be preserved through RegisterTool")
}
}

View File

@ -0,0 +1,58 @@
package sdk
type SettingsAPI interface {
// Get reads the plugin's own config value (config_<name> table).
Get(key string) (interface{}, error)
// Set writes a config value to the plugin's own config table.
Set(key string, value interface{}) error
// List returns all keys matching the given prefix.
List(prefix string) ([]string, error)
// GetCore reads the core config table.
GetCore(key string) (interface{}, error)
// SetCore writes to the core config table.
SetCore(key string, value interface{}) error
// ListCore lists core config keys matching the prefix.
ListCore(prefix string) ([]string, error)
// GetPlugin reads another plugin's config table.
GetPlugin(plugin, key string) (interface{}, error)
// SetPlugin writes to another plugin's config table.
SetPlugin(plugin, key string, value interface{}) error
// ListPlugin lists another plugin's config keys matching the prefix.
ListPlugin(plugin, prefix string) ([]string, error)
// RegisterDef registers a config definition for UI display.
RegisterDef(def ConfigDef)
// Defs returns config definitions matching the prefix.
Defs(prefix string) []*ConfigDef
// Dump returns all config values.
Dump() map[string]interface{}
// Plugins returns a list of all plugin config namespaces.
Plugins() []string
}
// ConfigDef describes a configuration field for the WebUI.
type ConfigDef struct {
Key string `json:"key"`
Default interface{} `json:"default,omitempty"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Description string `json:"description,omitempty"`
Category string `json:"category,omitempty"`
Options []string `json:"options,omitempty"`
Min float64 `json:"min,omitempty"`
Max float64 `json:"max,omitempty"`
Step float64 `json:"step,omitempty"`
Required bool `json:"required,omitempty"`
Secret bool `json:"secret,omitempty"`
}