Why: Squad steps previously ran without tools, and the product had no way to make the squad the conversation entry without losing streaming, cards, or the deliverable guard. Evidence: go test ./... -count=1 passed, including streaming, cardflow, false-deliverable rejection, tool allowlist, model routing, usage, cancel, and turn and tool budgets. Impact: runtime.entry defaults to direct. squad uses one kernel step for simple work and the leader for the rest. squad_orchestrate is hidden in that mode. The default is not switched. Refs: deliverable-quality-system B3-B5 Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
6.0 KiB
Go
186 lines
6.0 KiB
Go
package runtime
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"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)
|
||
}
|
||
|
||
type artifactList struct {
|
||
items []*core.ArtifactRef
|
||
}
|
||
|
||
func (a *artifactList) add(ref *core.ArtifactRef) {
|
||
if ref != nil {
|
||
a.items = append(a.items, ref)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|