feat(sdk): 插件 SDK(Tool/Skill/Channel 接口 + 注册辅助 + 配置 hook)
This commit is contained in:
1 parent
c52ddfd9c7
commit
7b8cfc664a
5 files changed
+409
No files matched your search
@@ -0,0 +1,29 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Channel registers a chat platform adapter (QQ, WeChat, Telegram, ...).
|
||||||
|
//
|
||||||
|
// The SDK pins only the lifecycle shape. The authoritative contract for
|
||||||
|
// adapters lives in internal/channel/adapter (design authority
|
||||||
|
// DevDocs/design/08-渠道接入.md §1, the PlatformAdapter contract) and is
|
||||||
|
// implemented in milestone M6. This interface mirrors the lifecycle portion of
|
||||||
|
// that contract — Name/Connect/Disconnect/IsConnected/HealthCheck — without
|
||||||
|
// depending on the internal message model, so a platform plugin can register
|
||||||
|
// itself here while the message flow is provided by the channel layer.
|
||||||
|
//
|
||||||
|
// Implementations must be safe for concurrent use.
|
||||||
|
type Channel interface {
|
||||||
|
// Name returns the platform identifier (for example "qq", "telegram").
|
||||||
|
Name() string
|
||||||
|
// Connect establishes the platform connection using config. It may be
|
||||||
|
// called more than once and must be idempotent.
|
||||||
|
Connect(ctx context.Context, config any) error
|
||||||
|
// Disconnect tears the connection down gracefully. It returns nil when the
|
||||||
|
// adapter was already disconnected.
|
||||||
|
Disconnect(ctx context.Context) error
|
||||||
|
// IsConnected reports whether the connection is currently up.
|
||||||
|
IsConnected() bool
|
||||||
|
// HealthCheck probes the platform and returns an error when it is unhealthy.
|
||||||
|
HealthCheck(ctx context.Context) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Package sdk is the standalone plugin SDK for Cyrene v2. Third-party plugins
|
||||||
|
// import this package, implement the Tool, Skill and Channel interfaces, and
|
||||||
|
// register themselves through the Registrar that the core capability registry
|
||||||
|
// (internal/capability/registry) implements.
|
||||||
|
//
|
||||||
|
// Plugins are compiled into the binary and registered at startup; there is no
|
||||||
|
// Go plugin dynamic loading. That keeps the plugin surface portable across
|
||||||
|
// Windows, ARM and other targets, as required by DevDocs/design/09 §2.
|
||||||
|
//
|
||||||
|
// The three plugin categories mirror DevDocs/design/09 §0:
|
||||||
|
//
|
||||||
|
// - Tool: a function plus an input JSON Schema, callable directly by the
|
||||||
|
// agent (calculator, web-search, file, ...).
|
||||||
|
// - Skill: a reusable workflow (an ordered set of steps) with prompts,
|
||||||
|
// references and guardrails, higher-level than a Tool (daily brief, code
|
||||||
|
// review, multi-stage report).
|
||||||
|
// - Channel: a platform adapter (QQ, WeChat, Telegram, ...).
|
||||||
|
//
|
||||||
|
// Capabilities are globally shared and always available to every character and
|
||||||
|
// mode; the registry is what makes them visible. A plugin never needs to know
|
||||||
|
// the registry's concrete type — it registers through the Registrar interface.
|
||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNilRegistrar is returned by the helper registration functions when the
|
||||||
|
// supplied Registrar is nil.
|
||||||
|
var ErrNilRegistrar = errors.New("sdk: nil registrar")
|
||||||
|
|
||||||
|
// Registrar is the subset of a capability registry that accepts plugins. The
|
||||||
|
// core registry (internal/capability/registry) implements it, and third-party
|
||||||
|
// plugins register through it.
|
||||||
|
type Registrar interface {
|
||||||
|
// RegisterTool registers a Tool.
|
||||||
|
RegisterTool(t Tool) error
|
||||||
|
// RegisterSkill registers a Skill.
|
||||||
|
RegisterSkill(s Skill) error
|
||||||
|
// RegisterChannel registers a Channel adapter.
|
||||||
|
RegisterChannel(c Channel) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigHook validates or mutates a plugin's configuration section at startup,
|
||||||
|
// before the runtime uses it. The host applies hooks in registration order and
|
||||||
|
// stops at the first error or on context cancellation.
|
||||||
|
type ConfigHook func(ctx context.Context, config map[string]any) error
|
||||||
|
|
||||||
|
// RegisterTool registers t with r. It returns ErrNilRegistrar when r is nil.
|
||||||
|
func RegisterTool(r Registrar, t Tool) error {
|
||||||
|
if r == nil {
|
||||||
|
return ErrNilRegistrar
|
||||||
|
}
|
||||||
|
return r.RegisterTool(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterSkill registers s with r. It returns ErrNilRegistrar when r is nil.
|
||||||
|
func RegisterSkill(r Registrar, s Skill) error {
|
||||||
|
if r == nil {
|
||||||
|
return ErrNilRegistrar
|
||||||
|
}
|
||||||
|
return r.RegisterSkill(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterChannel registers c with r. It returns ErrNilRegistrar when r is nil.
|
||||||
|
func RegisterChannel(r Registrar, c Channel) error {
|
||||||
|
if r == nil {
|
||||||
|
return ErrNilRegistrar
|
||||||
|
}
|
||||||
|
return r.RegisterChannel(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunConfigHooks applies hooks in order against config, stopping at the first
|
||||||
|
// error or when ctx is canceled. Nil hooks are skipped. It returns nil when
|
||||||
|
// every hook succeeds.
|
||||||
|
func RunConfigHooks(ctx context.Context, config map[string]any, hooks ...ConfigHook) error {
|
||||||
|
for _, h := range hooks {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := h(ctx, config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stubTool is a mock Tool used to verify the registration helpers forward to a
|
||||||
|
// Registrar.
|
||||||
|
type stubTool struct{ name string }
|
||||||
|
|
||||||
|
func (s stubTool) Name() string { return s.name }
|
||||||
|
func (s stubTool) Description() string { return "desc " + s.name }
|
||||||
|
func (s stubTool) Parameters() (map[string]any, error) { return map[string]any{"type": "object"}, nil }
|
||||||
|
func (s stubTool) Execute(_ context.Context, _ any) (any, error) { return "exec " + s.name, nil }
|
||||||
|
|
||||||
|
// stubSkill is a mock Skill used to verify the registration helpers forward.
|
||||||
|
type stubSkill struct{ name string }
|
||||||
|
|
||||||
|
func (s stubSkill) Name() string { return s.name }
|
||||||
|
func (s stubSkill) Description() string { return "desc " + s.name }
|
||||||
|
func (s stubSkill) Prompt() string { return "prompt " + s.name }
|
||||||
|
func (s stubSkill) Steps() []Step { return []Step{{Name: "s1", Prompt: "p1"}} }
|
||||||
|
func (s stubSkill) Refs() []Ref { return []Ref{{Name: "r1", Type: "doc"}} }
|
||||||
|
func (s stubSkill) Guardrails() []Guardrail {
|
||||||
|
return []Guardrail{{ID: "g1", Message: "no", Enforced: true}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stubChannel is a mock Channel used to verify the registration helpers forward.
|
||||||
|
type stubChannel struct{ name string }
|
||||||
|
|
||||||
|
func (s stubChannel) Name() string { return s.name }
|
||||||
|
func (s stubChannel) Connect(_ context.Context, _ any) error { return nil }
|
||||||
|
func (s stubChannel) Disconnect(_ context.Context) error { return nil }
|
||||||
|
func (s stubChannel) IsConnected() bool { return true }
|
||||||
|
func (s stubChannel) HealthCheck(_ context.Context) error { return nil }
|
||||||
|
|
||||||
|
// stubRegistrar records registrations and always succeeds.
|
||||||
|
type stubRegistrar struct {
|
||||||
|
tools []Tool
|
||||||
|
skills []Skill
|
||||||
|
channels []Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubRegistrar) RegisterTool(t Tool) error {
|
||||||
|
s.tools = append(s.tools, t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubRegistrar) RegisterSkill(skl Skill) error {
|
||||||
|
s.skills = append(s.skills, skl)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubRegistrar) RegisterChannel(c Channel) error {
|
||||||
|
s.channels = append(s.channels, c)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errRegistrar fails every registration with the error it carries.
|
||||||
|
type errRegistrar struct{ err error }
|
||||||
|
|
||||||
|
func (e errRegistrar) RegisterTool(Tool) error { return e.err }
|
||||||
|
func (e errRegistrar) RegisterSkill(Skill) error { return e.err }
|
||||||
|
func (e errRegistrar) RegisterChannel(Channel) error { return e.err }
|
||||||
|
|
||||||
|
func TestRegisterTool_Forwards(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
r := &stubRegistrar{}
|
||||||
|
tool := stubTool{name: "calc"}
|
||||||
|
if err := RegisterTool(r, tool); err != nil {
|
||||||
|
t.Fatalf("RegisterTool: %v", err)
|
||||||
|
}
|
||||||
|
if len(r.tools) != 1 || r.tools[0] != tool {
|
||||||
|
t.Fatalf("registered %d tools (%v), want exactly tool %v", len(r.tools), r.tools, tool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterSkill_Forwards(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
r := &stubRegistrar{}
|
||||||
|
skill := stubSkill{name: "review"}
|
||||||
|
if err := RegisterSkill(r, skill); err != nil {
|
||||||
|
t.Fatalf("RegisterSkill: %v", err)
|
||||||
|
}
|
||||||
|
if len(r.skills) != 1 || r.skills[0] != skill {
|
||||||
|
t.Fatalf("registered %d skills (%v), want exactly skill %v", len(r.skills), r.skills, skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterChannel_Forwards(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
r := &stubRegistrar{}
|
||||||
|
channel := stubChannel{name: "telegram"}
|
||||||
|
if err := RegisterChannel(r, channel); err != nil {
|
||||||
|
t.Fatalf("RegisterChannel: %v", err)
|
||||||
|
}
|
||||||
|
if len(r.channels) != 1 || r.channels[0] != channel {
|
||||||
|
t.Fatalf("registered %d channels (%v), want exactly channel %v", len(r.channels), r.channels, channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterHelpers_NilRegistrar(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
call func() error
|
||||||
|
}{
|
||||||
|
{name: "tool", call: func() error { return RegisterTool(nil, stubTool{name: "t"}) }},
|
||||||
|
{name: "skill", call: func() error { return RegisterSkill(nil, stubSkill{name: "s"}) }},
|
||||||
|
{name: "channel", call: func() error { return RegisterChannel(nil, stubChannel{name: "c"}) }},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if err := tt.call(); !errors.Is(err, ErrNilRegistrar) {
|
||||||
|
t.Fatalf("err = %v, want ErrNilRegistrar", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterHelpers_PropagateError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
want := errors.New("sdk: propagate error")
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
call func() error
|
||||||
|
}{
|
||||||
|
{name: "tool", call: func() error { return RegisterTool(errRegistrar{err: want}, stubTool{name: "t"}) }},
|
||||||
|
{name: "skill", call: func() error { return RegisterSkill(errRegistrar{err: want}, stubSkill{name: "s"}) }},
|
||||||
|
{name: "channel", call: func() error { return RegisterChannel(errRegistrar{err: want}, stubChannel{name: "c"}) }},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if err := tt.call(); !errors.Is(err, want) {
|
||||||
|
t.Fatalf("err = %v, want %v", err, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunConfigHooks_RunsInOrder(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var order []string
|
||||||
|
hooks := []ConfigHook{
|
||||||
|
func(_ context.Context, _ map[string]any) error { order = append(order, "a"); return nil },
|
||||||
|
func(_ context.Context, _ map[string]any) error { order = append(order, "b"); return nil },
|
||||||
|
}
|
||||||
|
if err := RunConfigHooks(context.Background(), nil, hooks...); err != nil {
|
||||||
|
t.Fatalf("RunConfigHooks: %v", err)
|
||||||
|
}
|
||||||
|
if len(order) != 2 || order[0] != "a" || order[1] != "b" {
|
||||||
|
t.Fatalf("hook order = %v, want [a b]", order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunConfigHooks_MutatesConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cfg := map[string]any{"a": "1"}
|
||||||
|
hook := func(_ context.Context, c map[string]any) error { c["a"] = "2"; return nil }
|
||||||
|
if err := RunConfigHooks(context.Background(), cfg, hook); err != nil {
|
||||||
|
t.Fatalf("RunConfigHooks: %v", err)
|
||||||
|
}
|
||||||
|
if cfg["a"] != "2" {
|
||||||
|
t.Fatalf("config a = %v, want 2", cfg["a"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunConfigHooks_StopsOnError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
hookErr := errors.New("sdk: hook error")
|
||||||
|
ranSecond := false
|
||||||
|
hooks := []ConfigHook{
|
||||||
|
func(_ context.Context, _ map[string]any) error { return hookErr },
|
||||||
|
func(_ context.Context, _ map[string]any) error { ranSecond = true; return nil },
|
||||||
|
}
|
||||||
|
if err := RunConfigHooks(context.Background(), nil, hooks...); !errors.Is(err, hookErr) {
|
||||||
|
t.Fatalf("err = %v, want hookErr", err)
|
||||||
|
}
|
||||||
|
if ranSecond {
|
||||||
|
t.Fatal("second hook ran after the first returned an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunConfigHooks_NilHookSkipped(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if err := RunConfigHooks(context.Background(), nil, nil, nil); err != nil {
|
||||||
|
t.Fatalf("RunConfigHooks(all nil): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunConfigHooks_ContextCanceled(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
ran := false
|
||||||
|
err := RunConfigHooks(ctx, nil, func(_ context.Context, _ map[string]any) error {
|
||||||
|
ran = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("err = %v, want context.Canceled", err)
|
||||||
|
}
|
||||||
|
if ran {
|
||||||
|
t.Fatal("hook ran on an already-canceled context")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
// Step is one unit of a Skill's reusable workflow.
|
||||||
|
type Step struct {
|
||||||
|
// Name is a short identifier for the step.
|
||||||
|
Name string
|
||||||
|
// Prompt is the instruction the model follows for this step.
|
||||||
|
Prompt string
|
||||||
|
// Depends lists the Names of steps that must finish before this step runs.
|
||||||
|
Depends []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ref is a reference a Skill may pull in for extra context or shared tooling.
|
||||||
|
type Ref struct {
|
||||||
|
// Name identifies the reference.
|
||||||
|
Name string
|
||||||
|
// Type is a free-form kind, for example "tool", "doc" or "file".
|
||||||
|
Type string
|
||||||
|
// Value is the reference payload or pointer.
|
||||||
|
Value string
|
||||||
|
// Optional reports whether the reference may be absent without failing.
|
||||||
|
Optional bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardrail is a constraint a Skill applies while running.
|
||||||
|
type Guardrail struct {
|
||||||
|
// ID names the guardrail.
|
||||||
|
ID string
|
||||||
|
// Message is the rule text enforced on the workflow.
|
||||||
|
Message string
|
||||||
|
// Enforced reports whether a violation aborts the Skill (true) or is only a
|
||||||
|
// warning (false).
|
||||||
|
Enforced bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill is a reusable, higher-order workflow: an ordered set of steps paired
|
||||||
|
// with prompts, references and guardrails. It is more structured than a Tool
|
||||||
|
// and typically composes several steps.
|
||||||
|
//
|
||||||
|
// Implementations must be safe for concurrent use.
|
||||||
|
type Skill interface {
|
||||||
|
// Name returns the unique skill identifier.
|
||||||
|
Name() string
|
||||||
|
// Description explains what the Skill accomplishes; the model uses it to
|
||||||
|
// select a Skill for a task.
|
||||||
|
Description() string
|
||||||
|
// Prompt returns the base system prompt for the Skill.
|
||||||
|
Prompt() string
|
||||||
|
// Steps returns the ordered workflow steps.
|
||||||
|
Steps() []Step
|
||||||
|
// Refs returns the references the Skill may draw on.
|
||||||
|
Refs() []Ref
|
||||||
|
// Guardrails returns the constraints the Skill enforces.
|
||||||
|
Guardrails() []Guardrail
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Tool is a callable function plus its input JSON Schema. Agents invoke tools
|
||||||
|
// directly by name; the dispatcher decodes the agent-provided arguments into
|
||||||
|
// args before calling Execute.
|
||||||
|
//
|
||||||
|
// Implementations must be safe for concurrent use, because Tool dispatch in the
|
||||||
|
// runtime may invoke them from multiple goroutines.
|
||||||
|
type Tool interface {
|
||||||
|
// Name returns the unique tool identifier the agent uses to invoke it.
|
||||||
|
Name() string
|
||||||
|
// Description explains what the tool does; the model uses it to select a
|
||||||
|
// tool for a task.
|
||||||
|
Description() string
|
||||||
|
// Parameters returns the tool input JSON Schema as a JSON object, for
|
||||||
|
// example {"type":"object","properties":{...}}. A nil map with a nil error
|
||||||
|
// means the tool accepts no arguments.
|
||||||
|
Parameters() (map[string]any, error)
|
||||||
|
// Execute runs the tool with args decoded from the caller's JSON input.
|
||||||
|
Execute(ctx context.Context, args any) (any, error)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user