1package tools
2
3import (
4 "bytes"
5 "context"
6 "html/template"
7 "os/exec"
8 "testing"
9
10 "charm.land/fantasy"
11)
12
13type (
14 sessionIDContextKey string
15 messageIDContextKey string
16 supportsImagesKey string
17 modelNameKey string
18)
19
20const (
21 // SessionIDContextKey is the key for the session ID in the context.
22 SessionIDContextKey sessionIDContextKey = "session_id"
23 // MessageIDContextKey is the key for the message ID in the context.
24 MessageIDContextKey messageIDContextKey = "message_id"
25 // SupportsImagesContextKey is the key for the model's image support capability.
26 SupportsImagesContextKey supportsImagesKey = "supports_images"
27 // ModelNameContextKey is the key for the model name in the context.
28 ModelNameContextKey modelNameKey = "model_name"
29)
30
31// getContextValue is a generic helper that retrieves a typed value from context.
32// If the value is not found or has the wrong type, it returns the default value.
33func getContextValue[T any](ctx context.Context, key any, defaultValue T) T {
34 value := ctx.Value(key)
35 if value == nil {
36 return defaultValue
37 }
38 if typedValue, ok := value.(T); ok {
39 return typedValue
40 }
41 return defaultValue
42}
43
44// GetSessionFromContext retrieves the session ID from the context.
45func GetSessionFromContext(ctx context.Context) string {
46 return getContextValue(ctx, SessionIDContextKey, "")
47}
48
49// GetMessageFromContext retrieves the message ID from the context.
50func GetMessageFromContext(ctx context.Context) string {
51 return getContextValue(ctx, MessageIDContextKey, "")
52}
53
54// GetSupportsImagesFromContext retrieves whether the model supports images from the context.
55func GetSupportsImagesFromContext(ctx context.Context) bool {
56 return getContextValue(ctx, SupportsImagesContextKey, false)
57}
58
59// GetModelNameFromContext retrieves the model name from the context.
60func GetModelNameFromContext(ctx context.Context) string {
61 return getContextValue(ctx, ModelNameContextKey, "")
62}
63
64// NewPermissionDeniedResponse returns a tool response indicating the user
65// denied permission, with StopTurn set so the agent loop does not retry.
66func NewPermissionDeniedResponse() fantasy.ToolResponse {
67 resp := fantasy.NewTextErrorResponse("User denied permission")
68 resp.StopTurn = true
69 return resp
70}
71
72// ghAvailable indicates whether the `gh` CLI is available on PATH.
73var ghAvailable = func() bool {
74 if testing.Testing() {
75 return false
76 }
77 _, err := exec.LookPath("gh")
78 return err == nil
79}()
80
81// toolDescriptionData is the common data structure for tool description templates.
82type toolDescriptionData struct {
83 GhAvailable bool
84}
85
86// renderToolDescription renders a tool description template with the given data.
87func renderToolDescription(tmpl *template.Template) string {
88 data := toolDescriptionData{
89 GhAvailable: ghAvailable,
90 }
91 var out bytes.Buffer
92 if err := tmpl.Execute(&out, data); err != nil {
93 panic("failed to execute tool description template: " + err.Error())
94 }
95 return out.String()
96}
97
98// renderTemplate renders a Go template with the given data.
99func renderTemplate(tmpl *template.Template, data any) string {
100 var out bytes.Buffer
101 if err := tmpl.Execute(&out, data); err != nil {
102 panic("failed to execute tool description template: " + err.Error())
103 }
104 return out.String()
105}