tools.go

  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}