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}
23
24type JobOutputResponseMetadata struct {
25 ShellID string `json:"shell_id"`
26 Command string `json:"command"`
27 Description string `json:"description"`
28 Done bool `json:"done"`
29 WorkingDirectory string `json:"working_directory"`
30}
31
32func NewJobOutputTool() fantasy.AgentTool {
33 return fantasy.NewAgentTool(
34 JobOutputToolName,
35 string(jobOutputDescription),
36 func(ctx context.Context, params JobOutputParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
37 if params.ShellID == "" {
38 return fantasy.NewTextErrorResponse("missing shell_id"), nil
39 }
40
41 bgManager := shell.GetBackgroundShellManager()
42 bgShell, ok := bgManager.Get(params.ShellID)
43 if !ok {
44 return fantasy.NewTextErrorResponse(fmt.Sprintf("background shell not found: %s", params.ShellID)), nil
45 }
46
47 stdout, stderr, done, err := bgShell.GetOutput()
48
49 var outputParts []string
50 if stdout != "" {
51 outputParts = append(outputParts, stdout)
52 }
53 if stderr != "" {
54 outputParts = append(outputParts, stderr)
55 }
56
57 status := "running"
58 if done {
59 status = "completed"
60 if err != nil {
61 exitCode := shell.ExitCode(err)
62 if exitCode != 0 {
63 outputParts = append(outputParts, fmt.Sprintf("Exit code %d", exitCode))
64 }
65 }
66 }
67
68 output := strings.Join(outputParts, "\n")
69
70 metadata := JobOutputResponseMetadata{
71 ShellID: params.ShellID,
72 Command: bgShell.Command,
73 Description: bgShell.Description,
74 Done: done,
75 WorkingDirectory: bgShell.WorkingDir,
76 }
77
78 if output == "" {
79 output = BashNoOutput
80 }
81
82 result := fmt.Sprintf("Status: %s\n\n%s", status, output)
83 return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result), metadata), nil
84 })
85}