Why: models need a capped way to call several tools from one script, and a slow MCP server must not block the first prompt. Evidence: GOWORK=off go test ./runtime/codeorch/ ./runtime/tools/ ./runtime/mcp/ ./runtime/policy/ ./adapters/native/ Impact: run_tool_script stays unregistered until the caller turns it on. Deferred tools stay out of the model list. An unready MCP server connects in the background. Refs: feature/code-orchestration Co-authored-by: Cursor <cursoragent@cursor.com>
165 lines
4.2 KiB
Go
165 lines
4.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"agent-native/runtime/codeorch"
|
|
)
|
|
|
|
type searchTool struct{ reg *Registry }
|
|
|
|
func (t *searchTool) Name() string { return "tool_search" }
|
|
func (t *searchTool) Description() string {
|
|
return "按名称或描述查找延迟暴露的工具,并以文本返回它们的 schema"
|
|
}
|
|
func (t *searchTool) Parameters() json.RawMessage {
|
|
return json.RawMessage(`{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}`)
|
|
}
|
|
func (t *searchTool) Execute(_ context.Context, args json.RawMessage) (Result, error) {
|
|
var q struct {
|
|
Query string `json:"query"`
|
|
}
|
|
if len(args) > 0 {
|
|
_ = json.Unmarshal(args, &q)
|
|
}
|
|
return Result{Content: t.reg.Search(q.Query)}, nil
|
|
}
|
|
func (t *searchTool) Replay() string { return ReplaySafe }
|
|
|
|
type callTool struct{ reg *Registry }
|
|
|
|
func (t *callTool) Name() string { return "call_tool" }
|
|
func (t *callTool) Description() string {
|
|
return "调用一个延迟暴露的工具。参数 schema 固定,不随工具清单变化"
|
|
}
|
|
func (t *callTool) Parameters() json.RawMessage {
|
|
return json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"},"arguments":{"type":"object"}},"required":["name"]}`)
|
|
}
|
|
func (t *callTool) Execute(ctx context.Context, args json.RawMessage) (Result, error) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Arguments json.RawMessage `json:"arguments"`
|
|
}
|
|
if err := json.Unmarshal(args, &req); err != nil {
|
|
return Result{Content: err.Error(), IsError: true}, nil
|
|
}
|
|
if req.Name == "" || req.Name == "call_tool" || req.Name == "tool_search" || codeorch.Forbidden(req.Name) {
|
|
return Result{Content: "不能调用 " + req.Name, IsError: true}, nil
|
|
}
|
|
res := t.reg.Execute(ctx, req.Name, req.Arguments)
|
|
if !res.IsError {
|
|
t.reg.markActivated(req.Name)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (r *Registry) ensureMetaLocked() {
|
|
if _, ok := r.tools["tool_search"]; ok {
|
|
return
|
|
}
|
|
r.tools["tool_search"] = &searchTool{reg: r}
|
|
r.order = append(r.order, "tool_search")
|
|
r.tools["call_tool"] = &callTool{reg: r}
|
|
r.order = append(r.order, "call_tool")
|
|
}
|
|
|
|
// Search 把名称或描述命中的延迟工具 schema 写成文本。
|
|
func (r *Registry) Search(query string) string {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
q := strings.ToLower(strings.TrimSpace(query))
|
|
var b strings.Builder
|
|
for _, name := range r.order {
|
|
t := r.tools[name]
|
|
if ExposureOf(t) != ExposureDeferred {
|
|
continue
|
|
}
|
|
blob := strings.ToLower(t.Name() + " " + t.Description())
|
|
if q != "" && !strings.Contains(blob, q) {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "name: %s\ndescription: %s\nparameters: %s\n\n", t.Name(), t.Description(), t.Parameters())
|
|
}
|
|
if b.Len() == 0 {
|
|
return "没有匹配的延迟工具"
|
|
}
|
|
return strings.TrimSpace(b.String())
|
|
}
|
|
|
|
// SetActivationFile 指定已激活工具的持久文件。
|
|
func (r *Registry) SetActivationFile(path string) {
|
|
r.mu.Lock()
|
|
r.activatePath = path
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
// RestoreActivated 读回上次激活的延迟工具名。清单仍然不包含它们。
|
|
func (r *Registry) RestoreActivated() error {
|
|
r.mu.Lock()
|
|
path := r.activatePath
|
|
r.mu.Unlock()
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var names []string
|
|
if err := json.Unmarshal(raw, &names); err != nil {
|
|
return err
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.activated == nil {
|
|
r.activated = map[string]bool{}
|
|
}
|
|
for _, name := range names {
|
|
r.activated[name] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Activated 返回已恢复或本次调用过的延迟工具名。
|
|
func (r *Registry) Activated() []string {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
var out []string
|
|
for name := range r.activated {
|
|
out = append(out, name)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Registry) markActivated(name string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
t, ok := r.tools[name]
|
|
if !ok || ExposureOf(t) != ExposureDeferred {
|
|
return
|
|
}
|
|
if r.activated == nil {
|
|
r.activated = map[string]bool{}
|
|
}
|
|
r.activated[name] = true
|
|
if r.activatePath == "" {
|
|
return
|
|
}
|
|
var names []string
|
|
for n := range r.activated {
|
|
names = append(names, n)
|
|
}
|
|
raw, err := json.Marshal(names)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = os.WriteFile(r.activatePath, raw, 0o644)
|
|
}
|