159 lines
5.5 KiB
Go
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
|
|
}
|