Files
Agent-System-Backend/internal/runtime/hostbridge.go
T
weichenandCursor 268f4a9abf Route conversations through a squad entry when selected
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>
2026-10-01 09:34:53 +00:00

186 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}