Files
BlackBean/pigo/internal/agenttool/bash_control.go
T
2026-08-14 23:41:57 +08:00

159 lines
5.5 KiB
Go

// This file implements the two companion tools for background bash jobs:
// bash_output drains a job's new output and reports its status, and kill_bash
// terminates a running job. Both address a job by the bash_id returned from a
// `bash` call with run_in_background=true, sharing the same BashJobStore so a
// job launched by the bash tool is visible here. This mirrors Claude Code's
// BashOutput/KillShell tools.
package agenttool
import (
"context"
"encoding/json"
"fmt"
"github.com/smallnest/pigo/internal/agentcore"
)
// BashOutputTool reads the output a background job has produced since the last
// read and reports whether it is still running or has exited. Jobs is the shared
// store the bash tool populates.
type BashOutputTool struct {
Jobs *BashJobStore
}
// bashOutputArgs is the decoded argument shape for BashOutputTool.
type bashOutputArgs struct {
// BashID is the job handle returned by a background `bash` call.
BashID string `json:"bash_id"`
}
// Name implements AgentTool.
func (t *BashOutputTool) Name() string { return "bash_output" }
// Description implements AgentTool.
func (t *BashOutputTool) Description() string {
return "Read new output from a background command started with bash " +
"run_in_background=true, addressed by its bash_id. Returns output " +
"accumulated since the last read plus the command's status (running or " +
"exited, with exit code). Call repeatedly to stream a long job's output."
}
// Schema implements AgentTool.
func (t *BashOutputTool) Schema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"bash_id": {"type": "string", "description": "The bash_id returned by a background bash call."}
},
"required": ["bash_id"],
"additionalProperties": false
}`)
}
// ExecutionMode implements AgentTool. Reading output has no side effects.
func (t *BashOutputTool) ExecutionMode() agentcore.ToolExecutionMode {
return agentcore.ToolExecutionParallel
}
// Execute implements AgentTool. It returns the job's new output and status.
func (t *BashOutputTool) Execute(ctx context.Context, id string, args json.RawMessage, onUpdate agentcore.ToolUpdateFunc) (agentcore.AgentToolResult, error) {
a, bad := decodeArgs[bashOutputArgs](args, "bash_output")
if bad != nil {
return *bad, nil
}
if t.Jobs == nil {
return errorResult("bash_output: background jobs are not available in this environment"), nil
}
job, ok := t.Jobs.Get(a.BashID)
if !ok {
return errorResult(fmt.Sprintf("bash_output: no background command with id %q", a.BashID)), nil
}
out := truncateBashOutput(job.readNew())
status, exitCode, errMsg := job.snapshot()
var statusLine string
if status == BashRunning {
statusLine = fmt.Sprintf("[%s: running]", a.BashID)
} else if errMsg != "" && exitCode != 0 {
statusLine = fmt.Sprintf("[%s: exited code %d: %s]", a.BashID, exitCode, errMsg)
} else {
statusLine = fmt.Sprintf("[%s: exited code %d]", a.BashID, exitCode)
}
text := statusLine
if out != "" {
text = out + "\n" + statusLine
}
return agentcore.AgentToolResult{
Content: agentcore.ContentList{agentcore.NewTextContent(text)},
Details: map[string]any{"bash_id": a.BashID, "status": string(status), "exitCode": exitCode},
}, nil
}
// BashKillTool terminates a running background job by canceling its context.
// Jobs is the shared store the bash tool populates.
type BashKillTool struct {
Jobs *BashJobStore
}
// bashKillArgs is the decoded argument shape for BashKillTool.
type bashKillArgs struct {
// BashID is the job handle returned by a background `bash` call.
BashID string `json:"bash_id"`
}
// Name implements AgentTool.
func (t *BashKillTool) Name() string { return "kill_bash" }
// Description implements AgentTool.
func (t *BashKillTool) Description() string {
return "Terminate a background command started with bash " +
"run_in_background=true, addressed by its bash_id. The command's " +
"process is killed; already-exited jobs report that they were not running."
}
// Schema implements AgentTool.
func (t *BashKillTool) Schema() json.RawMessage {
return json.RawMessage(`{
"type": "object",
"properties": {
"bash_id": {"type": "string", "description": "The bash_id returned by a background bash call."}
},
"required": ["bash_id"],
"additionalProperties": false
}`)
}
// ExecutionMode implements AgentTool. Killing a process is a side effect.
func (t *BashKillTool) ExecutionMode() agentcore.ToolExecutionMode {
return agentcore.ToolExecutionSequential
}
// Execute implements AgentTool. It kills the job and reports whether it had been
// running.
func (t *BashKillTool) Execute(ctx context.Context, id string, args json.RawMessage, onUpdate agentcore.ToolUpdateFunc) (agentcore.AgentToolResult, error) {
a, bad := decodeArgs[bashKillArgs](args, "kill_bash")
if bad != nil {
return *bad, nil
}
if t.Jobs == nil {
return errorResult("kill_bash: background jobs are not available in this environment"), nil
}
job, ok := t.Jobs.Get(a.BashID)
if !ok {
return errorResult(fmt.Sprintf("kill_bash: no background command with id %q", a.BashID)), nil
}
if job.kill() {
return agentcore.AgentToolResult{
Content: agentcore.ContentList{agentcore.NewTextContent(fmt.Sprintf("killed background command %s", a.BashID))},
Details: map[string]any{"bash_id": a.BashID, "killed": true},
}, nil
}
return agentcore.AgentToolResult{
Content: agentcore.ContentList{agentcore.NewTextContent(fmt.Sprintf("background command %s was not running", a.BashID))},
Details: map[string]any{"bash_id": a.BashID, "killed": false},
}, nil
}