feat(tools): add background execution support to shell tool to prevent agent blocking (fixes #197)
This commit is contained in:
parent
4777024781
commit
0759823248
1 changed files with 57 additions and 2 deletions
|
|
@ -19,6 +19,7 @@ type ExecTool struct {
|
||||||
denyPatterns []*regexp.Regexp
|
denyPatterns []*regexp.Regexp
|
||||||
allowPatterns []*regexp.Regexp
|
allowPatterns []*regexp.Regexp
|
||||||
restrictToWorkspace bool
|
restrictToWorkspace bool
|
||||||
|
callback AsyncCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
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(`\bdd\s+if=`),
|
||||||
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
|
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(`\b(shutdown|reboot|poweroff)\b`),
|
||||||
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ExecTool{
|
return &ExecTool{
|
||||||
workingDir: workingDir,
|
workingDir: workingDir,
|
||||||
timeout: 60 * time.Second,
|
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 {
|
func (t *ExecTool) Name() string {
|
||||||
return "exec"
|
return "exec"
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +66,10 @@ func (t *ExecTool) Parameters() map[string]interface{} {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional working directory for the command",
|
"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"},
|
"required": []string{"command"},
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +97,53 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *To
|
||||||
return ErrorResult(guardError)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue