Files
Agent-System-Backend/internal/runtime/hostbridge.go
weichen fc0a0505b3 Honor tool-call identity and real status in squad runner events
Why: F14 backend half - tool events used the tool name as the call id
and hardcoded status ok, so same-named tools could not be paired and
failures displayed as success; parallel callbacks shared artifact state.
Evidence: squad runner event tests pass (verify-u11c.json); pairing,
error mapping and legacy passthrough cases added; artifact list locked.
Impact: legacy empty-field events keep byte-identical output.
Refs: feature/agent-stack-audit-remediation-b2 U11c
2026-10-22 03:49:37 +00:00

203 lines
6.5 KiB
Go
Raw Permalink 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"
"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
}
}