// Package tools 实现统一工具注册表(4.8):内置、声明式 HTTP、插件工具同一形态; // 来源标记(builtin/plugin/user)、启用开关、确认策略由画像三清单决定(runmgr 执行)。 package tools import ( "encoding/json" "fmt" "sort" "strings" "sync" "time" core "agent-system-core" ) // Executor 是无状态执行器:入参已校验,返回输出文本或错误。 type Executor func(rc ToolExecCtx, args json.RawMessage) (string, error) // ToolExecCtx 供工具访问受控环境(当前项目/检索/工件写入)。 type ToolExecCtx interface { ProjectID() string ConversationID() string Retrieve(q core.RetrievalQuery) ([]core.RetrievalHit, error) WriteArtifact(name, kind, mime string, content []byte) (core.Artifact, error) } type tool struct { desc core.ToolDesc exec Executor } type Registry struct { mu sync.RWMutex tools map[string]tool } func NewRegistry() *Registry { return &Registry{tools: map[string]tool{}} } // Register 幂等注册(后注册覆盖同名;来源标记随之更新)。 // 声明式 HTTP 工具经 HTTPExecutor(spec, lookup) 绑定执行器。 func (r *Registry) Register(desc core.ToolDesc, exec Executor) error { if desc.Name == "" { return fmt.Errorf("tools: empty name") } if exec == nil { return fmt.Errorf("tools: %s has no executor", desc.Name) } r.mu.Lock() r.tools[desc.Name] = tool{desc: desc, exec: exec} r.mu.Unlock() return nil } func (r *Registry) UnregisterBySource(source core.SourceKind, pluginID string) { r.mu.Lock() for name, t := range r.tools { if t.desc.Source == source && (pluginID == "" || t.desc.PluginID == pluginID) { delete(r.tools, name) } } r.mu.Unlock() } func (r *Registry) List() []core.ToolDesc { r.mu.RLock() defer r.mu.RUnlock() out := make([]core.ToolDesc, 0, len(r.tools)) for _, t := range r.tools { out = append(out, t.desc) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out } func (r *Registry) Get(name string) (core.ToolDesc, Executor, bool) { r.mu.RLock() t, ok := r.tools[name] r.mu.RUnlock() return t.desc, t.exec, ok } // Execute 校验存在性并调用执行器;策略(allow/deny/confirm)与截断由 runmgr 负责。 func (r *Registry) Execute(rc ToolExecCtx, call core.ToolCallRequest) core.ToolCallResult { desc, exec, ok := r.Get(call.Name) if !ok { return core.ToolCallResult{CallID: call.CallID, Status: "error", Output: fmt.Sprintf("tool %q not found (registry miss)", call.Name)} } out, err := exec(rc, call.Arguments) if err != nil { return core.ToolCallResult{CallID: call.CallID, Status: "error", Output: err.Error()} } _ = desc return core.ToolCallResult{CallID: call.CallID, Status: "ok", Output: out, Truncated: false, Artifact: nil, } } // ---- 内置最小工具集(一切可禁用:画像三清单控制)---- // RegisterBuiltin 注册骨架期内置工具:时间、会话历史检索、知识检索、读文件、写工件。 func (r *Registry) RegisterBuiltin() error { if err := r.Register(core.ToolDesc{ Name: "time_now", Description: "获取当前时间(RFC3339)与时区", Source: core.SourceBuiltin, Parameters: json.RawMessage(`{"type":"object","properties":{}}`), }, func(rc ToolExecCtx, args json.RawMessage) (string, error) { return time.Now().Format(time.RFC3339), nil }); err != nil { return err } if err := r.Register(core.ToolDesc{ Name: "knowledge_search", Description: "在项目知识与插件知识库中做全文检索,返回命中片段", Source: core.SourceBuiltin, Parameters: json.RawMessage(`{"type":"object","properties":{"query":{"type":"string","description":"检索词"}},"required":["query"]}`), }, func(rc ToolExecCtx, args json.RawMessage) (string, error) { var a struct{ Query string `json:"query"`; TopK int `json:"topK"` } if len(args) > 0 { if err := json.Unmarshal(args, &a); err != nil { return "", fmt.Errorf("knowledge_search: bad args: %w", err) } } if a.Query == "" { return "", fmt.Errorf("knowledge_search: query is required") } hits, err := rc.Retrieve(core.RetrievalQuery{ProjectID: rc.ProjectID(), Text: a.Query, TopK: a.TopK}) if err != nil { return "", err } if len(hits) == 0 { return "(无命中)", nil } var sb strings.Builder for i, h := range hits { fmt.Fprintf(&sb, "[%d] %s(来源 %s,得分 %.2f)\n%s\n\n", i+1, h.Doc.Title, h.Doc.Source, h.Score, h.Doc.Chunk) } return sb.String(), nil }); err != nil { return err } if err := r.Register(core.ToolDesc{ Name: "write_artifact", Description: "把生成内容写入工件(文档/数据),返回工件 ID 与名称", Source: core.SourceBuiltin, Parameters: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"},"kind":{"type":"string","enum":["document","data"]},"content":{"type":"string"}},"required":["name","content"]}`), }, func(rc ToolExecCtx, args json.RawMessage) (string, error) { var a struct { Name string `json:"name"` Kind string `json:"kind"` Content string `json:"content"` } if err := json.Unmarshal(args, &a); err != nil { return "", err } if a.Name == "" { return "", fmt.Errorf("write_artifact: name required") } kind := a.Kind if kind == "" { kind = "document" } mime := "text/markdown" if kind == "data" { mime = "application/json" } art, err := rc.WriteArtifact(a.Name, kind, mime, []byte(a.Content)) if err != nil { return "", err } return fmt.Sprintf("已写入工件 %s(%s,%d 字节)", art.Name, art.ID, art.Size), nil }); err != nil { return err } return nil }