diff --git a/examples/downstream-rdm/go.mod b/examples/downstream-rdm/go.mod index 4865a243..6a050575 100644 --- a/examples/downstream-rdm/go.mod +++ b/examples/downstream-rdm/go.mod @@ -2,6 +2,14 @@ module example.com/agent-system-plugin-rdm go 1.27 -require agent-system-backend v0.0.0 +require ( + agent-system-backend v0.0.0 + agent-system-core v0.0.0 +) +// 声明式路径(仅 pkg/extension 根包)只需下面两条 replace。 +// 空白导入 pkg/extension/link 拉起完整后端时,须复制基座 go.mod 的 +// 全部 8 条 deps replace——见 README 与 extension-compat-policy §4。 replace agent-system-backend => ../../modules/backend + +replace agent-system-core => ../../modules/backend/deps/agent-system-core diff --git a/examples/downstream-rdm/plugin.go b/examples/downstream-rdm/plugin.go index db286a89..19a89b07 100644 --- a/examples/downstream-rdm/plugin.go +++ b/examples/downstream-rdm/plugin.go @@ -1,12 +1,50 @@ package rdm -import "agent-system-backend/pkg/extension" +import ( + "context" + "encoding/json" + + "agent-system-backend/pkg/extension" +) func init() { extension.RegisterCompiled(extension.InProc{ - ID: "rdm", - Register: func(reg extension.Registry) error { - return reg.AddSkill("rdm-note", "# 研发记忆\n\n下游切片技能,不修改基座。\n") - }, + ID: "rdm", + Register: register, }) } + +// register 登记下游扩展:技能 + 进程内命令处理器(EXT-1 最小集示范)。 +func register(reg extension.Registry) error { + if err := reg.AddSkill("rdm-note", "# 研发记忆\n\n下游切片技能,不修改基座。\n"); err != nil { + return err + } + return reg.AddCommandHandler(json.RawMessage(`{ + "id": "rdm.ping", + "title": "连通性自检", + "description": "读工件元数据 + 写审计,验证 EXT-1 owner 作用域宿主句柄", + "paramsSchema": {"type": "object", "properties": {"artifactId": {"type": "string"}}} + }`), rdmPing) +} + +// rdmPing 经 owner 作用域宿主句柄:可选读工件元数据 → 写审计 → 返回回执。 +// 处理器自身不持有任何 owner 凭证,一切能力经 h 取用。 +func rdmPing(ctx context.Context, h extension.Host, req extension.Request) (any, error) { + artifact := "" + if id, _ := req.Params["artifactId"].(string); id != "" { + a, err := h.ArtifactGet(id) + if err != nil { + return nil, err + } + artifact = a.Name + } + if err := h.Audit("rdm.ping", req.ID, "{}"); err != nil { + return nil, err + } + return map[string]any{ + "pong": true, + "owner": h.Owner(), + "plugin": h.PluginID(), + "artifact": artifact, + }, nil +} diff --git a/examples/downstream-rdm/plugin_test.go b/examples/downstream-rdm/plugin_test.go index a76969de..f196d9ea 100644 --- a/examples/downstream-rdm/plugin_test.go +++ b/examples/downstream-rdm/plugin_test.go @@ -1,9 +1,13 @@ package rdm import ( + "context" + "encoding/json" + "errors" "testing" "agent-system-backend/pkg/extension" + core "agent-system-core" ) func TestCompiledSkillRegistration(t *testing.T) { @@ -17,3 +21,83 @@ func TestCompiledSkillRegistration(t *testing.T) { t.Fatal("surface version required") } } + +// fakeRegistry 记录 register() 的登记动作,不拉起基座。 +type fakeRegistry struct { + skills []string + commands []string + handlers int +} + +func (f *fakeRegistry) AddSkill(name, markdown string) error { + f.skills = append(f.skills, name) + return nil +} +func (f *fakeRegistry) AddCommand(spec json.RawMessage) error { return nil } +func (f *fakeRegistry) AddCommandHandler(spec json.RawMessage, h extension.Handler) error { + var s struct { + ID string `json:"id"` + } + if err := json.Unmarshal(spec, &s); err != nil { + return err + } + if h == nil { + return errors.New("nil handler") + } + f.commands = append(f.commands, s.ID) + f.handlers++ + return nil +} +func (f *fakeRegistry) Note(kind, name string, payload json.RawMessage) error { return nil } + +func TestRegisterBindsPingHandler(t *testing.T) { + reg := &fakeRegistry{} + if err := register(reg); err != nil { + t.Fatal(err) + } + if len(reg.skills) != 1 || reg.skills[0] != "rdm-note" { + t.Fatalf("skills: %+v", reg.skills) + } + if len(reg.commands) != 1 || reg.commands[0] != "rdm.ping" || reg.handlers != 1 { + t.Fatalf("commands: %+v handlers=%d", reg.commands, reg.handlers) + } +} + +// fakeHost 是下游测试里的最小句柄替身:只实现 rdm.ping 用到的能力。 +type fakeHost struct{ owner string } + +func (f fakeHost) PluginID() string { return "rdm" } +func (f fakeHost) Owner() string { return f.owner } +func (f fakeHost) KVPut(key, value string) error { return nil } +func (f fakeHost) KVGet(key string) (string, bool, error) { return "", false, nil } +func (f fakeHost) Audit(action, target, evidence string) error { return nil } +func (f fakeHost) Complete(ctx context.Context, prompt string) (string, error) { + return "", nil +} +func (f fakeHost) KnowledgeSearch(ctx context.Context, projectID core.ID, text string, topK int) ([]core.RetrievalHit, error) { + return nil, nil +} +func (f fakeHost) KnowledgeWrite(ctx context.Context, docs []core.KnowledgeDoc, embed bool) error { + return nil +} +func (f fakeHost) TaskCreate(kind, title, conversationID string) (string, error) { + return "task_1", nil +} +func (f fakeHost) TaskUpdate(taskID, status string, progress int, errText string) error { return nil } +func (f fakeHost) ArtifactGet(artifactID string) (core.Artifact, error) { + return core.Artifact{}, errors.New("not found") +} +func (f fakeHost) ArtifactContent(artifactID string) ([]byte, error) { return nil, nil } + +func TestRdmPingHandler(t *testing.T) { + out, err := rdmPing(context.Background(), fakeHost{owner: "alice"}, extension.Request{ + ID: "rdm.ping", Owner: "alice", Params: map[string]any{}, + }) + if err != nil { + t.Fatal(err) + } + m, _ := out.(map[string]any) + if m["pong"] != true || m["owner"] != "alice" || m["plugin"] != "rdm" { + t.Fatalf("payload: %+v", out) + } +} diff --git a/modules/backend b/modules/backend index 3f438b9e..cb26e4c9 160000 --- a/modules/backend +++ b/modules/backend @@ -1 +1 @@ -Subproject commit 3f438b9e8092fbdaefffa6b631aa3d4039550871 +Subproject commit cb26e4c9473f75ad015a83e3bba8137a08ff56f9 diff --git a/modules/documents b/modules/documents index f3f823ef..a47754b4 160000 --- a/modules/documents +++ b/modules/documents @@ -1 +1 @@ -Subproject commit f3f823ef8d68045350039e1fbffd1bf8ed591093 +Subproject commit a47754b4944ffab2f09db80d8f3272324f5ef4bf