chore: upgrade to latest fantasy

kujtimiihoxha created

Change summary

go.mod                              |  18 +++++
go.sum                              |  30 ++++++++
internal/agent/agent.go             |  92 ++++++++++++++--------------
internal/agent/agent_test.go        |   4 
internal/agent/agent_tool.go        |  30 ++++----
internal/agent/common_test.go       |  26 ++++----
internal/agent/coordinator.go       |  49 +++++++-------
internal/agent/tools/bash.go        |  20 +++---
internal/agent/tools/diagnostics.go |  12 +-
internal/agent/tools/download.go    |  36 +++++-----
internal/agent/tools/edit.go        | 100 +++++++++++++++---------------
internal/agent/tools/fetch.go       |  40 ++++++------
internal/agent/tools/glob.go        |  16 ++--
internal/agent/tools/grep.go        |  16 ++--
internal/agent/tools/ls.go          |  24 +++---
internal/agent/tools/mcp-tools.go   |  28 ++++----
internal/agent/tools/multiedit.go   |  70 ++++++++++----------
internal/agent/tools/sourcegraph.go |  28 ++++----
internal/agent/tools/view.go        |  38 +++++-----
internal/agent/tools/write.go       |  32 ++++----
internal/app/app.go                 |   4 
internal/message/content.go         |  46 +++++++-------
22 files changed, 401 insertions(+), 358 deletions(-)

Detailed changes

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

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=

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 <think>\n\n</think>", 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
 }
 

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)

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), &params); 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
 }

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),

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)

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<cwd>%s</cwd>", currentWorkingDir)
-			return ai.WithResponseMetadata(ai.NewTextResponse(stdout), metadata), nil
+			return fantasy.WithResponseMetadata(fantasy.NewTextResponse(stdout), metadata), nil
 		})
 }
 

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
 		})
 }
 

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
 		})
 }

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,

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 = "<html>\n<body>\n" + body + "\n</body>\n</html>"
 				}
@@ -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
 		})
 }
 

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,

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,

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
 		})

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)

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,

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
 		})
 }
 

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</file>\n"
 			output += getDiagnostics(filePath, lspClients)
 			recordFileRead(filePath)
-			return ai.WithResponseMetadata(
-				ai.NewTextResponse(output),
+			return fantasy.WithResponseMetadata(
+				fantasy.NewTextResponse(output),
 				ViewResponseMetadata{
 					FilePath: filePath,
 					Content:  content,

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("<result>\n%s\n</result>", result)
 			result += getDiagnostics(filePath, lspClients)
-			return ai.WithResponseMetadata(ai.NewTextResponse(result),
+			return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
 				WriteResponseMetadata{
 					Diff:      diff,
 					Additions: additions,

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)

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,
 		})
 	}