// 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 }