feat(tools): add background execution support to shell tool to prevent agent blocking (fixes #197)
This commit is contained in:
parent
b031fed067
commit
93e0d3be27
1 changed files with 57 additions and 2 deletions
|
|
@ -19,6 +19,7 @@ type ExecTool struct {
|
|||
denyPatterns []*regexp.Regexp
|
||||
allowPatterns []*regexp.Regexp
|
||||
restrictToWorkspace bool
|
||||
callback AsyncCallback
|
||||
}
|
||||
|
||||
func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
||||
|
|
@ -30,9 +31,7 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
|||
regexp.MustCompile(`\bdd\s+if=`),
|
||||
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
|
||||
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
|
||||
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
|
||||
}
|
||||
|
||||
return &ExecTool{
|
||||
workingDir: workingDir,
|
||||
timeout: 60 * time.Second,
|
||||
|
|
@ -42,6 +41,11 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
|||
}
|
||||
}
|
||||
|
||||
// SetCallback implements AsyncTool interface
|
||||
func (t *ExecTool) SetCallback(cb AsyncCallback) {
|
||||
t.callback = cb
|
||||
}
|
||||
|
||||
func (t *ExecTool) Name() string {
|
||||
return "exec"
|
||||
}
|
||||
|
|
@ -62,6 +66,10 @@ func (t *ExecTool) Parameters() map[string]interface{} {
|
|||
"type": "string",
|
||||
"description": "Optional working directory for the command",
|
||||
},
|
||||
"background": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "Run the command in the background. Use this for starting servers or long-running tasks. The tool will return immediately and report results later.",
|
||||
},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
}
|
||||
|
|
@ -89,6 +97,53 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *To
|
|||
return ErrorResult(guardError)
|
||||
}
|
||||
|
||||
if background, _ := args["background"].(bool); background {
|
||||
// Run in background
|
||||
go func() {
|
||||
// Create a fresh context for background execution
|
||||
// We don't use the timeout from the tool since background tasks are expected to be long-lived
|
||||
bgCtx := context.Background()
|
||||
|
||||
var bgCmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
bgCmd = exec.CommandContext(bgCtx, "powershell", "-NoProfile", "-NonInteractive", "-Command", command)
|
||||
} else {
|
||||
bgCmd = exec.CommandContext(bgCtx, "sh", "-c", command)
|
||||
}
|
||||
if cwd != "" {
|
||||
bgCmd.Dir = cwd
|
||||
}
|
||||
|
||||
var bgStdout, bgStderr bytes.Buffer
|
||||
bgCmd.Stdout = &bgStdout
|
||||
bgCmd.Stderr = &bgStderr
|
||||
|
||||
err := bgCmd.Run()
|
||||
bgOutput := bgStdout.String()
|
||||
if bgStderr.Len() > 0 {
|
||||
bgOutput += "\nSTDERR:\n" + bgStderr.String()
|
||||
}
|
||||
if err != nil {
|
||||
bgOutput += fmt.Sprintf("\nExit code: %v", err)
|
||||
}
|
||||
if bgOutput == "" {
|
||||
bgOutput = "(no output)"
|
||||
}
|
||||
|
||||
if t.callback != nil {
|
||||
res := &ToolResult{
|
||||
ForLLM: fmt.Sprintf("Background command '%s' completed:\n%s", command, bgOutput),
|
||||
ForUser: fmt.Sprintf("✅ Background command '%s' completed.", command),
|
||||
IsError: err != nil,
|
||||
}
|
||||
t.callback(bgCtx, res)
|
||||
}
|
||||
}()
|
||||
|
||||
msg := fmt.Sprintf("Started command '%s' in background.", command)
|
||||
return AsyncResult(msg)
|
||||
}
|
||||
|
||||
// timeout == 0 means no timeout
|
||||
var cmdCtx context.Context
|
||||
var cancel context.CancelFunc
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue