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