first commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user