Why: Tool calls stayed invisible until execution finished, and turn, tool, timeout, and output caps stopped long tasks early. Evidence: go test ./internal/runtime/ ./internal/tools/ ./internal/prompts/ ./internal/squadadapter/ ./internal/runmgr/ ./internal/app/ ./internal/settings/ Impact: tool.call.delta and tool.output reach the client; default profile, subagents, and squad no longer cap turns or tool calls; exec timeout defaults to 600s with no hard ceiling. A profile or subagent maxTurns above 0 still applies. Refs: feature/stream-display-overhaul Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
4.3 KiB
Go
119 lines
4.3 KiB
Go
// 执行原语工具族(agent-native 交付,2026-10-01 用户定调):文档/表格/演示稿
|
||
// 等产物生成是 Agent 的能力——系统只提供原语与登记桥,生成方法沉淀在提示词库
|
||
// (base.deliverables 配方),不提供 write_docx 这类领域工具。
|
||
//
|
||
// 信任模型:本地单用户工作台(单二进制自托管)。run_shell/run_python 的脚本
|
||
// 一律先落盘为会话工作区内的文件再交给白名单解释器(bash/python3)执行,
|
||
// 工作目录锁定会话沙箱,超时与输出上限硬性截断;画像三清单与 runtime.exec
|
||
// 开关可禁用(fail-closed)。脚本落盘保留审计痕迹。
|
||
package tools
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
core "agent-system-core"
|
||
)
|
||
|
||
const (
|
||
execMaxOutput = 64 << 10 // 单次输出上限 64KB(头尾保留)
|
||
execDefaultTimeoutS = 600 // 未指定 timeout_sec 时的默认超时;模型可自行设更长,没有硬上限
|
||
execMaxArtifactBytes = 10 << 20 // register_artifact 登记上限 10MB
|
||
execScriptDir = ".agent" // 脚本落位(会话工作区内,可审计)
|
||
execSetting = "runtime.exec"
|
||
)
|
||
|
||
// execGate runtime.exec 总闸(默认开;false = 执行原语整体禁用,fail-closed)。
|
||
func execGate(rc ToolExecCtx) error {
|
||
if !rc.SettingBool(execSetting, true) {
|
||
return fmt.Errorf("exec tools: 已被设置禁用(%s=false)", execSetting)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// execSandbox 返回(并确保存在)会话沙箱目录。
|
||
func execSandbox(rc ToolExecCtx) (string, error) {
|
||
sandbox, err := fsSandbox(rc)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if err := os.MkdirAll(sandbox, 0o755); err != nil {
|
||
return "", fmt.Errorf("exec tools: create sandbox: %w", err)
|
||
}
|
||
return sandbox, nil
|
||
}
|
||
|
||
// RegisterExecTools 注册执行原语族的登记桥(register_artifact)。
|
||
// run_shell / run_python 在 exec_interpreter.go,由 RegisterInterpreterTools 同批注册
|
||
// (契约见 base.deliverables 提示词)。
|
||
func RegisterExecTools(r *Registry) error {
|
||
return r.Register(core.ToolDesc{
|
||
Name: "register_artifact", Source: core.SourceBuiltin,
|
||
Description: "把会话工作区内已生成的文件登记为交付物工件(docx/xlsx/pptx/pdf/图片/数据等):登记后文件出现在对话交付卡片与工件面板,可预览、下载。二进制文件不能用 fs_write_file(只收文本)——先由 run_python/run_shell 生成到工作区,再用本工具登记。",
|
||
Parameters: json.RawMessage(`{"type":"object","properties":{` +
|
||
`"path":{"type":"string","description":"工作区内相对路径(如 报告.docx)"}}` +
|
||
`,"required":["path"]}`),
|
||
}, execRegisterArtifact)
|
||
}
|
||
|
||
func execRegisterArtifact(rc ToolExecCtx, args json.RawMessage) (string, error) {
|
||
if err := execGate(rc); err != nil {
|
||
return "", err
|
||
}
|
||
var a struct {
|
||
Path string `json:"path"`
|
||
}
|
||
if len(args) > 0 {
|
||
if err := json.Unmarshal(args, &a); err != nil {
|
||
return "", fmt.Errorf("register_artifact: bad args: %w", err)
|
||
}
|
||
}
|
||
if strings.TrimSpace(a.Path) == "" {
|
||
return "", fmt.Errorf("register_artifact: path required")
|
||
}
|
||
sandbox, err := execSandbox(rc)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
abs, err := fsResolve(sandbox, a.Path)
|
||
if err != nil {
|
||
return "", fmt.Errorf("register_artifact: %w", err)
|
||
}
|
||
st, err := os.Stat(abs)
|
||
if err != nil {
|
||
return "", fmt.Errorf("register_artifact: %s: %v", a.Path, err)
|
||
}
|
||
if st.IsDir() {
|
||
return "", fmt.Errorf("register_artifact: %s 是目录(逐个文件登记)", a.Path)
|
||
}
|
||
if st.Size() > execMaxArtifactBytes {
|
||
return "", fmt.Errorf("register_artifact: 文件超过 10MB 上限(%d 字节)", st.Size())
|
||
}
|
||
b, err := os.ReadFile(abs)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
name := filepath.Base(abs)
|
||
mime := mimeOfExt(name)
|
||
art, err := rc.WriteArtifact(name, artifactKindOf(name, mime), mime, b)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return fmt.Sprintf("已登记交付物 %s(%s,%d 字节)——对话内会出现可预览的文件卡片", art.Name, art.ID, art.Size), nil
|
||
}
|
||
|
||
// artifactKindOf 按扩展名/mime 归类工件 kind(工件面板过滤与卡片图标用)。
|
||
func artifactKindOf(name, mime string) string {
|
||
switch {
|
||
case strings.HasPrefix(mime, "image/"):
|
||
return "image"
|
||
case strings.Contains(mime, "json") || strings.Contains(mime, "csv"):
|
||
return "data"
|
||
default:
|
||
return "document"
|
||
}
|
||
}
|