Files
weichenandCursor e4020d297c
ci / vet-and-test (pull_request) Successful in 49s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add a script host, deferred tools, and background MCP connect
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>
2026-10-03 18:02:53 +00:00

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)
}