Run tool scripts through the shared host and keep orchestration off #60
Vendored
+1
-1
Submodule deps/agent-native updated: fb2b3b796c...e4020d297c
Vendored
+1
-1
Submodule deps/agent-squad updated: a3b49e7b1f...dcd523ea05
+29
-4
@@ -107,6 +107,9 @@ func Run(opt Options) error {
|
||||
if err := tools.RegisterInterpreterTools(reg); err != nil {
|
||||
return fmt.Errorf("interpreter tools: %w", err)
|
||||
}
|
||||
if err := tools.RegisterCodeTool(reg); err != nil {
|
||||
return fmt.Errorf("code orchestration tool: %w", err)
|
||||
}
|
||||
setSvc := settings.NewService(st)
|
||||
setSvc.SetSystemOverride(map[string]any{"storage": map[string]any{"dataDir": opt.DataDir}})
|
||||
// 网络搜索(Tavily):配置密钥池才注册(对齐 agent-native 语义:未配置不静默降级)。
|
||||
@@ -188,9 +191,26 @@ func Run(opt Options) error {
|
||||
if pluginHost != nil {
|
||||
rawMCP = mergeMCPServers(rawMCP, pluginHost.MCPServers())
|
||||
}
|
||||
if mgr := mcpManagerFor(rawMCP); mgr != nil {
|
||||
if mgr := mcpManagerFor(rawMCP, strings.TrimSpace(effNow.GetString("tools.mcp.overrides", ""))); mgr != nil {
|
||||
cfg.MCP = mgr
|
||||
}
|
||||
cfg.CodeOrchestration = effNow.GetBool("runtime.codeOrchestration", false)
|
||||
if ask, ok := rc.(interface {
|
||||
AskUser(card core.HITLCard) (core.HITLResponse, error)
|
||||
}); ok {
|
||||
cfg.Ask = func(_ context.Context, question string) (string, error) {
|
||||
resp, err := ask.AskUser(core.HITLCard{
|
||||
Question: question, Kind: core.HITLKindInput, AllowFreeInput: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(resp.FreeText) != "" {
|
||||
return strings.TrimSpace(resp.FreeText), nil
|
||||
}
|
||||
return resp.ChoiceID, nil
|
||||
}
|
||||
}
|
||||
cfg.Trace = traceFn
|
||||
if host, ok := rc.(interface {
|
||||
OpenCheckpoints() (*checkpoint.Log, error)
|
||||
@@ -524,9 +544,10 @@ var (
|
||||
mcpManager *agentmcp.Manager
|
||||
)
|
||||
|
||||
func mcpManagerFor(raw string) *agentmcp.Manager {
|
||||
func mcpManagerFor(raw, overrides string) *agentmcp.Manager {
|
||||
mcpMu.Lock()
|
||||
defer mcpMu.Unlock()
|
||||
key := raw + "\n" + overrides
|
||||
if raw == "" {
|
||||
if mcpManager != nil {
|
||||
mcpManager.Close()
|
||||
@@ -535,7 +556,7 @@ func mcpManagerFor(raw string) *agentmcp.Manager {
|
||||
mcpKey = ""
|
||||
return nil
|
||||
}
|
||||
if raw == mcpKey && mcpManager != nil {
|
||||
if key == mcpKey && mcpManager != nil {
|
||||
return mcpManager
|
||||
}
|
||||
var servers []agentmcp.ServerConfig
|
||||
@@ -548,10 +569,14 @@ func mcpManagerFor(raw string) *agentmcp.Manager {
|
||||
log.Printf("squad agent-native: MCP config invalid, MCP disabled: %v", err)
|
||||
return nil
|
||||
}
|
||||
if err := mgr.ApplyOverrides(overrides); err != nil {
|
||||
log.Printf("squad agent-native: MCP overrides invalid, MCP disabled: %v", err)
|
||||
return nil
|
||||
}
|
||||
if mcpManager != nil {
|
||||
mcpManager.Close()
|
||||
}
|
||||
mcpKey, mcpManager = raw, mgr
|
||||
mcpKey, mcpManager = key, mgr
|
||||
return mgr
|
||||
}
|
||||
|
||||
|
||||
@@ -21,24 +21,24 @@ func TestMcpManagerForCacheAndRebuild(t *testing.T) {
|
||||
cfgA := `[{"name":"a","command":"/bin/true"}]`
|
||||
cfgB := `[{"name":"b","command":"/bin/true"}]`
|
||||
|
||||
m1 := mcpManagerFor(cfgA)
|
||||
m1 := mcpManagerFor(cfgA, "")
|
||||
if m1 == nil {
|
||||
t.Fatal("valid config must build a manager")
|
||||
}
|
||||
if m2 := mcpManagerFor(cfgA); m2 != m1 {
|
||||
if m2 := mcpManagerFor(cfgA, ""); m2 != m1 {
|
||||
t.Fatal("same config must reuse the cached manager")
|
||||
}
|
||||
m3 := mcpManagerFor(cfgB)
|
||||
m3 := mcpManagerFor(cfgB, "")
|
||||
if m3 == nil || m3 == m1 {
|
||||
t.Fatal("changed config must rebuild a fresh manager")
|
||||
}
|
||||
if mcpManagerFor("") != nil {
|
||||
if mcpManagerFor("", "") != nil {
|
||||
t.Fatal("empty config disables MCP and clears the cache")
|
||||
}
|
||||
if mcpManagerFor(`{bad json`) != nil {
|
||||
if mcpManagerFor(`{bad json`, "") != nil {
|
||||
t.Fatal("invalid JSON must disable MCP (nil), not panic")
|
||||
}
|
||||
if mcpManagerFor(`[{"name":"bad name","command":"x"}]`) != nil {
|
||||
if mcpManagerFor(`[{"name":"bad name","command":"x"}]`, "") != nil {
|
||||
t.Fatal("invalid server config must disable MCP (nil)")
|
||||
}
|
||||
// 并发调用不死锁、返回同一实例或各自重建(缓存一致性由锁保证)。
|
||||
@@ -47,7 +47,7 @@ func TestMcpManagerForCacheAndRebuild(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = mcpManagerFor(cfgB)
|
||||
_ = mcpManagerFor(cfgB, "")
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -66,7 +66,7 @@ func TestMcpCacheConcurrencyWithSettingsResolution(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
_ = mcpManagerFor(cfgs[i%len(cfgs)])
|
||||
_ = mcpManagerFor(cfgs[i%len(cfgs)], "")
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
@@ -290,10 +290,14 @@ func (c *runCtx) Tools() []core.ToolDesc {
|
||||
list := c.mgr.registry.List()
|
||||
out := make([]core.ToolDesc, 0, len(list))
|
||||
fs := c.eff.GetBool("runtime.fsTools", false)
|
||||
code := c.eff.GetBool("runtime.codeOrchestration", false)
|
||||
for _, t := range list {
|
||||
if t.Hidden && !fs {
|
||||
continue
|
||||
}
|
||||
if t.Name == "run_tool_script" && !code {
|
||||
continue
|
||||
}
|
||||
readOnly := t.ReadOnly || agentpolicy.TraitsFor(t.Name).ReadOnly
|
||||
if !agentpolicy.Visible(c.permissionMode, t.Name, readOnly) {
|
||||
continue
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
"""工作流脚本回调。用法:import agentwf as wf。
|
||||
|
||||
请求写到 fd 3,响应从 fd 4 按行读回。宿主一次只处理一条。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
_seq = 0
|
||||
_out = os.fdopen(3, "w", encoding="utf-8", buffering=1)
|
||||
_inp = os.fdopen(4, "r", encoding="utf-8", buffering=1)
|
||||
|
||||
|
||||
def _rpc(op, **fields):
|
||||
global _seq
|
||||
_seq += 1
|
||||
msg = {"id": str(_seq), "op": op}
|
||||
msg.update(fields)
|
||||
_out.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
||||
_out.flush()
|
||||
line = _inp.readline()
|
||||
if not line:
|
||||
raise RuntimeError("workflow host closed")
|
||||
resp = json.loads(line)
|
||||
if not resp.get("ok"):
|
||||
raise RuntimeError(resp.get("error") or "workflow call failed")
|
||||
return resp.get("output", "")
|
||||
|
||||
|
||||
def tool(name, args=None):
|
||||
return _rpc("tool.call", name=name, args=args or {})
|
||||
|
||||
|
||||
def ask(question, choices=None):
|
||||
return _rpc("hitl.ask", question=question, choices=choices or [])
|
||||
|
||||
|
||||
def agent(name, task):
|
||||
return _rpc("agent.spawn", agent=name, task=task)
|
||||
|
||||
|
||||
def artifact(name, content):
|
||||
return _rpc("artifact.register", name=name, content=content)
|
||||
|
||||
|
||||
def card(title, kind="result"):
|
||||
return _rpc("card.create", title=title, kind=kind)
|
||||
|
||||
|
||||
def step(step_id, title, status):
|
||||
return _rpc("step", stepId=step_id, title=title, status=status)
|
||||
@@ -1,27 +1,21 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"agent-native/runtime/codeorch"
|
||||
"agent-system-backend/internal/tools"
|
||||
"agent-system-backend/internal/workflow"
|
||||
core "agent-system-core"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed agentwf.py
|
||||
var agentwfSource string
|
||||
|
||||
const (
|
||||
scriptDefaultTimeout = 600 * time.Second
|
||||
scriptMaxTimeout = 1800 * time.Second
|
||||
@@ -29,28 +23,6 @@ const (
|
||||
scriptMaxLine = 1 << 20
|
||||
)
|
||||
|
||||
var (
|
||||
agentwfOnce sync.Once
|
||||
agentwfDir string
|
||||
agentwfErr error
|
||||
)
|
||||
|
||||
func agentwfHelperDir() (string, error) {
|
||||
agentwfOnce.Do(func() {
|
||||
dir, err := os.MkdirTemp("", "agentwf-*")
|
||||
if err != nil {
|
||||
agentwfErr = err
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "agentwf.py"), []byte(agentwfSource), 0o644); err != nil {
|
||||
agentwfErr = err
|
||||
return
|
||||
}
|
||||
agentwfDir = dir
|
||||
})
|
||||
return agentwfDir, agentwfErr
|
||||
}
|
||||
|
||||
func scriptsEnabled(rc RunCtx) bool {
|
||||
return rc.Settings().GetBool("runtime.workflowScripts", false)
|
||||
}
|
||||
@@ -176,176 +148,33 @@ func runScriptFile(rc RunCtx, runner WorkflowRunner, path string) (string, []*co
|
||||
if timeout <= 0 {
|
||||
timeout = scriptDefaultTimeout
|
||||
}
|
||||
if timeout > scriptMaxTimeout {
|
||||
timeout = scriptMaxTimeout
|
||||
}
|
||||
helper, err := agentwfHelperDir()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
parent := rc.Context()
|
||||
if parent == nil {
|
||||
parent = context.Background()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parent, timeout)
|
||||
defer cancel()
|
||||
cmd, err := tools.ScriptCommand(ctx, sandbox, path)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
cmd.Env = prependPythonPath(cmd.Env, helper)
|
||||
reqR, reqW, err := os.Pipe()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
respR, respW, err := os.Pipe()
|
||||
if err != nil {
|
||||
reqR.Close()
|
||||
reqW.Close()
|
||||
return "", nil, err
|
||||
}
|
||||
cmd.ExtraFiles = []*os.File{reqW, respR}
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
reqR.Close()
|
||||
reqW.Close()
|
||||
respR.Close()
|
||||
respW.Close()
|
||||
return "", nil, err
|
||||
}
|
||||
reqW.Close()
|
||||
respR.Close()
|
||||
defer reqR.Close()
|
||||
defer respW.Close()
|
||||
arts, callErr := serveScript(ctx, rc, reqR, respW)
|
||||
if callErr != nil {
|
||||
cancel()
|
||||
}
|
||||
waitErr := cmd.Wait()
|
||||
text := clipScriptOutput(&stdout, &stderr)
|
||||
if callErr != nil {
|
||||
return text, arts, callErr
|
||||
}
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return text, arts, fmt.Errorf("workflow: 脚本超时")
|
||||
}
|
||||
if ctx.Err() == context.Canceled {
|
||||
return text, arts, fmt.Errorf("workflow: 脚本已取消")
|
||||
}
|
||||
if waitErr != nil {
|
||||
return text, arts, fmt.Errorf("workflow: 脚本退出:%v\n%s", waitErr, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return text, arts, nil
|
||||
}
|
||||
|
||||
func clipScriptOutput(stdout, stderr *bytes.Buffer) string {
|
||||
text := strings.TrimSpace(stdout.String())
|
||||
errText := strings.TrimSpace(stderr.String())
|
||||
if errText != "" {
|
||||
if text != "" {
|
||||
text += "\n"
|
||||
}
|
||||
text += errText
|
||||
}
|
||||
if len(text) > 16000 {
|
||||
text = text[:16000] + "…(超长截断)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func prependPythonPath(env []string, dir string) []string {
|
||||
for i, kv := range env {
|
||||
if strings.HasPrefix(kv, "PYTHONPATH=") {
|
||||
env[i] = "PYTHONPATH=" + dir + string(os.PathListSeparator) + strings.TrimPrefix(kv, "PYTHONPATH=")
|
||||
return env
|
||||
}
|
||||
}
|
||||
return append(env, "PYTHONPATH="+dir)
|
||||
}
|
||||
|
||||
type scriptRequest struct {
|
||||
ID string `json:"id"`
|
||||
Op string `json:"op"`
|
||||
Name string `json:"name"`
|
||||
Args json.RawMessage `json:"args"`
|
||||
Question string `json:"question"`
|
||||
Choices []core.HITLChoice `json:"choices"`
|
||||
Agent string `json:"agent"`
|
||||
Task string `json:"task"`
|
||||
Content string `json:"content"`
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"`
|
||||
Status string `json:"status"`
|
||||
StepID string `json:"stepId"`
|
||||
}
|
||||
|
||||
type scriptResponse struct {
|
||||
ID string `json:"id"`
|
||||
OK bool `json:"ok"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func serveScript(ctx context.Context, rc RunCtx, reqR, respW *os.File) ([]*core.ArtifactRef, error) {
|
||||
sc := bufio.NewScanner(reqR)
|
||||
sc.Buffer(make([]byte, 64*1024), scriptMaxLine)
|
||||
var arts []*core.ArtifactRef
|
||||
calls := 0
|
||||
for sc.Scan() {
|
||||
line := strings.TrimSpace(sc.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
calls++
|
||||
var req scriptRequest
|
||||
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
||||
_ = writeScriptResponse(respW, scriptResponse{OK: false, Error: "请求不是合法 JSON"})
|
||||
continue
|
||||
}
|
||||
if calls > scriptMaxCalls {
|
||||
_ = writeScriptResponse(respW, scriptResponse{ID: req.ID, OK: false, Error: "脚本回调超过 50 次"})
|
||||
return arts, fmt.Errorf("workflow: 脚本回调超过 50 次")
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return arts, ctx.Err()
|
||||
}
|
||||
output, got, err := dispatchScript(rc, req)
|
||||
out := codeorch.RunFile(parent, sandbox, path, codeorch.Limits{
|
||||
Timeout: timeout,
|
||||
MaxTimeout: scriptMaxTimeout,
|
||||
MaxCalls: scriptMaxCalls,
|
||||
MaxResultBytes: scriptMaxLine,
|
||||
MaxModelChars: 16000,
|
||||
FullDir: sandbox,
|
||||
}, func(ctx context.Context, dir, scriptPath string) (*exec.Cmd, error) {
|
||||
return tools.ScriptCommand(ctx, dir, scriptPath)
|
||||
}, func(_ context.Context, call codeorch.Call) (string, error) {
|
||||
output, got, err := dispatchScript(rc, call)
|
||||
arts = append(arts, got...)
|
||||
if err != nil {
|
||||
_ = writeScriptResponse(respW, scriptResponse{ID: req.ID, OK: false, Error: err.Error()})
|
||||
if ctx.Err() != nil {
|
||||
return arts, ctx.Err()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := writeScriptResponse(respW, scriptResponse{ID: req.ID, OK: true, Output: output}); err != nil {
|
||||
return arts, err
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
if err == bufio.ErrTooLong {
|
||||
return arts, fmt.Errorf("workflow: 单行请求超过 1MB")
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return arts, ctx.Err()
|
||||
}
|
||||
return arts, err
|
||||
}
|
||||
return arts, nil
|
||||
return output, err
|
||||
})
|
||||
return out.ModelText(), arts, out.Err
|
||||
}
|
||||
|
||||
func writeScriptResponse(w io.Writer, resp scriptResponse) error {
|
||||
raw, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
func dispatchScript(rc RunCtx, req codeorch.Call) (string, []*core.ArtifactRef, error) {
|
||||
var choices []core.HITLChoice
|
||||
if len(req.Choices) > 0 {
|
||||
_ = json.Unmarshal(req.Choices, &choices)
|
||||
}
|
||||
_, err = w.Write(append(raw, '\n'))
|
||||
return err
|
||||
}
|
||||
|
||||
func dispatchScript(rc RunCtx, req scriptRequest) (string, []*core.ArtifactRef, error) {
|
||||
switch req.Op {
|
||||
case "tool.call":
|
||||
if _, denied := workflow.DeniedTools[req.Name]; denied {
|
||||
@@ -363,10 +192,10 @@ func dispatchScript(rc RunCtx, req scriptRequest) (string, []*core.ArtifactRef,
|
||||
return res.Output, arts, nil
|
||||
case "hitl.ask":
|
||||
card := core.HITLCard{Question: req.Question, Kind: core.HITLKindInput, AllowFreeInput: true}
|
||||
if len(req.Choices) > 0 {
|
||||
if len(choices) > 0 {
|
||||
card.Kind = core.HITLKindChoice
|
||||
card.AllowFreeInput = false
|
||||
card.Choices = req.Choices
|
||||
card.Choices = choices
|
||||
for i := range card.Choices {
|
||||
if card.Choices[i].Impact == "" {
|
||||
card.Choices[i].Impact = "会按这个选择继续"
|
||||
|
||||
@@ -31,13 +31,14 @@ func SystemDefaults() map[string]any {
|
||||
"tavily": map[string]any{"apiKeys": "", "endpoint": ""}, // 网络搜索(逗号分隔密钥池;空 = 不注册搜索工具)
|
||||
},
|
||||
"runtime": map[string]any{
|
||||
"mock": false, // true = 全部会话走 mock runner(演示/测试)
|
||||
"squad": true, // 小队编排(Agent-Squad 引擎)可用
|
||||
"entry": "direct", // direct = 现有对话循环;squad = 小队分诊入口(灰度,达标后再改默认)
|
||||
"rateLimitPerMin": 0, // 租户限流(次/分;0 = 不限)
|
||||
"redisAddr": "", // 限流共享后端(原生 Redis addr;空 = 进程内)
|
||||
"verify": true, // 终轮确定性检查与独立验证
|
||||
"llmRetries": 2, // 直连模型在尚未输出时的额外重试次数
|
||||
"mock": false, // true = 全部会话走 mock runner(演示/测试)
|
||||
"squad": true, // 小队编排(Agent-Squad 引擎)可用
|
||||
"entry": "direct", // direct = 现有对话循环;squad = 小队分诊入口(灰度,达标后再改默认)
|
||||
"rateLimitPerMin": 0, // 租户限流(次/分;0 = 不限)
|
||||
"redisAddr": "", // 限流共享后端(原生 Redis addr;空 = 进程内)
|
||||
"verify": true, // 终轮确定性检查与独立验证
|
||||
"llmRetries": 2, // 直连模型在尚未输出时的额外重试次数
|
||||
"codeOrchestration": false, // 模型用脚本批量调用工具;默认关
|
||||
},
|
||||
"storage": map[string]any{
|
||||
"dataDir": "./data", // 启动注入实际值(--data)
|
||||
@@ -85,6 +86,7 @@ func Schema() []core.SettingDesc {
|
||||
{Key: "runtime.redisAddr", Group: "runtime", Label: "Redis 地址(限流共享)", Type: core.SettingString, Default: "", Effect: core.EffectRestart, Description: "原生 Redis(如 127.0.0.1:6379);空 = 进程内限流(单实例)"},
|
||||
{Key: "runtime.verify", Group: "runtime", Label: "终轮结果检查", Type: core.SettingBool, Default: true, Effect: core.EffectNewRun, Description: "终轮确定性检查与独立验证"},
|
||||
{Key: "runtime.llmRetries", Group: "runtime", Label: "模型重试次数", Type: core.SettingNumber, Default: 2, Effect: core.EffectNewRun, Description: "直连模型在还没有任何输出时,遇到超时、限流或服务端错误的额外重试次数。0 = 不重试。已经开始输出后不再重试"},
|
||||
{Key: "runtime.codeOrchestration", Group: "runtime", Label: "代码编排", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "允许模型用 run_tool_script 写脚本批量调用工具。默认关闭"},
|
||||
{Key: "runtime.composeFinal", Group: "runtime", Label: "终轮整理回复", Type: core.SettingBool, Default: true, Effect: core.EffectNewRun, Description: "用过工具的回合结束后,再用主模型写一段直接回答用户的最终回复"},
|
||||
{Key: "context.maxHistoryMessages", Group: "context", Label: "历史窗口条数", Type: core.SettingNumber, Default: 40, Effect: core.EffectNewRun},
|
||||
{Key: "context.maxToolResultChars", Group: "context", Label: "工具结果上限(字符)", Type: core.SettingNumber, Default: 20000, Effect: core.EffectNewRun},
|
||||
@@ -94,6 +96,7 @@ func Schema() []core.SettingDesc {
|
||||
{Key: "ui.cardflow", Group: "ui", Label: "长结果进右侧卡片", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "开启后报告/纪要/方案等产物型长内容经 submit_card 进收件箱,对话只留结论摘要与锚点"},
|
||||
{Key: "tools.tavily.apiKeys", Group: "tools", Label: "Tavily API Keys(池)", Type: core.SettingSecret, Default: "", Effect: core.EffectRestart, Description: "逗号分隔;配置后注册 tavily_search/web_search 工具(429/401/403 自动轮换下一把)"},
|
||||
{Key: "tools.tavily.endpoint", Group: "tools", Label: "Tavily 端点", Type: core.SettingString, Default: "", Effect: core.EffectRestart, Description: "空 = 官方 https://api.tavily.com/search;可指向自建代理"},
|
||||
{Key: "tools.mcp.overrides", Group: "tools", Label: "MCP 项目覆盖", Type: core.SettingString, Default: "", Effect: core.EffectNewRun, Description: "JSON 对象。键是服务器名,值可含 enabled 与 exposure(direct 或 deferred)"},
|
||||
{Key: "deliverables.style", Group: "deliverables", Label: "交付物默认样式", Type: core.SettingEnum, Default: "modern", Options: []string{"modern", "formal", "minimal"}, Effect: core.EffectNewRun, Description: "modern 现代专业 / formal 公文 / minimal 极简。本轮指令、上传模板或参考文档优先于这项"},
|
||||
{Key: "deliverables.accentColor", Group: "deliverables", Label: "交付物强调色", Type: core.SettingString, Default: "", Effect: core.EffectNewRun, Description: "6 位十六进制,如 1B3A4B。空 = 使用样式自带主色"},
|
||||
{Key: "deliverables.templateArtifactId", Group: "deliverables", Label: "默认模板工件 ID", Type: core.SettingString, Default: "", Effect: core.EffectNewRun, Description: "填写已上传模板的工件 ID。每次运行会复制到会话 inputs/_default-template"},
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"agent-native/runtime/codeorch"
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
// RegisterCodeTool 注册 run_tool_script。开关关闭时执行会被拒绝,清单侧另行隐藏。
|
||||
func RegisterCodeTool(r *Registry) error {
|
||||
return r.Register(core.ToolDesc{
|
||||
Name: "run_tool_script",
|
||||
Description: "用一段 Python 依次调用已有工具。禁止再调用解释器、本工具、派发和工作流。失败时会列出已经执行的调用",
|
||||
Source: core.SourceBuiltin,
|
||||
Risk: "medium",
|
||||
Parameters: json.RawMessage(`{"type":"object","properties":{"script":{"type":"string"},"timeout_sec":{"type":"integer"}},"required":["script"]}`),
|
||||
}, execCodeScript)
|
||||
}
|
||||
|
||||
type toolCaller interface {
|
||||
ExecuteTool(call core.ToolCallRequest) core.ToolCallResult
|
||||
}
|
||||
|
||||
func execCodeScript(rc ToolExecCtx, args json.RawMessage) (string, error) {
|
||||
if !rc.SettingBool("runtime.codeOrchestration", false) {
|
||||
return "", fmt.Errorf("代码编排未开启")
|
||||
}
|
||||
caller, ok := rc.(toolCaller)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("代码编排未接入工具执行")
|
||||
}
|
||||
var req struct {
|
||||
Script string `json:"script"`
|
||||
Timeout int `json:"timeout_sec"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &req); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(req.Script) == "" {
|
||||
return "", fmt.Errorf("script 不能为空")
|
||||
}
|
||||
root := ""
|
||||
if s, ok := rc.(interface{ ConversationSandbox() string }); ok {
|
||||
root = s.ConversationSandbox()
|
||||
}
|
||||
if root == "" {
|
||||
root = rc.WorkspaceRoot()
|
||||
}
|
||||
if root == "" {
|
||||
return "", fmt.Errorf("代码编排没有会话目录")
|
||||
}
|
||||
lim := codeorch.Limits{FullDir: root}
|
||||
if req.Timeout > 0 {
|
||||
lim.Timeout = time.Duration(req.Timeout) * time.Second
|
||||
}
|
||||
out := codeorch.Run(context.Background(), root, req.Script, lim, nil, func(_ context.Context, call codeorch.Call) (string, error) {
|
||||
if call.Op != "tool.call" {
|
||||
return "", fmt.Errorf("不能调用 %s", call.Op)
|
||||
}
|
||||
if codeorch.Forbidden(call.Name) {
|
||||
return "", fmt.Errorf("不能调用 %s", call.Name)
|
||||
}
|
||||
res := caller.ExecuteTool(core.ToolCallRequest{Name: call.Name, Arguments: call.Args})
|
||||
if res.Status == "error" {
|
||||
return "", fmt.Errorf("%s", res.Output)
|
||||
}
|
||||
return res.Output, nil
|
||||
})
|
||||
if out.Err != nil {
|
||||
return out.ModelText(), fmt.Errorf("%s", out.ModelText())
|
||||
}
|
||||
return out.ModelText(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user