job_output.go

 1package tools
 2
 3import (
 4	"context"
 5	_ "embed"
 6	"fmt"
 7	"strings"
 8
 9	"charm.land/fantasy"
10	"github.com/charmbracelet/crush/internal/shell"
11)
12
13const (
14	JobOutputToolName = "job_output"
15)
16
17//go:embed job_output.md
18var jobOutputDescription []byte
19
20type JobOutputParams struct {
21	ShellID string `json:"shell_id" description:"The ID of the background shell to retrieve output from"`
22	Wait    bool   `json:"wait" description:"If true, block until the background shell completes before returning output"`
23}
24
25type JobOutputResponseMetadata struct {
26	ShellID          string `json:"shell_id"`
27	Command          string `json:"command"`
28	Description      string `json:"description"`
29	Done             bool   `json:"done"`
30	WorkingDirectory string `json:"working_directory"`
31}
32
33func NewJobOutputTool() fantasy.AgentTool {
34	return fantasy.NewAgentTool(
35		JobOutputToolName,
36		string(jobOutputDescription),
37		func(ctx context.Context, params JobOutputParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
38			if params.ShellID == "" {
39				return fantasy.NewTextErrorResponse("missing shell_id"), nil
40			}
41
42			bgManager := shell.GetBackgroundShellManager()
43			bgShell, ok := bgManager.Get(params.ShellID)
44			if !ok {
45				return fantasy.NewTextErrorResponse(fmt.Sprintf("background shell not found: %s", params.ShellID)), nil
46			}
47
48			if params.Wait {
49				bgShell.WaitContext(ctx)
50			}
51
52			stdout, stderr, done, err := bgShell.GetOutput()
53
54			var outputParts []string
55			if stdout != "" {
56				outputParts = append(outputParts, stdout)
57			}
58			if stderr != "" {
59				outputParts = append(outputParts, stderr)
60			}
61
62			status := "running"
63			if done {
64				status = "completed"
65				if err != nil {
66					exitCode := shell.ExitCode(err)
67					if exitCode != 0 {
68						outputParts = append(outputParts, fmt.Sprintf("Exit code %d", exitCode))
69					}
70				}
71			}
72
73			output := strings.Join(outputParts, "\n")
74
75			metadata := JobOutputResponseMetadata{
76				ShellID:          params.ShellID,
77				Command:          bgShell.Command,
78				Description:      bgShell.Description,
79				Done:             done,
80				WorkingDirectory: bgShell.WorkingDir,
81			}
82
83			if output == "" {
84				output = BashNoOutput
85			}
86
87			result := fmt.Sprintf("Status: %s\n\n%s", status, output)
88			return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result), metadata), nil
89		})
90}