package runtime import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "agent-native/adapters/native" "agent-native/runtime/tools" core "agent-system-core" ) // kernelToolNames 是内核工作区自带的工具。同名宿主工具不注入,避免步骤因冲突失败。 var kernelToolNames = map[string]bool{ "read_file": true, "write_file": true, "edit_file": true, "multi_edit": true, "list_dir": true, "grep": true, "glob": true, "load_skill": true, "run_python": true, "run_command": true, "web_search": true, "tavily_search": true, "fetch_url": true, "delegate_task": true, } type producedSink interface { add(ref *core.ArtifactRef) } // artifactList 是本次运行已登记的工件表。宿主工具可能被内核并行调用, // 因此写入与读取都在同一把锁下:并发调用既不丢条目也不读到半成品。 type artifactList struct { mu sync.Mutex items []*core.ArtifactRef } func (a *artifactList) add(ref *core.ArtifactRef) { if ref == nil { return } a.mu.Lock() defer a.mu.Unlock() a.items = append(a.items, ref) } // snapshot 返回当前工件的副本,供终局检查与部件装配使用。 func (a *artifactList) snapshot() []*core.ArtifactRef { if a == nil { return nil } a.mu.Lock() defer a.mu.Unlock() return append([]*core.ArtifactRef(nil), a.items...) } // bridgeHostTools 把画像允许的产品工具包成内核宿主工具。 // register_artifact 会先把内核步骤目录里的文件复制到会话工作区。 func bridgeHostTools(rc RunCtx, produced *artifactList) []tools.Tool { if produced == nil { produced = &artifactList{} } policy := rc.Profile().Tools var out []tools.Tool for _, desc := range rc.Tools() { if desc.Hidden || kernelToolNames[desc.Name] || !policy.ToolAllowed(desc.Name) { continue } out = append(out, &bridgedTool{rc: rc, desc: desc, produced: produced}) } if rc.Settings().GetBool("ui.cardflow", false) { for _, desc := range cardToolDescs() { if policy.ToolAllowed(desc.Name) { out = append(out, &bridgedTool{rc: rc, desc: desc, produced: produced, card: true}) } } } return out } type bridgedTool struct { rc RunCtx desc core.ToolDesc produced *artifactList card bool } func (b *bridgedTool) Name() string { return b.desc.Name } func (b *bridgedTool) Description() string { return b.desc.Description } func (b *bridgedTool) Parameters() json.RawMessage { return b.desc.Parameters } func (b *bridgedTool) Execute(ctx context.Context, args json.RawMessage) (tools.Result, error) { if b.desc.Name == "register_artifact" { stageStepFile(ctx, conversationSandbox(b.rc), args) } if b.card { out, err := executeCardTool(b.rc, b.desc.Name, args) if err != nil { return tools.Result{Content: err.Error(), IsError: true}, nil } return tools.Result{Content: out}, nil } res := b.rc.ExecuteTool(core.ToolCallRequest{Name: b.desc.Name, Arguments: args}) if res.Artifact != nil { b.produced.add(&core.ArtifactRef{ArtifactID: res.Artifact.ID, Name: res.Artifact.Name, Kind: res.Artifact.Kind}) } if res.Status != "ok" { return tools.Result{Content: res.Output, IsError: true}, nil } return tools.Result{Content: res.Output}, nil } func conversationSandbox(rc RunCtx) string { s, ok := rc.(interface{ ConversationSandbox() string }) if !ok { return "" } return s.ConversationSandbox() } func stageStepFile(ctx context.Context, sandbox string, args json.RawMessage) { step := native.WorkDir(ctx) if step == "" || sandbox == "" { return } var a struct { Path string `json:"path"` } if json.Unmarshal(args, &a) != nil || strings.TrimSpace(a.Path) == "" || strings.Contains(a.Path, "..") { return } src := filepath.Join(step, filepath.FromSlash(a.Path)) dst := filepath.Join(sandbox, filepath.FromSlash(a.Path)) if _, err := os.Stat(dst); err == nil { return } body, err := os.ReadFile(src) if err != nil { return } if os.MkdirAll(filepath.Dir(dst), 0o755) != nil { return } _ = os.WriteFile(dst, body, 0o644) } func cardToolDescs() []core.ToolDesc { return []core.ToolDesc{ {Name: toolCard, Description: "提交产物卡片", Parameters: json.RawMessage(`{"type":"object","properties":{"title":{"type":"string"},"summary":{"type":"string"},"body":{"type":"string"},"scope":{"type":"string"}},"required":["title","body"]}`)}, {Name: toolCardBegin, Description: "开始流式产物卡", Parameters: json.RawMessage(`{"type":"object","properties":{"title":{"type":"string"},"summary":{"type":"string"},"scope":{"type":"string"}},"required":["title"]}`)}, {Name: toolCardAppend, Description: "追加流式产物卡", Parameters: json.RawMessage(`{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}`)}, {Name: toolCardCommit, Description: "收尾流式产物卡", Parameters: json.RawMessage(`{"type":"object","properties":{}}`)}, } } func executeCardTool(rc RunCtx, name string, args json.RawMessage) (string, error) { switch name { case toolCard: var a struct{ Title, Summary, Body, Scope string } if err := json.Unmarshal(args, &a); err != nil || strings.TrimSpace(a.Title) == "" || strings.TrimSpace(a.Body) == "" { return "", fmt.Errorf("submit_card 参数错误") } card := core.ActivityCard{Kind: core.CardKindResult, Title: a.Title, Status: core.CardDone, Tone: "ok", Body: []core.UINode{{Type: "markdown", Markdown: a.Body}}} if a.Scope == "project" { card.ProjectID = core.ID(rc.Run().ProjectID) } created, err := rc.SubmitCard(card) if err != nil { return "", err } return "已作为卡片发出(" + created.Title + ")", nil case toolCardBegin: var a struct{ Title, Summary, Scope string } if err := json.Unmarshal(args, &a); err != nil || strings.TrimSpace(a.Title) == "" { return "", fmt.Errorf("card_begin 参数错误") } card := core.ActivityCard{Kind: core.CardKindResult, Title: a.Title, Status: core.CardActive, Tone: "ok"} if a.Scope == "project" { card.ProjectID = core.ID(rc.Run().ProjectID) } created, err := rc.BeginStreamCard(card) if err != nil { return "", err } return "流式卡已开始(" + created.Title + ")", nil case toolCardAppend: var a struct{ Text string } if err := json.Unmarshal(args, &a); err != nil { return "", err } if err := rc.AppendStreamCard(a.Text); err != nil { return "", err } return "已追加", nil case toolCardCommit: card, err := rc.CommitStreamCard() if err != nil { return "", err } return "已作为卡片发出(" + card.Title + ")", nil default: return "", nil } }