diff --git a/go.mod b/go.mod
index 020e5eb1f0768b842b773e57255e72755e3c7351..1e3fdb41d6a219ff617c4cb7888af0c0f4ddf7c4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/charmbracelet/crush
go 1.25.0
require (
+ charm.land/fantasy v0.0.0
github.com/JohannesKaufmann/html-to-markdown v1.6.0
github.com/MakeNowJust/heredoc v1.0.0
github.com/PuerkitoBio/goquery v1.10.3
@@ -15,7 +16,6 @@ require (
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619
github.com/charmbracelet/catwalk v0.6.5-0.20251015124117-3bfe9eb6d20f
github.com/charmbracelet/fang v0.4.3
- github.com/charmbracelet/fantasy v0.0.0-20251015132833-5bc4cc524d70
github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea
github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
@@ -47,6 +47,8 @@ require (
mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
)
+replace charm.land/fantasy => ../../fantasy/main/
+
require (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.12.1 // indirect
@@ -56,6 +58,20 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
+ github.com/aws/smithy-go v1.20.3 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index 6dcdae5849b1ddc7d5edfe4184dcb2793dbfda07..99cf9b5f3dd573835f198edb9182fef4612f494c 100644
--- a/go.sum
+++ b/go.sum
@@ -36,6 +36,34 @@ github.com/anthropics/anthropic-sdk-go v1.12.0 h1:xPqlGnq7rWrTiHazIvCiumA0u7mGQn
github.com/anthropics/anthropic-sdk-go v1.12.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
+github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
+github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
+github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
+github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
+github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
@@ -61,8 +89,6 @@ github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqI
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY=
github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
-github.com/charmbracelet/fantasy v0.0.0-20251015132833-5bc4cc524d70 h1:zVfEkMs1ncdUVQic9cGHHoZYMR0QNsmQ+gz8+LBo4cw=
-github.com/charmbracelet/fantasy v0.0.0-20251015132833-5bc4cc524d70/go.mod h1:FugQt16qw07BjrmjHzu+l6BtlUnZgTcX/8qHyg4D/yQ=
github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
diff --git a/internal/agent/agent.go b/internal/agent/agent.go
index a90e3c6038de9d677d84ecc90bafae3ff2e756c9..9a237954179f83e9a60295162f3225b98a0cdbe3 100644
--- a/internal/agent/agent.go
+++ b/internal/agent/agent.go
@@ -10,6 +10,8 @@ import (
"sync"
"time"
+ "charm.land/fantasy"
+ "charm.land/fantasy/providers/anthropic"
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/agent/tools"
"github.com/charmbracelet/crush/internal/config"
@@ -17,8 +19,6 @@ import (
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/session"
- "github.com/charmbracelet/fantasy/ai"
- "github.com/charmbracelet/fantasy/anthropic"
)
//go:embed templates/title.md
@@ -30,7 +30,7 @@ var summaryPrompt []byte
type SessionAgentCall struct {
SessionID string
Prompt string
- ProviderOptions ai.ProviderOptions
+ ProviderOptions fantasy.ProviderOptions
Attachments []message.Attachment
MaxOutputTokens int64
Temperature *float64
@@ -41,9 +41,9 @@ type SessionAgentCall struct {
}
type SessionAgent interface {
- Run(context.Context, SessionAgentCall) (*ai.AgentResult, error)
+ Run(context.Context, SessionAgentCall) (*fantasy.AgentResult, error)
SetModels(large Model, small Model)
- SetTools(tools []ai.AgentTool)
+ SetTools(tools []fantasy.AgentTool)
Cancel(sessionID string)
CancelAll()
IsSessionBusy(sessionID string) bool
@@ -55,7 +55,7 @@ type SessionAgent interface {
}
type Model struct {
- Model ai.LanguageModel
+ Model fantasy.LanguageModel
CatwalkCfg catwalk.Model
ModelCfg config.SelectedModel
}
@@ -64,7 +64,7 @@ type sessionAgent struct {
largeModel Model
smallModel Model
systemPrompt string
- tools []ai.AgentTool
+ tools []fantasy.AgentTool
sessions session.Service
messages message.Service
disableAutoSummarize bool
@@ -80,7 +80,7 @@ type SessionAgentOptions struct {
DisableAutoSummarize bool
Sessions session.Service
Messages message.Service
- Tools []ai.AgentTool
+ Tools []fantasy.AgentTool
}
func NewSessionAgent(
@@ -99,7 +99,7 @@ func NewSessionAgent(
}
}
-func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.AgentResult, error) {
+func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) {
if call.Prompt == "" {
return nil, ErrEmptyPrompt
}
@@ -123,10 +123,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
a.tools[len(a.tools)-1].SetProviderOptions(a.getCacheControlOptions())
}
- agent := ai.NewAgent(
+ agent := fantasy.NewAgent(
a.largeModel.Model,
- ai.WithSystemPrompt(a.systemPrompt),
- ai.WithTools(a.tools...),
+ fantasy.WithSystemPrompt(a.systemPrompt),
+ fantasy.WithTools(a.tools...),
)
sessionLock := sync.Mutex{}
@@ -169,7 +169,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
var currentAssistant *message.Message
var shouldSummarize bool
- result, err := agent.Stream(genCtx, ai.AgentStreamCall{
+ result, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
Prompt: call.Prompt,
Files: files,
Messages: history,
@@ -181,7 +181,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
TopK: call.TopK,
FrequencyPenalty: call.FrequencyPenalty,
// Before each step create the new assistant message
- PrepareStep: func(callContext context.Context, options ai.PrepareStepFunctionOptions) (_ context.Context, prepared ai.PrepareStepResult, err error) {
+ PrepareStep: func(callContext context.Context, options fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
var assistantMsg message.Message
assistantMsg, err = a.messages.Create(callContext, call.SessionID, message.CreateMessageParams{
Role: message.Assistant,
@@ -217,7 +217,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
systemMessageUpdated := false
for i, msg := range prepared.Messages {
// only add cache control to the last message
- if msg.Role == ai.MessageRoleSystem {
+ if msg.Role == fantasy.MessageRoleSystem {
lastSystemRoleInx = i
} else if !systemMessageUpdated {
prepared.Messages[lastSystemRoleInx].ProviderOptions = a.getCacheControlOptions()
@@ -234,7 +234,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
currentAssistant.AppendReasoningContent(text)
return a.messages.Update(genCtx, *currentAssistant)
},
- OnReasoningEnd: func(id string, reasoning ai.ReasoningContent) error {
+ OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
// handle anthropic signature
if anthropicData, ok := reasoning.ProviderMetadata[anthropic.Name]; ok {
if reasoning, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok {
@@ -258,10 +258,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
currentAssistant.AddToolCall(toolCall)
return a.messages.Update(genCtx, *currentAssistant)
},
- OnRetry: func(err *ai.APICallError, delay time.Duration) {
+ OnRetry: func(err *fantasy.APICallError, delay time.Duration) {
// TODO: implement
},
- OnToolCall: func(tc ai.ToolCallContent) error {
+ OnToolCall: func(tc fantasy.ToolCallContent) error {
toolCall := message.ToolCall{
ID: tc.ToolCallID,
Name: tc.ToolName,
@@ -272,22 +272,22 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
currentAssistant.AddToolCall(toolCall)
return a.messages.Update(genCtx, *currentAssistant)
},
- OnToolResult: func(result ai.ToolResultContent) error {
+ OnToolResult: func(result fantasy.ToolResultContent) error {
var resultContent string
isError := false
switch result.Result.GetType() {
- case ai.ToolResultContentTypeText:
- r, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentText](result.Result)
+ case fantasy.ToolResultContentTypeText:
+ r, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Result)
if ok {
resultContent = r.Text
}
- case ai.ToolResultContentTypeError:
- r, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentError](result.Result)
+ case fantasy.ToolResultContentTypeError:
+ r, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Result)
if ok {
isError = true
resultContent = r.Error.Error()
}
- case ai.ToolResultContentTypeMedia:
+ case fantasy.ToolResultContentTypeMedia:
// TODO: handle this message type
}
toolResult := message.ToolResult{
@@ -308,14 +308,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
}
return nil
},
- OnStepFinish: func(stepResult ai.StepResult) error {
+ OnStepFinish: func(stepResult fantasy.StepResult) error {
finishReason := message.FinishReasonUnknown
switch stepResult.FinishReason {
- case ai.FinishReasonLength:
+ case fantasy.FinishReasonLength:
finishReason = message.FinishReasonMaxTokens
- case ai.FinishReasonStop:
+ case fantasy.FinishReasonStop:
finishReason = message.FinishReasonEndTurn
- case ai.FinishReasonToolCalls:
+ case fantasy.FinishReasonToolCalls:
finishReason = message.FinishReasonToolUse
}
currentAssistant.AddFinish(finishReason, "", "")
@@ -328,8 +328,8 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
}
return a.messages.Update(genCtx, *currentAssistant)
},
- StopWhen: []ai.StopCondition{
- func(_ []ai.StepResult) bool {
+ StopWhen: []fantasy.StopCondition{
+ func(_ []fantasy.StepResult) bool {
contextWindow := a.largeModel.CatwalkCfg.ContextWindow
tokens := currentSession.CompletionTokens + currentSession.PromptTokens
percentage := (float64(tokens) / float64(contextWindow)) * 100
@@ -461,8 +461,8 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
defer a.activeRequests.Del(sessionID)
defer cancel()
- agent := ai.NewAgent(a.largeModel.Model,
- ai.WithSystemPrompt(string(summaryPrompt)),
+ agent := fantasy.NewAgent(a.largeModel.Model,
+ fantasy.WithSystemPrompt(string(summaryPrompt)),
)
summaryMessage, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
Role: message.Assistant,
@@ -474,14 +474,14 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
return err
}
- resp, err := agent.Stream(genCtx, ai.AgentStreamCall{
+ resp, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
Prompt: "Provide a detailed summary of our conversation above.",
Messages: aiMsgs,
OnReasoningDelta: func(id string, text string) error {
summaryMessage.AppendReasoningContent(text)
return a.messages.Update(genCtx, summaryMessage)
},
- OnReasoningEnd: func(id string, reasoning ai.ReasoningContent) error {
+ OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
// handle anthropic signature
if anthropicData, ok := reasoning.ProviderMetadata["anthropic"]; ok {
if signature, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok && signature.Signature != "" {
@@ -523,8 +523,8 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
return err
}
-func (a *sessionAgent) getCacheControlOptions() ai.ProviderOptions {
- return ai.ProviderOptions{
+func (a *sessionAgent) getCacheControlOptions() fantasy.ProviderOptions {
+ return fantasy.ProviderOptions{
anthropic.Name: &anthropic.ProviderCacheControlOptions{
CacheControl: anthropic.CacheControl{Type: "ephemeral"},
},
@@ -548,8 +548,8 @@ func (a *sessionAgent) createUserMessage(ctx context.Context, call SessionAgentC
return msg, nil
}
-func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...message.Attachment) ([]ai.Message, []ai.FilePart) {
- var history []ai.Message
+func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...message.Attachment) ([]fantasy.Message, []fantasy.FilePart) {
+ var history []fantasy.Message
for _, m := range msgs {
if len(m.Parts) == 0 {
continue
@@ -561,9 +561,9 @@ func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...mess
history = append(history, m.ToAIMessage()...)
}
- var files []ai.FilePart
+ var files []fantasy.FilePart
for _, attachment := range attachments {
- files = append(files, ai.FilePart{
+ files = append(files, fantasy.FilePart{
Filename: attachment.FileName,
Data: attachment.Content,
MediaType: attachment.MimeType,
@@ -605,12 +605,12 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
maxOutput = a.smallModel.CatwalkCfg.DefaultMaxTokens
}
- agent := ai.NewAgent(a.smallModel.Model,
- ai.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
- ai.WithMaxOutputTokens(maxOutput),
+ agent := fantasy.NewAgent(a.smallModel.Model,
+ fantasy.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
+ fantasy.WithMaxOutputTokens(maxOutput),
)
- resp, err := agent.Stream(ctx, ai.AgentStreamCall{
+ resp, err := agent.Stream(ctx, fantasy.AgentStreamCall{
Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n \n\n", prompt),
})
if err != nil {
@@ -642,7 +642,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
}
}
-func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session, usage ai.Usage) {
+func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session, usage fantasy.Usage) {
modelConfig := model.CatwalkCfg
cost := modelConfig.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
modelConfig.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
@@ -727,7 +727,7 @@ func (a *sessionAgent) SetModels(large Model, small Model) {
a.smallModel = small
}
-func (a *sessionAgent) SetTools(tools []ai.AgentTool) {
+func (a *sessionAgent) SetTools(tools []fantasy.AgentTool) {
a.tools = tools
}
diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go
index 9eedec274c1fa86181476bed95276de661acb8fe..db691c9c7d5761d84fddc6a2a2cfcb7039c1707d 100644
--- a/internal/agent/agent_test.go
+++ b/internal/agent/agent_test.go
@@ -6,10 +6,10 @@ import (
"strings"
"testing"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/agent/tools"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/shell"
- "github.com/charmbracelet/fantasy/ai"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
@@ -24,7 +24,7 @@ var modelPairs = []modelPair{
{"zai-glm4.6", zAIBuilder("glm-4.6"), zAIBuilder("glm-4.5-air")},
}
-func getModels(t *testing.T, r *recorder.Recorder, pair modelPair) (ai.LanguageModel, ai.LanguageModel) {
+func getModels(t *testing.T, r *recorder.Recorder, pair modelPair) (fantasy.LanguageModel, fantasy.LanguageModel) {
large, err := pair.largeModel(t, r)
require.NoError(t, err)
small, err := pair.smallModel(t, r)
diff --git a/internal/agent/agent_tool.go b/internal/agent/agent_tool.go
index 5ffe3d6f4c3b5e56330bff8739d815191ec2dd8c..03a2f0c8c8cfa53eafae47c2fcffc2b1fc36886e 100644
--- a/internal/agent/agent_tool.go
+++ b/internal/agent/agent_tool.go
@@ -7,7 +7,7 @@ import (
"errors"
"fmt"
- "github.com/charmbracelet/fantasy/ai"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/agent/prompt"
"github.com/charmbracelet/crush/internal/agent/tools"
@@ -25,7 +25,7 @@ const (
AgentToolName = "agent"
)
-func (c *coordinator) agentTool() (ai.AgentTool, error) {
+func (c *coordinator) agentTool() (fantasy.AgentTool, error) {
agentCfg, ok := c.cfg.Agents[config.AgentTask]
if !ok {
return nil, errors.New("task agent not configured")
@@ -39,31 +39,31 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
if err != nil {
return nil, err
}
- return ai.NewAgentTool(
+ return fantasy.NewAgentTool(
AgentToolName,
string(agentToolDescription),
- func(ctx context.Context, params AgentParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params AgentParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil {
- return ai.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
}
if params.Prompt == "" {
- return ai.NewTextErrorResponse("prompt is required"), nil
+ return fantasy.NewTextErrorResponse("prompt is required"), nil
}
sessionID := tools.GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, errors.New("session id missing from context")
+ return fantasy.ToolResponse{}, errors.New("session id missing from context")
}
agentMessageID := tools.GetMessageFromContext(ctx)
if agentMessageID == "" {
- return ai.ToolResponse{}, errors.New("agent message id missing from context")
+ return fantasy.ToolResponse{}, errors.New("agent message id missing from context")
}
agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, call.ID)
session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, "New Agent Session")
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
}
model := agent.Model()
maxTokens := model.CatwalkCfg.DefaultMaxTokens
@@ -73,7 +73,7 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
if !ok {
- return ai.ToolResponse{}, errors.New("model provider not configured")
+ return fantasy.ToolResponse{}, errors.New("model provider not configured")
}
result, err := agent.Run(ctx, SessionAgentCall{
SessionID: session.ID,
@@ -87,23 +87,23 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
PresencePenalty: model.ModelCfg.PresencePenalty,
})
if err != nil {
- return ai.NewTextErrorResponse("error generating response"), nil
+ return fantasy.NewTextErrorResponse("error generating response"), nil
}
updatedSession, err := c.sessions.Get(ctx, session.ID)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
}
parentSession, err := c.sessions.Get(ctx, sessionID)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
}
parentSession.Cost += updatedSession.Cost
_, err = c.sessions.Save(ctx, parentSession)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
}
- return ai.NewTextResponse(result.Response.Content.Text()), nil
+ return fantasy.NewTextResponse(result.Response.Content.Text()), nil
}), nil
}
diff --git a/internal/agent/common_test.go b/internal/agent/common_test.go
index b6769acb458bd17641a7a8a7df20bfb6b56af4f7..0c1b34a6c88543862bea6550198cfb6b05b5c6c9 100644
--- a/internal/agent/common_test.go
+++ b/internal/agent/common_test.go
@@ -7,6 +7,11 @@ import (
"testing"
"time"
+ "charm.land/fantasy"
+ "charm.land/fantasy/providers/anthropic"
+ "charm.land/fantasy/providers/openai"
+ "charm.land/fantasy/providers/openaicompat"
+ "charm.land/fantasy/providers/openrouter"
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/agent/prompt"
"github.com/charmbracelet/crush/internal/agent/tools"
@@ -18,11 +23,6 @@ import (
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/session"
- "github.com/charmbracelet/fantasy/ai"
- "github.com/charmbracelet/fantasy/anthropic"
- "github.com/charmbracelet/fantasy/openai"
- "github.com/charmbracelet/fantasy/openaicompat"
- "github.com/charmbracelet/fantasy/openrouter"
"github.com/stretchr/testify/require"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
@@ -38,7 +38,7 @@ type env struct {
lspClients *csync.Map[string, *lsp.Client]
}
-type builderFunc func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error)
+type builderFunc func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error)
type modelPair struct {
name string
@@ -47,7 +47,7 @@ type modelPair struct {
}
func anthropicBuilder(model string) builderFunc {
- return func(_ *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
+ return func(_ *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
provider := anthropic.New(
anthropic.WithAPIKey(os.Getenv("CRUSH_ANTHROPIC_API_KEY")),
anthropic.WithHTTPClient(&http.Client{Transport: r}),
@@ -57,7 +57,7 @@ func anthropicBuilder(model string) builderFunc {
}
func openaiBuilder(model string) builderFunc {
- return func(_ *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
+ return func(_ *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
provider := openai.New(
openai.WithAPIKey(os.Getenv("CRUSH_OPENAI_API_KEY")),
openai.WithHTTPClient(&http.Client{Transport: r}),
@@ -67,7 +67,7 @@ func openaiBuilder(model string) builderFunc {
}
func openRouterBuilder(model string) builderFunc {
- return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
+ return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
provider := openrouter.New(
openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")),
openrouter.WithHTTPClient(&http.Client{Transport: r}),
@@ -77,7 +77,7 @@ func openRouterBuilder(model string) builderFunc {
}
func zAIBuilder(model string) builderFunc {
- return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
+ return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
provider := openaicompat.New(
openaicompat.WithBaseURL("https://api.z.ai/api/coding/paas/v4"),
openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")),
@@ -114,7 +114,7 @@ func testEnv(t *testing.T) env {
}
}
-func testSessionAgent(env env, large, small ai.LanguageModel, systemPrompt string, tools ...ai.AgentTool) SessionAgent {
+func testSessionAgent(env env, large, small fantasy.LanguageModel, systemPrompt string, tools ...fantasy.AgentTool) SessionAgent {
largeModel := Model{
Model: large,
CatwalkCfg: catwalk.Model{
@@ -133,7 +133,7 @@ func testSessionAgent(env env, large, small ai.LanguageModel, systemPrompt strin
return agent
}
-func coderAgent(r *recorder.Recorder, env env, large, small ai.LanguageModel) (SessionAgent, error) {
+func coderAgent(r *recorder.Recorder, env env, large, small fantasy.LanguageModel) (SessionAgent, error) {
fixedTime := func() time.Time {
t, _ := time.Parse("1/2/2006", "1/1/2025")
return t
@@ -155,7 +155,7 @@ func coderAgent(r *recorder.Recorder, env env, large, small ai.LanguageModel) (S
if err != nil {
return nil, err
}
- allTools := []ai.AgentTool{
+ allTools := []fantasy.AgentTool{
tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution),
tools.NewDownloadTool(env.permissions, env.workingDir, r.GetDefaultClient()),
tools.NewEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go
index 54ba4f44bb38265f7c5d5c7f1fd60a73441717fc..241d90d813d150cc3632e8731b83ed6a4ab6fa9b 100644
--- a/internal/agent/coordinator.go
+++ b/internal/agent/coordinator.go
@@ -11,6 +11,7 @@ import (
"slices"
"strings"
+ "charm.land/fantasy"
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/agent/prompt"
"github.com/charmbracelet/crush/internal/agent/tools"
@@ -22,20 +23,20 @@ import (
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/session"
- "github.com/charmbracelet/fantasy/ai"
- "github.com/charmbracelet/fantasy/anthropic"
- "github.com/charmbracelet/fantasy/azure"
- "github.com/charmbracelet/fantasy/google"
- "github.com/charmbracelet/fantasy/openai"
- "github.com/charmbracelet/fantasy/openaicompat"
- "github.com/charmbracelet/fantasy/openrouter"
+
+ "charm.land/fantasy/providers/anthropic"
+ "charm.land/fantasy/providers/azure"
+ "charm.land/fantasy/providers/google"
+ "charm.land/fantasy/providers/openai"
+ "charm.land/fantasy/providers/openaicompat"
+ "charm.land/fantasy/providers/openrouter"
"github.com/qjebbs/go-jsons"
)
type Coordinator interface {
// INFO: (kujtim) this is not used yet we will use this when we have multiple agents
// SetMainAgent(string)
- Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*ai.AgentResult, error)
+ Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
Cancel(sessionID string)
CancelAll()
IsSessionBusy(sessionID string) bool
@@ -98,7 +99,7 @@ func NewCoordinator(
}
// Run implements Coordinator.
-func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*ai.AgentResult, error) {
+func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error) {
model := c.currentAgent.Model()
maxTokens := model.CatwalkCfg.DefaultMaxTokens
if model.ModelCfg.MaxTokens != 0 {
@@ -130,8 +131,8 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
})
}
-func getProviderOptions(model Model, tp catwalk.Type) ai.ProviderOptions {
- options := ai.ProviderOptions{}
+func getProviderOptions(model Model, tp catwalk.Type) fantasy.ProviderOptions {
+ options := fantasy.ProviderOptions{}
cfgOpts := []byte("{}")
catwalkOpts := []byte("{}")
@@ -233,7 +234,7 @@ func getProviderOptions(model Model, tp catwalk.Type) ai.ProviderOptions {
return options
}
-func mergeCallOptions(model Model, tp catwalk.Type) (ai.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
+func mergeCallOptions(model Model, tp catwalk.Type) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
modelOptions := getProviderOptions(model, tp)
temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
@@ -261,8 +262,8 @@ func (c *coordinator) buildAgent(prompt *prompt.Prompt, agent config.Agent) (Ses
return NewSessionAgent(SessionAgentOptions{large, small, systemPrompt, c.cfg.Options.DisableAutoSummarize, c.sessions, c.messages, tools}), nil
}
-func (c *coordinator) buildTools(agent config.Agent) ([]ai.AgentTool, error) {
- var allTools []ai.AgentTool
+func (c *coordinator) buildTools(agent config.Agent) ([]fantasy.AgentTool, error) {
+ var allTools []fantasy.AgentTool
if slices.Contains(agent.AllowedTools, AgentToolName) {
agentTool, err := c.agentTool()
if err != nil {
@@ -285,7 +286,7 @@ func (c *coordinator) buildTools(agent config.Agent) ([]ai.AgentTool, error) {
tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
)
- var filteredTools []ai.AgentTool
+ var filteredTools []fantasy.AgentTool
for _, tool := range allTools {
if slices.Contains(agent.AllowedTools, tool.Info().Name) {
filteredTools = append(filteredTools, tool)
@@ -394,7 +395,7 @@ func (c *coordinator) buildAgentModels() (Model, Model, error) {
}, nil
}
-func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
+func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
hasBearerAuth := false
for key := range headers {
if strings.ToLower(key) == "authorization" {
@@ -429,7 +430,7 @@ func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map
return anthropic.New(opts...)
}
-func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
+func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
opts := []openai.Option{
openai.WithAPIKey(apiKey),
}
@@ -446,7 +447,7 @@ func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[st
return openai.New(opts...)
}
-func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) ai.Provider {
+func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) fantasy.Provider {
opts := []openrouter.Option{
openrouter.WithAPIKey(apiKey),
}
@@ -460,7 +461,7 @@ func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[stri
return openrouter.New(opts...)
}
-func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
+func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
opts := []openaicompat.Option{
openaicompat.WithBaseURL(baseURL),
openaicompat.WithAPIKey(apiKey),
@@ -476,7 +477,7 @@ func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers
return openaicompat.New(opts...)
}
-func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) ai.Provider {
+func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) fantasy.Provider {
opts := []azure.Option{
azure.WithBaseURL(baseURL),
azure.WithAPIKey(apiKey),
@@ -498,7 +499,7 @@ func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[str
return azure.New(opts...)
}
-func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
+func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
opts := []google.Option{
google.WithBaseURL(baseURL),
google.WithGeminiAPIKey(apiKey),
@@ -513,7 +514,7 @@ func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[st
return google.New(opts...)
}
-func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) ai.Provider {
+func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) fantasy.Provider {
opts := []google.Option{}
if c.cfg.Options.Debug {
httpClient := log.NewHTTPClient()
@@ -550,7 +551,7 @@ func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
return false
}
-func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (ai.Provider, error) {
+func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
headers := providerCfg.ExtraHeaders
// handle special headers for anthropic
@@ -561,7 +562,7 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
// TODO: make sure we have
apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
- var provider ai.Provider
+ var provider fantasy.Provider
switch providerCfg.Type {
case openai.Name:
provider = c.buildOpenaiProvider(baseURL, apiKey, headers)
diff --git a/internal/agent/tools/bash.go b/internal/agent/tools/bash.go
index f125d5ff1ca8262db99a10e5e895f04dc8fceef9..8b01887f4de2051aeeca5034c747a1adf744a039 100644
--- a/internal/agent/tools/bash.go
+++ b/internal/agent/tools/bash.go
@@ -9,10 +9,10 @@ import (
"strings"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/shell"
- "github.com/charmbracelet/fantasy/ai"
)
type BashParams struct {
@@ -177,14 +177,14 @@ func blockFuncs() []shell.BlockFunc {
}
}
-func NewBashTool(permissions permission.Service, workingDir string, attribution *config.Attribution) ai.AgentTool {
+func NewBashTool(permissions permission.Service, workingDir string, attribution *config.Attribution) fantasy.AgentTool {
// Set up command blocking on the persistent shell
persistentShell := shell.GetPersistentShell(workingDir)
persistentShell.SetBlockFuncs(blockFuncs())
- return ai.NewAgentTool(
+ return fantasy.NewAgentTool(
BashToolName,
string(bashDescription(attribution)),
- func(ctx context.Context, params BashParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params BashParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Timeout > MaxTimeout {
params.Timeout = MaxTimeout
} else if params.Timeout <= 0 {
@@ -192,7 +192,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
}
if params.Command == "" {
- return ai.NewTextErrorResponse("missing command"), nil
+ return fantasy.NewTextErrorResponse("missing command"), nil
}
isSafeReadOnly := false
@@ -209,7 +209,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
sessionID := GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for executing shell command")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for executing shell command")
}
if !isSafeReadOnly {
shell := shell.GetPersistentShell(workingDir)
@@ -228,7 +228,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
},
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
}
startTime := time.Now()
@@ -246,7 +246,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
interrupted := shell.IsInterrupt(err)
exitCode := shell.ExitCode(err)
if exitCode == 0 && !interrupted && err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error executing command: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error executing command: %w", err)
}
stdout = truncateOutput(stdout)
@@ -287,10 +287,10 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
WorkingDirectory: currentWorkingDir,
}
if stdout == "" {
- return ai.WithResponseMetadata(ai.NewTextResponse(BashNoOutput), metadata), nil
+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(BashNoOutput), metadata), nil
}
stdout += fmt.Sprintf("\n\n%s", currentWorkingDir)
- return ai.WithResponseMetadata(ai.NewTextResponse(stdout), metadata), nil
+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(stdout), metadata), nil
})
}
diff --git a/internal/agent/tools/diagnostics.go b/internal/agent/tools/diagnostics.go
index e2d27450f2e6d23e63cdf2359719c17df622221d..5eceaf0245444341c2c5589c8c14d56869da2a58 100644
--- a/internal/agent/tools/diagnostics.go
+++ b/internal/agent/tools/diagnostics.go
@@ -9,9 +9,9 @@ import (
"strings"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/lsp"
- "github.com/charmbracelet/fantasy/ai"
"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
)
@@ -24,17 +24,17 @@ const DiagnosticsToolName = "diagnostics"
//go:embed diagnostics.md
var diagnosticsDescription []byte
-func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) ai.AgentTool {
- return ai.NewAgentTool(
+func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
DiagnosticsToolName,
string(diagnosticsDescription),
- func(ctx context.Context, params DiagnosticsParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params DiagnosticsParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if lspClients.Len() == 0 {
- return ai.NewTextErrorResponse("no LSP clients available"), nil
+ return fantasy.NewTextErrorResponse("no LSP clients available"), nil
}
notifyLSPs(ctx, lspClients, params.FilePath)
output := getDiagnostics(params.FilePath, lspClients)
- return ai.NewTextResponse(output), nil
+ return fantasy.NewTextResponse(output), nil
})
}
diff --git a/internal/agent/tools/download.go b/internal/agent/tools/download.go
index 24ada06fa409a1b72983ccaff13c00bb830db567..cae5e5d6ab2f64f5e62521bf1f9379411dfbe0ad 100644
--- a/internal/agent/tools/download.go
+++ b/internal/agent/tools/download.go
@@ -11,8 +11,8 @@ import (
"strings"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/permission"
- "github.com/charmbracelet/fantasy/ai"
)
type DownloadParams struct {
@@ -32,7 +32,7 @@ const DownloadToolName = "download"
//go:embed download.md
var downloadDescription []byte
-func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) ai.AgentTool {
+func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
if client == nil {
client = &http.Client{
Timeout: 5 * time.Minute, // Default 5 minute timeout for downloads
@@ -43,20 +43,20 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
},
}
}
- return ai.NewAgentTool(
+ return fantasy.NewAgentTool(
DownloadToolName,
string(downloadDescription),
- func(ctx context.Context, params DownloadParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.URL == "" {
- return ai.NewTextErrorResponse("URL parameter is required"), nil
+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
}
if params.FilePath == "" {
- return ai.NewTextErrorResponse("file_path parameter is required"), nil
+ return fantasy.NewTextErrorResponse("file_path parameter is required"), nil
}
if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
- return ai.NewTextErrorResponse("URL must start with http:// or https://"), nil
+ return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
}
// Convert relative path to absolute path
@@ -69,7 +69,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
sessionID := GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for downloading files")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for downloading files")
}
p := permissions.Request(
@@ -84,7 +84,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
// Handle timeout with context
@@ -101,36 +101,36 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "crush/1.0")
resp, err := client.Do(req)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to download from URL: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to download from URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
}
// Check content length if available
maxSize := int64(100 * 1024 * 1024) // 100MB
if resp.ContentLength > maxSize {
- return ai.NewTextErrorResponse(fmt.Sprintf("File too large: %d bytes (max %d bytes)", resp.ContentLength, maxSize)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File too large: %d bytes (max %d bytes)", resp.ContentLength, maxSize)), nil
}
// Create parent directories if they don't exist
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
}
// Create the output file
outFile, err := os.Create(filePath)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to create output file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create output file: %w", err)
}
defer outFile.Close()
@@ -138,14 +138,14 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
limitedReader := io.LimitReader(resp.Body, maxSize)
bytesWritten, err := io.Copy(outFile, limitedReader)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
}
// Check if we hit the size limit
if bytesWritten == maxSize {
// Clean up the file since it might be incomplete
os.Remove(filePath)
- return ai.NewTextErrorResponse(fmt.Sprintf("File too large: exceeded %d bytes limit", maxSize)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File too large: exceeded %d bytes limit", maxSize)), nil
}
contentType := resp.Header.Get("Content-Type")
@@ -154,6 +154,6 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
responseMsg += fmt.Sprintf(" (Content-Type: %s)", contentType)
}
- return ai.NewTextResponse(responseMsg), nil
+ return fantasy.NewTextResponse(responseMsg), nil
})
}
diff --git a/internal/agent/tools/edit.go b/internal/agent/tools/edit.go
index 6b3246fda7ab4733060dba425b4196557cb56a5d..bcd6592f7c0693433bebcb4a76432269df1e2191 100644
--- a/internal/agent/tools/edit.go
+++ b/internal/agent/tools/edit.go
@@ -10,11 +10,11 @@ import (
"strings"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/diff"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/history"
- "github.com/charmbracelet/fantasy/ai"
"github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/permission"
@@ -52,20 +52,20 @@ type editContext struct {
workingDir string
}
-func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
- return ai.NewAgentTool(
+func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
EditToolName,
string(editDescription),
- func(ctx context.Context, params EditParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params EditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.FilePath == "" {
- return ai.NewTextErrorResponse("file_path is required"), nil
+ return fantasy.NewTextErrorResponse("file_path is required"), nil
}
if !filepath.IsAbs(params.FilePath) {
params.FilePath = filepath.Join(workingDir, params.FilePath)
}
- var response ai.ToolResponse
+ var response fantasy.ToolResponse
var err error
editCtx := editContext{ctx, permissions, files, workingDir}
@@ -103,25 +103,25 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
})
}
-func createNewFile(edit editContext, filePath, content string, call ai.ToolCall) (ai.ToolResponse, error) {
+func createNewFile(edit editContext, filePath, content string, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
fileInfo, err := os.Stat(filePath)
if err == nil {
if fileInfo.IsDir() {
- return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
}
- return ai.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
} else if !os.IsNotExist(err) {
- return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
}
dir := filepath.Dir(filePath)
if err = os.MkdirAll(dir, 0o755); err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
}
sessionID := GetSessionFromContext(edit.ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
}
_, additions, removals := diff.GenerateDiff(
@@ -145,19 +145,19 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
},
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
err = os.WriteFile(filePath, []byte(content), 0o644)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
}
// File can't be in the history so we create a new file history
_, err = edit.files.Create(edit.ctx, sessionID, filePath, "")
if err != nil {
// Log error but don't fail the operation
- return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
}
// Add the new content to the file history
@@ -170,8 +170,8 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
recordFileWrite(filePath)
recordFileRead(filePath)
- return ai.WithResponseMetadata(
- ai.NewTextResponse("File created: "+filePath),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse("File created: "+filePath),
EditResponseMetadata{
OldContent: "",
NewContent: content,
@@ -181,27 +181,27 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
), nil
}
-func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
+func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
- return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
}
- return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
}
if fileInfo.IsDir() {
- return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
}
if getLastReadTime(filePath).IsZero() {
- return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
}
modTime := fileInfo.ModTime()
lastRead := getLastReadTime(filePath)
if modTime.After(lastRead) {
- return ai.NewTextErrorResponse(
+ return fantasy.NewTextErrorResponse(
fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
)), nil
@@ -209,7 +209,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
content, err := os.ReadFile(filePath)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
}
oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -221,17 +221,17 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
newContent = strings.ReplaceAll(oldContent, oldString, "")
deletionCount = strings.Count(oldContent, oldString)
if deletionCount == 0 {
- return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
}
} else {
index := strings.Index(oldContent, oldString)
if index == -1 {
- return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
}
lastIndex := strings.LastIndex(oldContent, oldString)
if index != lastIndex {
- return ai.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
+ return fantasy.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
}
newContent = oldContent[:index] + oldContent[index+len(oldString):]
@@ -241,7 +241,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
sessionID := GetSessionFromContext(edit.ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
}
_, additions, removals := diff.GenerateDiff(
@@ -266,7 +266,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
},
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
if isCrlf {
@@ -275,7 +275,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
err = os.WriteFile(filePath, []byte(newContent), 0o644)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
}
// Check if file exists in history
@@ -284,7 +284,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
_, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
if err != nil {
// Log error but don't fail the operation
- return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
}
}
if file.Content != oldContent {
@@ -303,8 +303,8 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
recordFileWrite(filePath)
recordFileRead(filePath)
- return ai.WithResponseMetadata(
- ai.NewTextResponse("Content deleted from file: "+filePath),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse("Content deleted from file: "+filePath),
EditResponseMetadata{
OldContent: oldContent,
NewContent: newContent,
@@ -314,27 +314,27 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
), nil
}
-func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
+func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
- return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
}
- return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
}
if fileInfo.IsDir() {
- return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
}
if getLastReadTime(filePath).IsZero() {
- return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
}
modTime := fileInfo.ModTime()
lastRead := getLastReadTime(filePath)
if modTime.After(lastRead) {
- return ai.NewTextErrorResponse(
+ return fantasy.NewTextErrorResponse(
fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
)), nil
@@ -342,7 +342,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
content, err := os.ReadFile(filePath)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
}
oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -354,17 +354,17 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
newContent = strings.ReplaceAll(oldContent, oldString, newString)
replacementCount = strings.Count(oldContent, oldString)
if replacementCount == 0 {
- return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
}
} else {
index := strings.Index(oldContent, oldString)
if index == -1 {
- return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
}
lastIndex := strings.LastIndex(oldContent, oldString)
if index != lastIndex {
- return ai.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
+ return fantasy.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
}
newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
@@ -372,12 +372,12 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
}
if oldContent == newContent {
- return ai.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
+ return fantasy.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
}
sessionID := GetSessionFromContext(edit.ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
}
_, additions, removals := diff.GenerateDiff(
oldContent,
@@ -401,7 +401,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
},
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
if isCrlf {
@@ -410,7 +410,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
err = os.WriteFile(filePath, []byte(newContent), 0o644)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
}
// Check if file exists in history
@@ -419,7 +419,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
_, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
if err != nil {
// Log error but don't fail the operation
- return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
}
}
if file.Content != oldContent {
@@ -438,8 +438,8 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
recordFileWrite(filePath)
recordFileRead(filePath)
- return ai.WithResponseMetadata(
- ai.NewTextResponse("Content replaced in file: "+filePath),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse("Content replaced in file: "+filePath),
EditResponseMetadata{
OldContent: oldContent,
NewContent: newContent,
diff --git a/internal/agent/tools/fetch.go b/internal/agent/tools/fetch.go
index 2079b7f4f765e0c18ac266bd1b65f19627c2a342..0701bd2be151213f2fd0e313726d03a5c35833a3 100644
--- a/internal/agent/tools/fetch.go
+++ b/internal/agent/tools/fetch.go
@@ -10,10 +10,10 @@ import (
"time"
"unicode/utf8"
+ "charm.land/fantasy"
md "github.com/JohannesKaufmann/html-to-markdown"
"github.com/PuerkitoBio/goquery"
"github.com/charmbracelet/crush/internal/permission"
- "github.com/charmbracelet/fantasy/ai"
)
type FetchParams struct {
@@ -39,7 +39,7 @@ const FetchToolName = "fetch"
//go:embed fetch.md
var fetchDescription []byte
-func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) ai.AgentTool {
+func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
if client == nil {
client = &http.Client{
Timeout: 30 * time.Second,
@@ -51,26 +51,26 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
}
}
- return ai.NewAgentTool(
+ return fantasy.NewAgentTool(
FetchToolName,
string(fetchDescription),
- func(ctx context.Context, params FetchParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.URL == "" {
- return ai.NewTextErrorResponse("URL parameter is required"), nil
+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
}
format := strings.ToLower(params.Format)
if format != "text" && format != "markdown" && format != "html" {
- return ai.NewTextErrorResponse("Format must be one of: text, markdown, html"), nil
+ return fantasy.NewTextErrorResponse("Format must be one of: text, markdown, html"), nil
}
if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
- return ai.NewTextErrorResponse("URL must start with http:// or https://"), nil
+ return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
}
sessionID := GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
}
p := permissions.Request(
@@ -86,7 +86,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
// Handle timeout with context
@@ -103,32 +103,32 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "crush/1.0")
resp, err := client.Do(req)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
}
maxSize := int64(5 * 1024 * 1024) // 5MB
body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
if err != nil {
- return ai.NewTextErrorResponse("Failed to read response body: " + err.Error()), nil
+ return fantasy.NewTextErrorResponse("Failed to read response body: " + err.Error()), nil
}
content := string(body)
isValidUt8 := utf8.ValidString(content)
if !isValidUt8 {
- return ai.NewTextErrorResponse("Response content is not valid UTF-8"), nil
+ return fantasy.NewTextErrorResponse("Response content is not valid UTF-8"), nil
}
contentType := resp.Header.Get("Content-Type")
@@ -137,7 +137,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
if strings.Contains(contentType, "text/html") {
text, err := extractTextFromHTML(content)
if err != nil {
- return ai.NewTextErrorResponse("Failed to extract text from HTML: " + err.Error()), nil
+ return fantasy.NewTextErrorResponse("Failed to extract text from HTML: " + err.Error()), nil
}
content = text
}
@@ -146,7 +146,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
if strings.Contains(contentType, "text/html") {
markdown, err := convertHTMLToMarkdown(content)
if err != nil {
- return ai.NewTextErrorResponse("Failed to convert HTML to Markdown: " + err.Error()), nil
+ return fantasy.NewTextErrorResponse("Failed to convert HTML to Markdown: " + err.Error()), nil
}
content = markdown
}
@@ -158,14 +158,14 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
if strings.Contains(contentType, "text/html") {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
if err != nil {
- return ai.NewTextErrorResponse("Failed to parse HTML: " + err.Error()), nil
+ return fantasy.NewTextErrorResponse("Failed to parse HTML: " + err.Error()), nil
}
body, err := doc.Find("body").Html()
if err != nil {
- return ai.NewTextErrorResponse("Failed to extract body from HTML: " + err.Error()), nil
+ return fantasy.NewTextErrorResponse("Failed to extract body from HTML: " + err.Error()), nil
}
if body == "" {
- return ai.NewTextErrorResponse("No body content found in HTML"), nil
+ return fantasy.NewTextErrorResponse("No body content found in HTML"), nil
}
content = "\n
\n" + body + "\n\n"
}
@@ -177,7 +177,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
content += fmt.Sprintf("\n\n[Content truncated to %d bytes]", MaxReadSize)
}
- return ai.NewTextResponse(content), nil
+ return fantasy.NewTextResponse(content), nil
})
}
diff --git a/internal/agent/tools/glob.go b/internal/agent/tools/glob.go
index fd264a2aa01f46e266c0c3cc99ab20a91caeb63e..29396dbd969787889a44a013f75a4f8fcc781354 100644
--- a/internal/agent/tools/glob.go
+++ b/internal/agent/tools/glob.go
@@ -11,8 +11,8 @@ import (
"sort"
"strings"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/fsext"
- "github.com/charmbracelet/fantasy/ai"
)
const GlobToolName = "glob"
@@ -30,13 +30,13 @@ type GlobResponseMetadata struct {
Truncated bool `json:"truncated"`
}
-func NewGlobTool(workingDir string) ai.AgentTool {
- return ai.NewAgentTool(
+func NewGlobTool(workingDir string) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
GlobToolName,
string(globDescription),
- func(ctx context.Context, params GlobParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params GlobParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Pattern == "" {
- return ai.NewTextErrorResponse("pattern is required"), nil
+ return fantasy.NewTextErrorResponse("pattern is required"), nil
}
searchPath := params.Path
@@ -46,7 +46,7 @@ func NewGlobTool(workingDir string) ai.AgentTool {
files, truncated, err := globFiles(ctx, params.Pattern, searchPath, 100)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error finding files: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error finding files: %w", err)
}
var output string
@@ -59,8 +59,8 @@ func NewGlobTool(workingDir string) ai.AgentTool {
}
}
- return ai.WithResponseMetadata(
- ai.NewTextResponse(output),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse(output),
GlobResponseMetadata{
NumberOfFiles: len(files),
Truncated: truncated,
diff --git a/internal/agent/tools/grep.go b/internal/agent/tools/grep.go
index e299d7d71a3046f33fb27d14aa7870d20ef9670c..e10a24a0685bcd37f39cfa74ec9837cbecf69507 100644
--- a/internal/agent/tools/grep.go
+++ b/internal/agent/tools/grep.go
@@ -16,8 +16,8 @@ import (
"sync"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/fsext"
- "github.com/charmbracelet/fantasy/ai"
)
// regexCache provides thread-safe caching of compiled regex patterns
@@ -97,13 +97,13 @@ const (
//go:embed grep.md
var grepDescription []byte
-func NewGrepTool(workingDir string) ai.AgentTool {
- return ai.NewAgentTool(
+func NewGrepTool(workingDir string) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
GrepToolName,
string(grepDescription),
- func(ctx context.Context, params GrepParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params GrepParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Pattern == "" {
- return ai.NewTextErrorResponse("pattern is required"), nil
+ return fantasy.NewTextErrorResponse("pattern is required"), nil
}
// If literal_text is true, escape the pattern
@@ -119,7 +119,7 @@ func NewGrepTool(workingDir string) ai.AgentTool {
matches, truncated, err := searchFiles(ctx, searchPattern, searchPath, params.Include, 100)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error searching files: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error searching files: %w", err)
}
var output strings.Builder
@@ -153,8 +153,8 @@ func NewGrepTool(workingDir string) ai.AgentTool {
}
}
- return ai.WithResponseMetadata(
- ai.NewTextResponse(output.String()),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse(output.String()),
GrepResponseMetadata{
NumberOfMatches: len(matches),
Truncated: truncated,
diff --git a/internal/agent/tools/ls.go b/internal/agent/tools/ls.go
index 20a68cf89b761f4cdf736318e40dd880baabefdd..6e8bab1ec5ebb025a0efad870fb31afca1ee9e7d 100644
--- a/internal/agent/tools/ls.go
+++ b/internal/agent/tools/ls.go
@@ -9,10 +9,10 @@ import (
"path/filepath"
"strings"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/permission"
- "github.com/charmbracelet/fantasy/ai"
)
type LSParams struct {
@@ -47,14 +47,14 @@ const (
//go:embed ls.md
var lsDescription []byte
-func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) ai.AgentTool {
- return ai.NewAgentTool(
+func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
LSToolName,
string(lsDescription),
- func(ctx context.Context, params LSParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params LSParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error expanding path: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error expanding path: %w", err)
}
if !filepath.IsAbs(searchPath) {
@@ -64,12 +64,12 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
// Check if directory is outside working directory and request permission if needed
absWorkingDir, err := filepath.Abs(workingDir)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
}
absSearchPath, err := filepath.Abs(searchPath)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
}
relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
@@ -77,7 +77,7 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
// Directory is outside working directory, request permission
sessionID := GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for accessing directories outside working directory")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for accessing directories outside working directory")
}
granted := permissions.Request(
@@ -93,17 +93,17 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
)
if !granted {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
}
output, metadata, err := ListDirectoryTree(searchPath, params, lsConfig)
if err != nil {
- return ai.ToolResponse{}, err
+ return fantasy.ToolResponse{}, err
}
- return ai.WithResponseMetadata(
- ai.NewTextResponse(output),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse(output),
metadata,
), nil
})
diff --git a/internal/agent/tools/mcp-tools.go b/internal/agent/tools/mcp-tools.go
index 9c34c0f25033b365d382b1a14f92dfedb8c141ea..c6ffda4d75137a1acfc69cd176c699a087e00b06 100644
--- a/internal/agent/tools/mcp-tools.go
+++ b/internal/agent/tools/mcp-tools.go
@@ -16,13 +16,13 @@ import (
"sync"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/version"
- "github.com/charmbracelet/fantasy/ai"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@@ -92,14 +92,14 @@ type McpTool struct {
tool *mcp.Tool
permissions permission.Service
workingDir string
- providerOptions ai.ProviderOptions
+ providerOptions fantasy.ProviderOptions
}
-func (m *McpTool) SetProviderOptions(opts ai.ProviderOptions) {
+func (m *McpTool) SetProviderOptions(opts fantasy.ProviderOptions) {
m.providerOptions = opts
}
-func (m *McpTool) ProviderOptions() ai.ProviderOptions {
+func (m *McpTool) ProviderOptions() fantasy.ProviderOptions {
return m.providerOptions
}
@@ -115,7 +115,7 @@ func (m *McpTool) MCPToolName() string {
return m.tool.Name
}
-func (b *McpTool) Info() ai.ToolInfo {
+func (b *McpTool) Info() fantasy.ToolInfo {
input := b.tool.InputSchema.(map[string]any)
required, _ := input["required"].([]string)
if required == nil {
@@ -125,7 +125,7 @@ func (b *McpTool) Info() ai.ToolInfo {
if parameters == nil {
parameters = make(map[string]any)
}
- return ai.ToolInfo{
+ return fantasy.ToolInfo{
Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
Description: b.tool.Description,
Parameters: parameters,
@@ -133,22 +133,22 @@ func (b *McpTool) Info() ai.ToolInfo {
}
}
-func runTool(ctx context.Context, name, toolName string, input string) (ai.ToolResponse, error) {
+func runTool(ctx context.Context, name, toolName string, input string) (fantasy.ToolResponse, error) {
var args map[string]any
if err := json.Unmarshal([]byte(input), &args); err != nil {
- return ai.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
}
c, err := getOrRenewClient(ctx, name)
if err != nil {
- return ai.NewTextErrorResponse(err.Error()), nil
+ return fantasy.NewTextErrorResponse(err.Error()), nil
}
result, err := c.CallTool(ctx, &mcp.CallToolParams{
Name: toolName,
Arguments: args,
})
if err != nil {
- return ai.NewTextErrorResponse(err.Error()), nil
+ return fantasy.NewTextErrorResponse(err.Error()), nil
}
output := make([]string, 0, len(result.Content))
@@ -159,7 +159,7 @@ func runTool(ctx context.Context, name, toolName string, input string) (ai.ToolR
output = append(output, fmt.Sprintf("%v", v))
}
}
- return ai.NewTextResponse(strings.Join(output, "\n")), nil
+ return fantasy.NewTextResponse(strings.Join(output, "\n")), nil
}
func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
@@ -191,10 +191,10 @@ func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, err
return sess, nil
}
-func (m *McpTool) Run(ctx context.Context, params ai.ToolCall) (ai.ToolResponse, error) {
+func (m *McpTool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
sessionID := GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
}
permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name)
p := m.permissions.Request(
@@ -209,7 +209,7 @@ func (m *McpTool) Run(ctx context.Context, params ai.ToolCall) (ai.ToolResponse,
},
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
return runTool(ctx, m.mcpName, m.tool.Name, params.Input)
diff --git a/internal/agent/tools/multiedit.go b/internal/agent/tools/multiedit.go
index 0dffcf6e819ec4cdb35863986bc93729760ef672..a0f6d3c83decde013a829b4c4fe13902545bc9db 100644
--- a/internal/agent/tools/multiedit.go
+++ b/internal/agent/tools/multiedit.go
@@ -10,13 +10,13 @@ import (
"strings"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/diff"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/history"
"github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/permission"
- "github.com/charmbracelet/fantasy/ai"
)
type MultiEditOperation struct {
@@ -49,17 +49,17 @@ const MultiEditToolName = "multiedit"
//go:embed multiedit.md
var multieditDescription []byte
-func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
- return ai.NewAgentTool(
+func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
MultiEditToolName,
string(multieditDescription),
- func(ctx context.Context, params MultiEditParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params MultiEditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.FilePath == "" {
- return ai.NewTextErrorResponse("file_path is required"), nil
+ return fantasy.NewTextErrorResponse("file_path is required"), nil
}
if len(params.Edits) == 0 {
- return ai.NewTextErrorResponse("at least one edit operation is required"), nil
+ return fantasy.NewTextErrorResponse("at least one edit operation is required"), nil
}
if !filepath.IsAbs(params.FilePath) {
@@ -68,10 +68,10 @@ func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions pe
// Validate all edits before applying any
if err := validateEdits(params.Edits); err != nil {
- return ai.NewTextErrorResponse(err.Error()), nil
+ return fantasy.NewTextErrorResponse(err.Error()), nil
}
- var response ai.ToolResponse
+ var response fantasy.ToolResponse
var err error
editCtx := editContext{ctx, permissions, files, workingDir}
@@ -114,24 +114,24 @@ func validateEdits(edits []MultiEditOperation) error {
return nil
}
-func processMultiEditWithCreation(edit editContext, params MultiEditParams, call ai.ToolCall) (ai.ToolResponse, error) {
+func processMultiEditWithCreation(edit editContext, params MultiEditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
// First edit creates the file
firstEdit := params.Edits[0]
if firstEdit.OldString != "" {
- return ai.NewTextErrorResponse("first edit must have empty old_string for file creation"), nil
+ return fantasy.NewTextErrorResponse("first edit must have empty old_string for file creation"), nil
}
// Check if file already exists
if _, err := os.Stat(params.FilePath); err == nil {
- return ai.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", params.FilePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", params.FilePath)), nil
} else if !os.IsNotExist(err) {
- return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
}
// Create parent directories
dir := filepath.Dir(params.FilePath)
if err := os.MkdirAll(dir, 0o755); err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
}
// Start with the content from the first edit
@@ -142,7 +142,7 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
edit := params.Edits[i]
newContent, err := applyEditToContent(currentContent, edit)
if err != nil {
- return ai.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
}
currentContent = newContent
}
@@ -150,7 +150,7 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
// Get session and message IDs
sessionID := GetSessionFromContext(edit.ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
}
// Check permissions
@@ -170,19 +170,19 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
},
})
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
// Write the file
err := os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
}
// Update file history
_, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, "")
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
}
_, err = edit.files.CreateVersion(edit.ctx, sessionID, params.FilePath, currentContent)
@@ -193,8 +193,8 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
recordFileWrite(params.FilePath)
recordFileRead(params.FilePath)
- return ai.WithResponseMetadata(
- ai.NewTextResponse(fmt.Sprintf("File created with %d edits: %s", len(params.Edits), params.FilePath)),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse(fmt.Sprintf("File created with %d edits: %s", len(params.Edits), params.FilePath)),
MultiEditResponseMetadata{
OldContent: "",
NewContent: currentContent,
@@ -205,30 +205,30 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
), nil
}
-func processMultiEditExistingFile(edit editContext, params MultiEditParams, call ai.ToolCall) (ai.ToolResponse, error) {
+func processMultiEditExistingFile(edit editContext, params MultiEditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
// Validate file exists and is readable
fileInfo, err := os.Stat(params.FilePath)
if err != nil {
if os.IsNotExist(err) {
- return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", params.FilePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", params.FilePath)), nil
}
- return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
}
if fileInfo.IsDir() {
- return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", params.FilePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", params.FilePath)), nil
}
// Check if file was read before editing
if getLastReadTime(params.FilePath).IsZero() {
- return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
}
// Check if file was modified since last read
modTime := fileInfo.ModTime()
lastRead := getLastReadTime(params.FilePath)
if modTime.After(lastRead) {
- return ai.NewTextErrorResponse(
+ return fantasy.NewTextErrorResponse(
fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
params.FilePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
)), nil
@@ -237,7 +237,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
// Read current file content
content, err := os.ReadFile(params.FilePath)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
}
oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -247,20 +247,20 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
for i, edit := range params.Edits {
newContent, err := applyEditToContent(currentContent, edit)
if err != nil {
- return ai.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
}
currentContent = newContent
}
// Check if content actually changed
if oldContent == currentContent {
- return ai.NewTextErrorResponse("no changes made - all edits resulted in identical content"), nil
+ return fantasy.NewTextErrorResponse("no changes made - all edits resulted in identical content"), nil
}
// Get session and message IDs
sessionID := GetSessionFromContext(edit.ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for editing file")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for editing file")
}
// Generate diff and check permissions
@@ -279,7 +279,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
},
})
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
if isCrlf {
@@ -289,7 +289,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
// Write the updated content
err = os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
}
// Update file history
@@ -297,7 +297,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
if err != nil {
_, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, oldContent)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
}
}
if file.Content != oldContent {
@@ -317,8 +317,8 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
recordFileWrite(params.FilePath)
recordFileRead(params.FilePath)
- return ai.WithResponseMetadata(
- ai.NewTextResponse(fmt.Sprintf("Applied %d edits to file: %s", len(params.Edits), params.FilePath)),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse(fmt.Sprintf("Applied %d edits to file: %s", len(params.Edits), params.FilePath)),
MultiEditResponseMetadata{
OldContent: oldContent,
NewContent: currentContent,
diff --git a/internal/agent/tools/sourcegraph.go b/internal/agent/tools/sourcegraph.go
index f44f1530be4c4528d7c5e5500c04d2f7c6ce93a4..5216405e5538edbcd10386c2640f48ced69eb22b 100644
--- a/internal/agent/tools/sourcegraph.go
+++ b/internal/agent/tools/sourcegraph.go
@@ -11,7 +11,7 @@ import (
"strings"
"time"
- "github.com/charmbracelet/fantasy/ai"
+ "charm.land/fantasy"
)
type SourcegraphParams struct {
@@ -31,7 +31,7 @@ const SourcegraphToolName = "sourcegraph"
//go:embed sourcegraph.md
var sourcegraphDescription []byte
-func NewSourcegraphTool(client *http.Client) ai.AgentTool {
+func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
if client == nil {
client = &http.Client{
Timeout: 30 * time.Second,
@@ -42,12 +42,12 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
},
}
}
- return ai.NewAgentTool(
+ return fantasy.NewAgentTool(
SourcegraphToolName,
string(sourcegraphDescription),
- func(ctx context.Context, params SourcegraphParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.Query == "" {
- return ai.NewTextErrorResponse("Query parameter is required"), nil
+ return fantasy.NewTextErrorResponse("Query parameter is required"), nil
}
if params.Count <= 0 {
@@ -86,7 +86,7 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
graphqlQueryBytes, err := json.Marshal(request)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to marshal GraphQL request: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to marshal GraphQL request: %w", err)
}
graphqlQuery := string(graphqlQueryBytes)
@@ -97,7 +97,7 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
bytes.NewBuffer([]byte(graphqlQuery)),
)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
@@ -105,34 +105,34 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
resp, err := client.Do(req)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
if len(body) > 0 {
- return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d, response: %s", resp.StatusCode, string(body))), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d, response: %s", resp.StatusCode, string(body))), nil
}
- return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
}
body, err := io.ReadAll(resp.Body)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to read response body: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read response body: %w", err)
}
var result map[string]any
if err = json.Unmarshal(body, &result); err != nil {
- return ai.ToolResponse{}, fmt.Errorf("failed to unmarshal response: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("failed to unmarshal response: %w", err)
}
formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
if err != nil {
- return ai.NewTextErrorResponse("Failed to format results: " + err.Error()), nil
+ return fantasy.NewTextErrorResponse("Failed to format results: " + err.Error()), nil
}
- return ai.NewTextResponse(formattedResults), nil
+ return fantasy.NewTextResponse(formattedResults), nil
})
}
diff --git a/internal/agent/tools/view.go b/internal/agent/tools/view.go
index 545f5a348c6a02cea8728f56f0b6590a4969ca69..af9212acd9f9207a8e1b82b0f7e11aa2a1618994 100644
--- a/internal/agent/tools/view.go
+++ b/internal/agent/tools/view.go
@@ -11,10 +11,10 @@ import (
"strings"
"unicode/utf8"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/permission"
- "github.com/charmbracelet/fantasy/ai"
)
//go:embed view.md
@@ -50,13 +50,13 @@ const (
MaxLineLength = 2000
)
-func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) ai.AgentTool {
- return ai.NewAgentTool(
+func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
ViewToolName,
string(viewDescription),
- func(ctx context.Context, params ViewParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params ViewParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.FilePath == "" {
- return ai.NewTextErrorResponse("file_path is required"), nil
+ return fantasy.NewTextErrorResponse("file_path is required"), nil
}
// Handle relative paths
@@ -68,12 +68,12 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
// Check if file is outside working directory and request permission if needed
absWorkingDir, err := filepath.Abs(workingDir)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
}
absFilePath, err := filepath.Abs(filePath)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
}
relPath, err := filepath.Rel(absWorkingDir, absFilePath)
@@ -81,7 +81,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
// File is outside working directory, request permission
sessionID := GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session ID is required for accessing files outside working directory")
+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for accessing files outside working directory")
}
granted := permissions.Request(
@@ -97,7 +97,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
)
if !granted {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
}
@@ -123,24 +123,24 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
}
if len(suggestions) > 0 {
- return ai.NewTextErrorResponse(fmt.Sprintf("File not found: %s\n\nDid you mean one of these?\n%s",
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File not found: %s\n\nDid you mean one of these?\n%s",
filePath, strings.Join(suggestions, "\n"))), nil
}
}
- return ai.NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
}
- return ai.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
}
// Check if it's a directory
if fileInfo.IsDir() {
- return ai.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
}
// Check file size
if fileInfo.Size() > MaxReadSize {
- return ai.NewTextErrorResponse(fmt.Sprintf("File is too large (%d bytes). Maximum size is %d bytes",
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File is too large (%d bytes). Maximum size is %d bytes",
fileInfo.Size(), MaxReadSize)), nil
}
@@ -153,17 +153,17 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
isImage, imageType := isImageFile(filePath)
// TODO: handle images
if isImage {
- return ai.NewTextErrorResponse(fmt.Sprintf("This is an image file of type: %s\n", imageType)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("This is an image file of type: %s\n", imageType)), nil
}
// Read the file content
content, lineCount, err := readTextFile(filePath, params.Offset, params.Limit)
isValidUt8 := utf8.ValidString(content)
if !isValidUt8 {
- return ai.NewTextErrorResponse("File content is not valid UTF-8"), nil
+ return fantasy.NewTextErrorResponse("File content is not valid UTF-8"), nil
}
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error reading file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error reading file: %w", err)
}
notifyLSPs(ctx, lspClients, filePath)
@@ -179,8 +179,8 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
output += "\n\n"
output += getDiagnostics(filePath, lspClients)
recordFileRead(filePath)
- return ai.WithResponseMetadata(
- ai.NewTextResponse(output),
+ return fantasy.WithResponseMetadata(
+ fantasy.NewTextResponse(output),
ViewResponseMetadata{
FilePath: filePath,
Content: content,
diff --git a/internal/agent/tools/write.go b/internal/agent/tools/write.go
index 571360fe93b861c86bdf4cfd8e62eedd4cc6f424..6c62fb0d5c2a9454a02300c22b69a7a20574c470 100644
--- a/internal/agent/tools/write.go
+++ b/internal/agent/tools/write.go
@@ -10,11 +10,11 @@ import (
"strings"
"time"
+ "charm.land/fantasy"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/diff"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/history"
- "github.com/charmbracelet/fantasy/ai"
"github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/permission"
@@ -49,17 +49,17 @@ type WriteResponseMetadata struct {
const WriteToolName = "write"
-func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
- return ai.NewAgentTool(
+func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
+ return fantasy.NewAgentTool(
WriteToolName,
string(writeDescription),
- func(ctx context.Context, params WriteParams, call ai.ToolCall) (ai.ToolResponse, error) {
+ func(ctx context.Context, params WriteParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
if params.FilePath == "" {
- return ai.NewTextErrorResponse("file_path is required"), nil
+ return fantasy.NewTextErrorResponse("file_path is required"), nil
}
if params.Content == "" {
- return ai.NewTextErrorResponse("content is required"), nil
+ return fantasy.NewTextErrorResponse("content is required"), nil
}
filePath := params.FilePath
@@ -70,27 +70,27 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
fileInfo, err := os.Stat(filePath)
if err == nil {
if fileInfo.IsDir() {
- return ai.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
}
modTime := fileInfo.ModTime()
lastRead := getLastReadTime(filePath)
if modTime.After(lastRead) {
- return ai.NewTextErrorResponse(fmt.Sprintf("File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
}
oldContent, readErr := os.ReadFile(filePath)
if readErr == nil && string(oldContent) == params.Content {
- return ai.NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
}
} else if !os.IsNotExist(err) {
- return ai.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
}
dir := filepath.Dir(filePath)
if err = os.MkdirAll(dir, 0o755); err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
}
oldContent := ""
@@ -103,7 +103,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
sessionID := GetSessionFromContext(ctx)
if sessionID == "" {
- return ai.ToolResponse{}, fmt.Errorf("session_id is required")
+ return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
}
diff, additions, removals := diff.GenerateDiff(
@@ -128,12 +128,12 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
},
)
if !p {
- return ai.ToolResponse{}, permission.ErrorPermissionDenied
+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
err = os.WriteFile(filePath, []byte(params.Content), 0o644)
if err != nil {
- return ai.ToolResponse{}, fmt.Errorf("error writing file: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error writing file: %w", err)
}
// Check if file exists in history
@@ -142,7 +142,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
_, err = files.Create(ctx, sessionID, filePath, oldContent)
if err != nil {
// Log error but don't fail the operation
- return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
}
}
if file.Content != oldContent {
@@ -166,7 +166,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
result := fmt.Sprintf("File successfully written: %s", filePath)
result = fmt.Sprintf("\n%s\n", result)
result += getDiagnostics(filePath, lspClients)
- return ai.WithResponseMetadata(ai.NewTextResponse(result),
+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
WriteResponseMetadata{
Diff: diff,
Additions: additions,
diff --git a/internal/app/app.go b/internal/app/app.go
index 7d1904096931fe80ecee6292309305afc05994bc..8309d21057a4c00ef18e2094a611d59b597ccf27 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -9,6 +9,7 @@ import (
"sync"
"time"
+ "charm.land/fantasy"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/agent"
"github.com/charmbracelet/crush/internal/agent/tools"
@@ -23,7 +24,6 @@ import (
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
- "github.com/charmbracelet/fantasy/ai"
"github.com/charmbracelet/x/ansi"
)
@@ -145,7 +145,7 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
app.Permissions.AutoApproveSession(sess.ID)
type response struct {
- result *ai.AgentResult
+ result *fantasy.AgentResult
err error
}
done := make(chan response, 1)
diff --git a/internal/message/content.go b/internal/message/content.go
index e397946ab6a04439a55639d2934b6da761796e0d..6c95aea9e94b5730858361f77e36d35ec7cd1a2e 100644
--- a/internal/message/content.go
+++ b/internal/message/content.go
@@ -7,9 +7,9 @@ import (
"strings"
"time"
+ "charm.land/fantasy"
+ "charm.land/fantasy/providers/anthropic"
"github.com/charmbracelet/catwalk/pkg/catwalk"
- "github.com/charmbracelet/fantasy/ai"
- "github.com/charmbracelet/fantasy/anthropic"
)
type MessageRole string
@@ -390,35 +390,35 @@ func (m *Message) AddBinary(mimeType string, data []byte) {
m.Parts = append(m.Parts, BinaryContent{MIMEType: mimeType, Data: data})
}
-func (m *Message) ToAIMessage() []ai.Message {
- var messages []ai.Message
+func (m *Message) ToAIMessage() []fantasy.Message {
+ var messages []fantasy.Message
switch m.Role {
case User:
- var parts []ai.MessagePart
+ var parts []fantasy.MessagePart
text := strings.TrimSpace(m.Content().Text)
if text != "" {
- parts = append(parts, ai.TextPart{Text: text})
+ parts = append(parts, fantasy.TextPart{Text: text})
}
for _, content := range m.BinaryContent() {
- parts = append(parts, ai.FilePart{
+ parts = append(parts, fantasy.FilePart{
Filename: content.Path,
Data: content.Data,
MediaType: content.MIMEType,
})
}
- messages = append(messages, ai.Message{
- Role: ai.MessageRoleUser,
+ messages = append(messages, fantasy.Message{
+ Role: fantasy.MessageRoleUser,
Content: parts,
})
case Assistant:
- var parts []ai.MessagePart
+ var parts []fantasy.MessagePart
text := strings.TrimSpace(m.Content().Text)
if text != "" {
- parts = append(parts, ai.TextPart{Text: text})
+ parts = append(parts, fantasy.TextPart{Text: text})
}
reasoning := m.ReasoningContent()
if reasoning.Thinking != "" {
- reasoningPart := ai.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: ai.ProviderOptions{}}
+ reasoningPart := fantasy.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: fantasy.ProviderOptions{}}
if reasoning.Signature != "" {
reasoningPart.ProviderOptions["anthropic"] = &anthropic.ReasoningOptionMetadata{
Signature: reasoning.Signature,
@@ -427,42 +427,42 @@ func (m *Message) ToAIMessage() []ai.Message {
parts = append(parts, reasoningPart)
}
for _, call := range m.ToolCalls() {
- parts = append(parts, ai.ToolCallPart{
+ parts = append(parts, fantasy.ToolCallPart{
ToolCallID: call.ID,
ToolName: call.Name,
Input: call.Input,
ProviderExecuted: call.ProviderExecuted,
})
}
- messages = append(messages, ai.Message{
- Role: ai.MessageRoleAssistant,
+ messages = append(messages, fantasy.Message{
+ Role: fantasy.MessageRoleAssistant,
Content: parts,
})
case Tool:
- var parts []ai.MessagePart
+ var parts []fantasy.MessagePart
for _, result := range m.ToolResults() {
- var content ai.ToolResultOutputContent
+ var content fantasy.ToolResultOutputContent
if result.IsError {
- content = ai.ToolResultOutputContentError{
+ content = fantasy.ToolResultOutputContentError{
Error: errors.New(result.Content),
}
} else if result.Data != "" {
- content = ai.ToolResultOutputContentMedia{
+ content = fantasy.ToolResultOutputContentMedia{
Data: result.Data,
MediaType: result.MIMEType,
}
} else {
- content = ai.ToolResultOutputContentText{
+ content = fantasy.ToolResultOutputContentText{
Text: result.Content,
}
}
- parts = append(parts, ai.ToolResultPart{
+ parts = append(parts, fantasy.ToolResultPart{
ToolCallID: result.ToolCallID,
Output: content,
})
}
- messages = append(messages, ai.Message{
- Role: ai.MessageRoleTool,
+ messages = append(messages, fantasy.Message{
+ Role: fantasy.MessageRoleTool,
Content: parts,
})
}