on main help sign in
chore: upgrade to latest fantasy
commitd2c67893708e5e5210c8f0bf4b76ce31113947d5 authorkujtimiihoxha <kujtimii.h@gmail.com> dateFri 17 Oct 2025, 17:16 (1 year ago) parent7956fa6
22 files changed+401−358
go.mod+17−1 go.sum+28−2 internal/agent/agent.go+46−46 internal/agent/agent_test.go+2−2 internal/agent/agent_tool.go+15−15 internal/agent/common_test.go+13−13 internal/agent/coordinator.go+25−24 internal/agent/tools/bash.go+10−10 internal/agent/tools/diagnostics.go+6−6 internal/agent/tools/download.go+18−18 internal/agent/tools/edit.go+50−50 internal/agent/tools/fetch.go+20−20 internal/agent/tools/glob.go+8−8 internal/agent/tools/grep.go+8−8 internal/agent/tools/ls.go+12−12 internal/agent/tools/mcp-tools.go+14−14 internal/agent/tools/multiedit.go+35−35 internal/agent/tools/sourcegraph.go+14−14 internal/agent/tools/view.go+19−19 internal/agent/tools/write.go+16−16 internal/app/app.go+2−2 internal/message/content.go+23−23
go.mod+17−1
@@ -3,6 +3,7 @@ module github.com/charmbracelet/crush
33go 1.25.0
44
55require (
6+ charm.land/fantasy v0.0.0
67 github.com/JohannesKaufmann/html-to-markdown v1.6.0
78 github.com/MakeNowJust/heredoc v1.0.0
89 github.com/PuerkitoBio/goquery v1.10.3
@@ -15,7 +16,6 @@ require (
1516 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619
1617 github.com/charmbracelet/catwalk v0.6.5-0.20251015124117-3bfe9eb6d20f
1718 github.com/charmbracelet/fang v0.4.3
18− github.com/charmbracelet/fantasy v0.0.0-20251015132833-5bc4cc524d70
1919 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
2020 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea
2121 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
@@ -47,6 +47,8 @@ require (
4747 mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
4848)
4949
50+replace charm.land/fantasy => ../../fantasy/main/
51+
5052require (
5153 cloud.google.com/go v0.116.0 // indirect
5254 cloud.google.com/go/auth v0.12.1 // indirect
@@ -56,6 +58,20 @@ require (
5658 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
5759 github.com/andybalholm/cascadia v1.3.3 // indirect
5860 github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
61+ github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
62+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
63+ github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
64+ github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
65+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
66+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
67+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
68+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
69+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
70+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
71+ github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
72+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
73+ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
74+ github.com/aws/smithy-go v1.20.3 // indirect
5975 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
6076 github.com/aymerick/douceur v0.2.0 // indirect
6177 github.com/bahlo/generic-list-go v0.2.0 // indirect
go.sum+28−2
@@ -36,6 +36,34 @@ github.com/anthropics/anthropic-sdk-go v1.12.0 h1:xPqlGnq7rWrTiHazIvCiumA0u7mGQn
3636github.com/anthropics/anthropic-sdk-go v1.12.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
3737github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
3838github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
39+github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
40+github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
41+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
42+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
43+github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
44+github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
45+github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
46+github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
47+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
48+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
49+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
50+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
51+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
52+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
53+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
54+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
55+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
56+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
57+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
58+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
59+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
60+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
61+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
62+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
63+github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
64+github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
65+github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
66+github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
3967github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4068github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
4169github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
@@ -61,8 +89,6 @@ github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqI
6189github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
6290github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY=
6391github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
64−github.com/charmbracelet/fantasy v0.0.0-20251015132833-5bc4cc524d70 h1:zVfEkMs1ncdUVQic9cGHHoZYMR0QNsmQ+gz8+LBo4cw=
65−github.com/charmbracelet/fantasy v0.0.0-20251015132833-5bc4cc524d70/go.mod h1:FugQt16qw07BjrmjHzu+l6BtlUnZgTcX/8qHyg4D/yQ=
6692github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
6793github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
6894github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
internal/agent/agent.go+46−46
@@ -10,6 +10,8 @@ import (
1010 "sync"
1111 "time"
1212
13+ "charm.land/fantasy"
14+ "charm.land/fantasy/providers/anthropic"
1315 "github.com/charmbracelet/catwalk/pkg/catwalk"
1416 "github.com/charmbracelet/crush/internal/agent/tools"
1517 "github.com/charmbracelet/crush/internal/config"
@@ -17,8 +19,6 @@ import (
1719 "github.com/charmbracelet/crush/internal/message"
1820 "github.com/charmbracelet/crush/internal/permission"
1921 "github.com/charmbracelet/crush/internal/session"
20− "github.com/charmbracelet/fantasy/ai"
21− "github.com/charmbracelet/fantasy/anthropic"
2222)
2323
2424//go:embed templates/title.md
@@ -30,7 +30,7 @@ var summaryPrompt []byte
3030type SessionAgentCall struct {
3131 SessionID string
3232 Prompt string
33− ProviderOptions ai.ProviderOptions
33+ ProviderOptions fantasy.ProviderOptions
3434 Attachments []message.Attachment
3535 MaxOutputTokens int64
3636 Temperature *float64
@@ -41,9 +41,9 @@ type SessionAgentCall struct {
4141}
4242
4343type SessionAgent interface {
44− Run(context.Context, SessionAgentCall) (*ai.AgentResult, error)
44+ Run(context.Context, SessionAgentCall) (*fantasy.AgentResult, error)
4545 SetModels(large Model, small Model)
46− SetTools(tools []ai.AgentTool)
46+ SetTools(tools []fantasy.AgentTool)
4747 Cancel(sessionID string)
4848 CancelAll()
4949 IsSessionBusy(sessionID string) bool
@@ -55,7 +55,7 @@ type SessionAgent interface {
5555}
5656
5757type Model struct {
58− Model ai.LanguageModel
58+ Model fantasy.LanguageModel
5959 CatwalkCfg catwalk.Model
6060 ModelCfg config.SelectedModel
6161}
@@ -64,7 +64,7 @@ type sessionAgent struct {
6464 largeModel Model
6565 smallModel Model
6666 systemPrompt string
67− tools []ai.AgentTool
67+ tools []fantasy.AgentTool
6868 sessions session.Service
6969 messages message.Service
7070 disableAutoSummarize bool
@@ -80,7 +80,7 @@ type SessionAgentOptions struct {
8080 DisableAutoSummarize bool
8181 Sessions session.Service
8282 Messages message.Service
83− Tools []ai.AgentTool
83+ Tools []fantasy.AgentTool
8484}
8585
8686func NewSessionAgent(
@@ -99,7 +99,7 @@ func NewSessionAgent(
9999 }
100100}
101101
102−func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.AgentResult, error) {
102+func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) {
103103 if call.Prompt == "" {
104104 return nil, ErrEmptyPrompt
105105 }
@@ -123,10 +123,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
123123 a.tools[len(a.tools)-1].SetProviderOptions(a.getCacheControlOptions())
124124 }
125125
126− agent := ai.NewAgent(
126+ agent := fantasy.NewAgent(
127127 a.largeModel.Model,
128− ai.WithSystemPrompt(a.systemPrompt),
129− ai.WithTools(a.tools...),
128+ fantasy.WithSystemPrompt(a.systemPrompt),
129+ fantasy.WithTools(a.tools...),
130130 )
131131
132132 sessionLock := sync.Mutex{}
@@ -169,7 +169,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
169169
170170 var currentAssistant *message.Message
171171 var shouldSummarize bool
172− result, err := agent.Stream(genCtx, ai.AgentStreamCall{
172+ result, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
173173 Prompt: call.Prompt,
174174 Files: files,
175175 Messages: history,
@@ -181,7 +181,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
181181 TopK: call.TopK,
182182 FrequencyPenalty: call.FrequencyPenalty,
183183 // Before each step create the new assistant message
184− PrepareStep: func(callContext context.Context, options ai.PrepareStepFunctionOptions) (_ context.Context, prepared ai.PrepareStepResult, err error) {
184+ PrepareStep: func(callContext context.Context, options fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
185185 var assistantMsg message.Message
186186 assistantMsg, err = a.messages.Create(callContext, call.SessionID, message.CreateMessageParams{
187187 Role: message.Assistant,
@@ -217,7 +217,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
217217 systemMessageUpdated := false
218218 for i, msg := range prepared.Messages {
219219 // only add cache control to the last message
220− if msg.Role == ai.MessageRoleSystem {
220+ if msg.Role == fantasy.MessageRoleSystem {
221221 lastSystemRoleInx = i
222222 } else if !systemMessageUpdated {
223223 prepared.Messages[lastSystemRoleInx].ProviderOptions = a.getCacheControlOptions()
@@ -234,7 +234,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
234234 currentAssistant.AppendReasoningContent(text)
235235 return a.messages.Update(genCtx, *currentAssistant)
236236 },
237− OnReasoningEnd: func(id string, reasoning ai.ReasoningContent) error {
237+ OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
238238 // handle anthropic signature
239239 if anthropicData, ok := reasoning.ProviderMetadata[anthropic.Name]; ok {
240240 if reasoning, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok {
@@ -258,10 +258,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
258258 currentAssistant.AddToolCall(toolCall)
259259 return a.messages.Update(genCtx, *currentAssistant)
260260 },
261− OnRetry: func(err *ai.APICallError, delay time.Duration) {
261+ OnRetry: func(err *fantasy.APICallError, delay time.Duration) {
262262 // TODO: implement
263263 },
264− OnToolCall: func(tc ai.ToolCallContent) error {
264+ OnToolCall: func(tc fantasy.ToolCallContent) error {
265265 toolCall := message.ToolCall{
266266 ID: tc.ToolCallID,
267267 Name: tc.ToolName,
@@ -272,22 +272,22 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
272272 currentAssistant.AddToolCall(toolCall)
273273 return a.messages.Update(genCtx, *currentAssistant)
274274 },
275− OnToolResult: func(result ai.ToolResultContent) error {
275+ OnToolResult: func(result fantasy.ToolResultContent) error {
276276 var resultContent string
277277 isError := false
278278 switch result.Result.GetType() {
279− case ai.ToolResultContentTypeText:
280− r, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentText](result.Result)
279+ case fantasy.ToolResultContentTypeText:
280+ r, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Result)
281281 if ok {
282282 resultContent = r.Text
283283 }
284− case ai.ToolResultContentTypeError:
285− r, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentError](result.Result)
284+ case fantasy.ToolResultContentTypeError:
285+ r, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Result)
286286 if ok {
287287 isError = true
288288 resultContent = r.Error.Error()
289289 }
290− case ai.ToolResultContentTypeMedia:
290+ case fantasy.ToolResultContentTypeMedia:
291291 // TODO: handle this message type
292292 }
293293 toolResult := message.ToolResult{
@@ -308,14 +308,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
308308 }
309309 return nil
310310 },
311− OnStepFinish: func(stepResult ai.StepResult) error {
311+ OnStepFinish: func(stepResult fantasy.StepResult) error {
312312 finishReason := message.FinishReasonUnknown
313313 switch stepResult.FinishReason {
314− case ai.FinishReasonLength:
314+ case fantasy.FinishReasonLength:
315315 finishReason = message.FinishReasonMaxTokens
316− case ai.FinishReasonStop:
316+ case fantasy.FinishReasonStop:
317317 finishReason = message.FinishReasonEndTurn
318− case ai.FinishReasonToolCalls:
318+ case fantasy.FinishReasonToolCalls:
319319 finishReason = message.FinishReasonToolUse
320320 }
321321 currentAssistant.AddFinish(finishReason, "", "")
@@ -328,8 +328,8 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
328328 }
329329 return a.messages.Update(genCtx, *currentAssistant)
330330 },
331− StopWhen: []ai.StopCondition{
332− func(_ []ai.StepResult) bool {
331+ StopWhen: []fantasy.StopCondition{
332+ func(_ []fantasy.StepResult) bool {
333333 contextWindow := a.largeModel.CatwalkCfg.ContextWindow
334334 tokens := currentSession.CompletionTokens + currentSession.PromptTokens
335335 percentage := (float64(tokens) / float64(contextWindow)) * 100
@@ -461,8 +461,8 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
461461 defer a.activeRequests.Del(sessionID)
462462 defer cancel()
463463
464− agent := ai.NewAgent(a.largeModel.Model,
465− ai.WithSystemPrompt(string(summaryPrompt)),
464+ agent := fantasy.NewAgent(a.largeModel.Model,
465+ fantasy.WithSystemPrompt(string(summaryPrompt)),
466466 )
467467 summaryMessage, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
468468 Role: message.Assistant,
@@ -474,14 +474,14 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
474474 return err
475475 }
476476
477− resp, err := agent.Stream(genCtx, ai.AgentStreamCall{
477+ resp, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
478478 Prompt: "Provide a detailed summary of our conversation above.",
479479 Messages: aiMsgs,
480480 OnReasoningDelta: func(id string, text string) error {
481481 summaryMessage.AppendReasoningContent(text)
482482 return a.messages.Update(genCtx, summaryMessage)
483483 },
484− OnReasoningEnd: func(id string, reasoning ai.ReasoningContent) error {
484+ OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
485485 // handle anthropic signature
486486 if anthropicData, ok := reasoning.ProviderMetadata["anthropic"]; ok {
487487 if signature, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok && signature.Signature != "" {
@@ -523,8 +523,8 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
523523 return err
524524}
525525
526−func (a *sessionAgent) getCacheControlOptions() ai.ProviderOptions {
527− return ai.ProviderOptions{
526+func (a *sessionAgent) getCacheControlOptions() fantasy.ProviderOptions {
527+ return fantasy.ProviderOptions{
528528 anthropic.Name: &anthropic.ProviderCacheControlOptions{
529529 CacheControl: anthropic.CacheControl{Type: "ephemeral"},
530530 },
@@ -548,8 +548,8 @@ func (a *sessionAgent) createUserMessage(ctx context.Context, call SessionAgentC
548548 return msg, nil
549549}
550550
551−func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...message.Attachment) ([]ai.Message, []ai.FilePart) {
552− var history []ai.Message
551+func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...message.Attachment) ([]fantasy.Message, []fantasy.FilePart) {
552+ var history []fantasy.Message
553553 for _, m := range msgs {
554554 if len(m.Parts) == 0 {
555555 continue
@@ -561,9 +561,9 @@ func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...mess
561561 history = append(history, m.ToAIMessage()...)
562562 }
563563
564− var files []ai.FilePart
564+ var files []fantasy.FilePart
565565 for _, attachment := range attachments {
566− files = append(files, ai.FilePart{
566+ files = append(files, fantasy.FilePart{
567567 Filename: attachment.FileName,
568568 Data: attachment.Content,
569569 MediaType: attachment.MimeType,
@@ -605,12 +605,12 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
605605 maxOutput = a.smallModel.CatwalkCfg.DefaultMaxTokens
606606 }
607607
608− agent := ai.NewAgent(a.smallModel.Model,
609− ai.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
610− ai.WithMaxOutputTokens(maxOutput),
608+ agent := fantasy.NewAgent(a.smallModel.Model,
609+ fantasy.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
610+ fantasy.WithMaxOutputTokens(maxOutput),
611611 )
612612
613− resp, err := agent.Stream(ctx, ai.AgentStreamCall{
613+ resp, err := agent.Stream(ctx, fantasy.AgentStreamCall{
614614 Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", prompt),
615615 })
616616 if err != nil {
@@ -642,7 +642,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
642642 }
643643}
644644
645−func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session, usage ai.Usage) {
645+func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session, usage fantasy.Usage) {
646646 modelConfig := model.CatwalkCfg
647647 cost := modelConfig.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
648648 modelConfig.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
@@ -727,7 +727,7 @@ func (a *sessionAgent) SetModels(large Model, small Model) {
727727 a.smallModel = small
728728}
729729
730−func (a *sessionAgent) SetTools(tools []ai.AgentTool) {
730+func (a *sessionAgent) SetTools(tools []fantasy.AgentTool) {
731731 a.tools = tools
732732}
733733
internal/agent/agent_test.go+2−2
@@ -6,10 +6,10 @@ import (
66 "strings"
77 "testing"
88
9+ "charm.land/fantasy"
910 "github.com/charmbracelet/crush/internal/agent/tools"
1011 "github.com/charmbracelet/crush/internal/message"
1112 "github.com/charmbracelet/crush/internal/shell"
12− "github.com/charmbracelet/fantasy/ai"
1313 "github.com/stretchr/testify/assert"
1414 "github.com/stretchr/testify/require"
1515 "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
@@ -24,7 +24,7 @@ var modelPairs = []modelPair{
2424 {"zai-glm4.6", zAIBuilder("glm-4.6"), zAIBuilder("glm-4.5-air")},
2525}
2626
27−func getModels(t *testing.T, r *recorder.Recorder, pair modelPair) (ai.LanguageModel, ai.LanguageModel) {
27+func getModels(t *testing.T, r *recorder.Recorder, pair modelPair) (fantasy.LanguageModel, fantasy.LanguageModel) {
2828 large, err := pair.largeModel(t, r)
2929 require.NoError(t, err)
3030 small, err := pair.smallModel(t, r)
internal/agent/agent_tool.go+15−15
@@ -7,7 +7,7 @@ import (
77 "errors"
88 "fmt"
99
10− "github.com/charmbracelet/fantasy/ai"
10+ "charm.land/fantasy"
1111
1212 "github.com/charmbracelet/crush/internal/agent/prompt"
1313 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -25,7 +25,7 @@ const (
2525 AgentToolName = "agent"
2626)
2727
28−func (c *coordinator) agentTool() (ai.AgentTool, error) {
28+func (c *coordinator) agentTool() (fantasy.AgentTool, error) {
2929 agentCfg, ok := c.cfg.Agents[config.AgentTask]
3030 if !ok {
3131 return nil, errors.New("task agent not configured")
@@ -39,31 +39,31 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
3939 if err != nil {
4040 return nil, err
4141 }
42− return ai.NewAgentTool(
42+ return fantasy.NewAgentTool(
4343 AgentToolName,
4444 string(agentToolDescription),
45− func(ctx context.Context, params AgentParams, call ai.ToolCall) (ai.ToolResponse, error) {
45+ func(ctx context.Context, params AgentParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
4646 if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
47− return ai.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
47+ return fantasy.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
4848 }
4949 if params.Prompt == "" {
50− return ai.NewTextErrorResponse("prompt is required"), nil
50+ return fantasy.NewTextErrorResponse("prompt is required"), nil
5151 }
5252
5353 sessionID := tools.GetSessionFromContext(ctx)
5454 if sessionID == "" {
55− return ai.ToolResponse{}, errors.New("session id missing from context")
55+ return fantasy.ToolResponse{}, errors.New("session id missing from context")
5656 }
5757
5858 agentMessageID := tools.GetMessageFromContext(ctx)
5959 if agentMessageID == "" {
60− return ai.ToolResponse{}, errors.New("agent message id missing from context")
60+ return fantasy.ToolResponse{}, errors.New("agent message id missing from context")
6161 }
6262
6363 agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, call.ID)
6464 session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, "New Agent Session")
6565 if err != nil {
66− return ai.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
66+ return fantasy.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
6767 }
6868 model := agent.Model()
6969 maxTokens := model.CatwalkCfg.DefaultMaxTokens
@@ -73,7 +73,7 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
7373
7474 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
7575 if !ok {
76− return ai.ToolResponse{}, errors.New("model provider not configured")
76+ return fantasy.ToolResponse{}, errors.New("model provider not configured")
7777 }
7878 result, err := agent.Run(ctx, SessionAgentCall{
7979 SessionID: session.ID,
@@ -87,23 +87,23 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
8787 PresencePenalty: model.ModelCfg.PresencePenalty,
8888 })
8989 if err != nil {
90− return ai.NewTextErrorResponse("error generating response"), nil
90+ return fantasy.NewTextErrorResponse("error generating response"), nil
9191 }
9292 updatedSession, err := c.sessions.Get(ctx, session.ID)
9393 if err != nil {
94− return ai.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
94+ return fantasy.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
9595 }
9696 parentSession, err := c.sessions.Get(ctx, sessionID)
9797 if err != nil {
98− return ai.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
98+ return fantasy.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
9999 }
100100
101101 parentSession.Cost += updatedSession.Cost
102102
103103 _, err = c.sessions.Save(ctx, parentSession)
104104 if err != nil {
105− return ai.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
105+ return fantasy.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
106106 }
107− return ai.NewTextResponse(result.Response.Content.Text()), nil
107+ return fantasy.NewTextResponse(result.Response.Content.Text()), nil
108108 }), nil
109109}
internal/agent/common_test.go+13−13
@@ -7,6 +7,11 @@ import (
77 "testing"
88 "time"
99
10+ "charm.land/fantasy"
11+ "charm.land/fantasy/providers/anthropic"
12+ "charm.land/fantasy/providers/openai"
13+ "charm.land/fantasy/providers/openaicompat"
14+ "charm.land/fantasy/providers/openrouter"
1015 "github.com/charmbracelet/catwalk/pkg/catwalk"
1116 "github.com/charmbracelet/crush/internal/agent/prompt"
1217 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -18,11 +23,6 @@ import (
1823 "github.com/charmbracelet/crush/internal/message"
1924 "github.com/charmbracelet/crush/internal/permission"
2025 "github.com/charmbracelet/crush/internal/session"
21− "github.com/charmbracelet/fantasy/ai"
22− "github.com/charmbracelet/fantasy/anthropic"
23− "github.com/charmbracelet/fantasy/openai"
24− "github.com/charmbracelet/fantasy/openaicompat"
25− "github.com/charmbracelet/fantasy/openrouter"
2626 "github.com/stretchr/testify/require"
2727 "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
2828
@@ -38,7 +38,7 @@ type env struct {
3838 lspClients *csync.Map[string, *lsp.Client]
3939}
4040
41−type builderFunc func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error)
41+type builderFunc func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error)
4242
4343type modelPair struct {
4444 name string
@@ -47,7 +47,7 @@ type modelPair struct {
4747}
4848
4949func anthropicBuilder(model string) builderFunc {
50− return func(_ *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
50+ return func(_ *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
5151 provider := anthropic.New(
5252 anthropic.WithAPIKey(os.Getenv("CRUSH_ANTHROPIC_API_KEY")),
5353 anthropic.WithHTTPClient(&http.Client{Transport: r}),
@@ -57,7 +57,7 @@ func anthropicBuilder(model string) builderFunc {
5757}
5858
5959func openaiBuilder(model string) builderFunc {
60− return func(_ *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
60+ return func(_ *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
6161 provider := openai.New(
6262 openai.WithAPIKey(os.Getenv("CRUSH_OPENAI_API_KEY")),
6363 openai.WithHTTPClient(&http.Client{Transport: r}),
@@ -67,7 +67,7 @@ func openaiBuilder(model string) builderFunc {
6767}
6868
6969func openRouterBuilder(model string) builderFunc {
70− return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
70+ return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
7171 provider := openrouter.New(
7272 openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")),
7373 openrouter.WithHTTPClient(&http.Client{Transport: r}),
@@ -77,7 +77,7 @@ func openRouterBuilder(model string) builderFunc {
7777}
7878
7979func zAIBuilder(model string) builderFunc {
80− return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
80+ return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
8181 provider := openaicompat.New(
8282 openaicompat.WithBaseURL("https://api.z.ai/api/coding/paas/v4"),
8383 openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")),
@@ -114,7 +114,7 @@ func testEnv(t *testing.T) env {
114114 }
115115}
116116
117−func testSessionAgent(env env, large, small ai.LanguageModel, systemPrompt string, tools ...ai.AgentTool) SessionAgent {
117+func testSessionAgent(env env, large, small fantasy.LanguageModel, systemPrompt string, tools ...fantasy.AgentTool) SessionAgent {
118118 largeModel := Model{
119119 Model: large,
120120 CatwalkCfg: catwalk.Model{
@@ -133,7 +133,7 @@ func testSessionAgent(env env, large, small ai.LanguageModel, systemPrompt strin
133133 return agent
134134}
135135
136−func coderAgent(r *recorder.Recorder, env env, large, small ai.LanguageModel) (SessionAgent, error) {
136+func coderAgent(r *recorder.Recorder, env env, large, small fantasy.LanguageModel) (SessionAgent, error) {
137137 fixedTime := func() time.Time {
138138 t, _ := time.Parse("1/2/2006", "1/1/2025")
139139 return t
@@ -155,7 +155,7 @@ func coderAgent(r *recorder.Recorder, env env, large, small ai.LanguageModel) (S
155155 if err != nil {
156156 return nil, err
157157 }
158− allTools := []ai.AgentTool{
158+ allTools := []fantasy.AgentTool{
159159 tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution),
160160 tools.NewDownloadTool(env.permissions, env.workingDir, r.GetDefaultClient()),
161161 tools.NewEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
internal/agent/coordinator.go+25−24
@@ -11,6 +11,7 @@ import (
1111 "slices"
1212 "strings"
1313
14+ "charm.land/fantasy"
1415 "github.com/charmbracelet/catwalk/pkg/catwalk"
1516 "github.com/charmbracelet/crush/internal/agent/prompt"
1617 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -22,20 +23,20 @@ import (
2223 "github.com/charmbracelet/crush/internal/message"
2324 "github.com/charmbracelet/crush/internal/permission"
2425 "github.com/charmbracelet/crush/internal/session"
25− "github.com/charmbracelet/fantasy/ai"
26− "github.com/charmbracelet/fantasy/anthropic"
27− "github.com/charmbracelet/fantasy/azure"
28− "github.com/charmbracelet/fantasy/google"
29− "github.com/charmbracelet/fantasy/openai"
30− "github.com/charmbracelet/fantasy/openaicompat"
31− "github.com/charmbracelet/fantasy/openrouter"
26+
27+ "charm.land/fantasy/providers/anthropic"
28+ "charm.land/fantasy/providers/azure"
29+ "charm.land/fantasy/providers/google"
30+ "charm.land/fantasy/providers/openai"
31+ "charm.land/fantasy/providers/openaicompat"
32+ "charm.land/fantasy/providers/openrouter"
3233 "github.com/qjebbs/go-jsons"
3334)
3435
3536type Coordinator interface {
3637 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
3738 // SetMainAgent(string)
38− Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*ai.AgentResult, error)
39+ Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
3940 Cancel(sessionID string)
4041 CancelAll()
4142 IsSessionBusy(sessionID string) bool
@@ -98,7 +99,7 @@ func NewCoordinator(
9899}
99100
100101// Run implements Coordinator.
101−func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*ai.AgentResult, error) {
102+func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error) {
102103 model := c.currentAgent.Model()
103104 maxTokens := model.CatwalkCfg.DefaultMaxTokens
104105 if model.ModelCfg.MaxTokens != 0 {
@@ -130,8 +131,8 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
130131 })
131132}
132133
133−func getProviderOptions(model Model, tp catwalk.Type) ai.ProviderOptions {
134− options := ai.ProviderOptions{}
134+func getProviderOptions(model Model, tp catwalk.Type) fantasy.ProviderOptions {
135+ options := fantasy.ProviderOptions{}
135136
136137 cfgOpts := []byte("{}")
137138 catwalkOpts := []byte("{}")
@@ -233,7 +234,7 @@ func getProviderOptions(model Model, tp catwalk.Type) ai.ProviderOptions {
233234 return options
234235}
235236
236−func mergeCallOptions(model Model, tp catwalk.Type) (ai.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
237+func mergeCallOptions(model Model, tp catwalk.Type) (fantasy.ProviderOptions, *float64, *float64, *int64, *float64, *float64) {
237238 modelOptions := getProviderOptions(model, tp)
238239 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
239240 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
261262 return NewSessionAgent(SessionAgentOptions{large, small, systemPrompt, c.cfg.Options.DisableAutoSummarize, c.sessions, c.messages, tools}), nil
262263}
263264
264−func (c *coordinator) buildTools(agent config.Agent) ([]ai.AgentTool, error) {
265− var allTools []ai.AgentTool
265+func (c *coordinator) buildTools(agent config.Agent) ([]fantasy.AgentTool, error) {
266+ var allTools []fantasy.AgentTool
266267 if slices.Contains(agent.AllowedTools, AgentToolName) {
267268 agentTool, err := c.agentTool()
268269 if err != nil {
@@ -285,7 +286,7 @@ func (c *coordinator) buildTools(agent config.Agent) ([]ai.AgentTool, error) {
285286 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
286287 )
287288
288− var filteredTools []ai.AgentTool
289+ var filteredTools []fantasy.AgentTool
289290 for _, tool := range allTools {
290291 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
291292 filteredTools = append(filteredTools, tool)
@@ -394,7 +395,7 @@ func (c *coordinator) buildAgentModels() (Model, Model, error) {
394395 }, nil
395396}
396397
397−func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
398+func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
398399 hasBearerAuth := false
399400 for key := range headers {
400401 if strings.ToLower(key) == "authorization" {
@@ -429,7 +430,7 @@ func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map
429430 return anthropic.New(opts...)
430431}
431432
432−func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
433+func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
433434 opts := []openai.Option{
434435 openai.WithAPIKey(apiKey),
435436 }
@@ -446,7 +447,7 @@ func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[st
446447 return openai.New(opts...)
447448}
448449
449−func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) ai.Provider {
450+func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) fantasy.Provider {
450451 opts := []openrouter.Option{
451452 openrouter.WithAPIKey(apiKey),
452453 }
@@ -460,7 +461,7 @@ func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[stri
460461 return openrouter.New(opts...)
461462}
462463
463−func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
464+func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
464465 opts := []openaicompat.Option{
465466 openaicompat.WithBaseURL(baseURL),
466467 openaicompat.WithAPIKey(apiKey),
@@ -476,7 +477,7 @@ func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers
476477 return openaicompat.New(opts...)
477478}
478479
479−func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) ai.Provider {
480+func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[string]string, options map[string]string) fantasy.Provider {
480481 opts := []azure.Option{
481482 azure.WithBaseURL(baseURL),
482483 azure.WithAPIKey(apiKey),
@@ -498,7 +499,7 @@ func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[str
498499 return azure.New(opts...)
499500}
500501
501−func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
502+func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[string]string) fantasy.Provider {
502503 opts := []google.Option{
503504 google.WithBaseURL(baseURL),
504505 google.WithGeminiAPIKey(apiKey),
@@ -513,7 +514,7 @@ func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[st
513514 return google.New(opts...)
514515}
515516
516−func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) ai.Provider {
517+func (c *coordinator) buildGoogleVertexProvider(headers map[string]string, options map[string]string) fantasy.Provider {
517518 opts := []google.Option{}
518519 if c.cfg.Options.Debug {
519520 httpClient := log.NewHTTPClient()
@@ -550,7 +551,7 @@ func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
550551 return false
551552}
552553
553−func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (ai.Provider, error) {
554+func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model config.SelectedModel) (fantasy.Provider, error) {
554555 headers := providerCfg.ExtraHeaders
555556
556557 // handle special headers for anthropic
@@ -561,7 +562,7 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
561562 // TODO: make sure we have
562563 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
563564 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
564− var provider ai.Provider
565+ var provider fantasy.Provider
565566 switch providerCfg.Type {
566567 case openai.Name:
567568 provider = c.buildOpenaiProvider(baseURL, apiKey, headers)
internal/agent/tools/bash.go+10−10
@@ -9,10 +9,10 @@ import (
99 "strings"
1010 "time"
1111
12+ "charm.land/fantasy"
1213 "github.com/charmbracelet/crush/internal/config"
1314 "github.com/charmbracelet/crush/internal/permission"
1415 "github.com/charmbracelet/crush/internal/shell"
15− "github.com/charmbracelet/fantasy/ai"
1616)
1717
1818type BashParams struct {
@@ -177,14 +177,14 @@ func blockFuncs() []shell.BlockFunc {
177177 }
178178}
179179
180−func NewBashTool(permissions permission.Service, workingDir string, attribution *config.Attribution) ai.AgentTool {
180+func NewBashTool(permissions permission.Service, workingDir string, attribution *config.Attribution) fantasy.AgentTool {
181181 // Set up command blocking on the persistent shell
182182 persistentShell := shell.GetPersistentShell(workingDir)
183183 persistentShell.SetBlockFuncs(blockFuncs())
184− return ai.NewAgentTool(
184+ return fantasy.NewAgentTool(
185185 BashToolName,
186186 string(bashDescription(attribution)),
187− func(ctx context.Context, params BashParams, call ai.ToolCall) (ai.ToolResponse, error) {
187+ func(ctx context.Context, params BashParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
188188 if params.Timeout > MaxTimeout {
189189 params.Timeout = MaxTimeout
190190 } else if params.Timeout <= 0 {
@@ -192,7 +192,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
192192 }
193193
194194 if params.Command == "" {
195− return ai.NewTextErrorResponse("missing command"), nil
195+ return fantasy.NewTextErrorResponse("missing command"), nil
196196 }
197197
198198 isSafeReadOnly := false
@@ -209,7 +209,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
209209
210210 sessionID := GetSessionFromContext(ctx)
211211 if sessionID == "" {
212− return ai.ToolResponse{}, fmt.Errorf("session ID is required for executing shell command")
212+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for executing shell command")
213213 }
214214 if !isSafeReadOnly {
215215 shell := shell.GetPersistentShell(workingDir)
@@ -228,7 +228,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
228228 },
229229 )
230230 if !p {
231− return ai.ToolResponse{}, permission.ErrorPermissionDenied
231+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
232232 }
233233 }
234234 startTime := time.Now()
@@ -246,7 +246,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
246246 interrupted := shell.IsInterrupt(err)
247247 exitCode := shell.ExitCode(err)
248248 if exitCode == 0 && !interrupted && err != nil {
249− return ai.ToolResponse{}, fmt.Errorf("error executing command: %w", err)
249+ return fantasy.ToolResponse{}, fmt.Errorf("error executing command: %w", err)
250250 }
251251
252252 stdout = truncateOutput(stdout)
@@ -287,10 +287,10 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
287287 WorkingDirectory: currentWorkingDir,
288288 }
289289 if stdout == "" {
290− return ai.WithResponseMetadata(ai.NewTextResponse(BashNoOutput), metadata), nil
290+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(BashNoOutput), metadata), nil
291291 }
292292 stdout += fmt.Sprintf("\n\n<cwd>%s</cwd>", currentWorkingDir)
293− return ai.WithResponseMetadata(ai.NewTextResponse(stdout), metadata), nil
293+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(stdout), metadata), nil
294294 })
295295}
296296
internal/agent/tools/diagnostics.go+6−6
@@ -9,9 +9,9 @@ import (
99 "strings"
1010 "time"
1111
12+ "charm.land/fantasy"
1213 "github.com/charmbracelet/crush/internal/csync"
1314 "github.com/charmbracelet/crush/internal/lsp"
14− "github.com/charmbracelet/fantasy/ai"
1515 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
1616)
1717
@@ -24,17 +24,17 @@ const DiagnosticsToolName = "diagnostics"
2424//go:embed diagnostics.md
2525var diagnosticsDescription []byte
2626
27−func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) ai.AgentTool {
28− return ai.NewAgentTool(
27+func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) fantasy.AgentTool {
28+ return fantasy.NewAgentTool(
2929 DiagnosticsToolName,
3030 string(diagnosticsDescription),
31− func(ctx context.Context, params DiagnosticsParams, call ai.ToolCall) (ai.ToolResponse, error) {
31+ func(ctx context.Context, params DiagnosticsParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
3232 if lspClients.Len() == 0 {
33− return ai.NewTextErrorResponse("no LSP clients available"), nil
33+ return fantasy.NewTextErrorResponse("no LSP clients available"), nil
3434 }
3535 notifyLSPs(ctx, lspClients, params.FilePath)
3636 output := getDiagnostics(params.FilePath, lspClients)
37− return ai.NewTextResponse(output), nil
37+ return fantasy.NewTextResponse(output), nil
3838 })
3939}
4040
internal/agent/tools/download.go+18−18
@@ -11,8 +11,8 @@ import (
1111 "strings"
1212 "time"
1313
14+ "charm.land/fantasy"
1415 "github.com/charmbracelet/crush/internal/permission"
15− "github.com/charmbracelet/fantasy/ai"
1616)
1717
1818type DownloadParams struct {
@@ -32,7 +32,7 @@ const DownloadToolName = "download"
3232//go:embed download.md
3333var downloadDescription []byte
3434
35−func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) ai.AgentTool {
35+func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
3636 if client == nil {
3737 client = &http.Client{
3838 Timeout: 5 * time.Minute, // Default 5 minute timeout for downloads
@@ -43,20 +43,20 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
4343 },
4444 }
4545 }
46− return ai.NewAgentTool(
46+ return fantasy.NewAgentTool(
4747 DownloadToolName,
4848 string(downloadDescription),
49− func(ctx context.Context, params DownloadParams, call ai.ToolCall) (ai.ToolResponse, error) {
49+ func(ctx context.Context, params DownloadParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
5050 if params.URL == "" {
51− return ai.NewTextErrorResponse("URL parameter is required"), nil
51+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
5252 }
5353
5454 if params.FilePath == "" {
55− return ai.NewTextErrorResponse("file_path parameter is required"), nil
55+ return fantasy.NewTextErrorResponse("file_path parameter is required"), nil
5656 }
5757
5858 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
59− return ai.NewTextErrorResponse("URL must start with http:// or https://"), nil
59+ return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
6060 }
6161
6262 // Convert relative path to absolute path
@@ -69,7 +69,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
6969
7070 sessionID := GetSessionFromContext(ctx)
7171 if sessionID == "" {
72− return ai.ToolResponse{}, fmt.Errorf("session ID is required for downloading files")
72+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for downloading files")
7373 }
7474
7575 p := permissions.Request(
@@ -84,7 +84,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
8484 )
8585
8686 if !p {
87− return ai.ToolResponse{}, permission.ErrorPermissionDenied
87+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
8888 }
8989
9090 // Handle timeout with context
@@ -101,36 +101,36 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
101101
102102 req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
103103 if err != nil {
104− return ai.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
104+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
105105 }
106106
107107 req.Header.Set("User-Agent", "crush/1.0")
108108
109109 resp, err := client.Do(req)
110110 if err != nil {
111− return ai.ToolResponse{}, fmt.Errorf("failed to download from URL: %w", err)
111+ return fantasy.ToolResponse{}, fmt.Errorf("failed to download from URL: %w", err)
112112 }
113113 defer resp.Body.Close()
114114
115115 if resp.StatusCode != http.StatusOK {
116− return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
116+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
117117 }
118118
119119 // Check content length if available
120120 maxSize := int64(100 * 1024 * 1024) // 100MB
121121 if resp.ContentLength > maxSize {
122− return ai.NewTextErrorResponse(fmt.Sprintf("File too large: %d bytes (max %d bytes)", resp.ContentLength, maxSize)), nil
122+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File too large: %d bytes (max %d bytes)", resp.ContentLength, maxSize)), nil
123123 }
124124
125125 // Create parent directories if they don't exist
126126 if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
127− return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
127+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
128128 }
129129
130130 // Create the output file
131131 outFile, err := os.Create(filePath)
132132 if err != nil {
133− return ai.ToolResponse{}, fmt.Errorf("failed to create output file: %w", err)
133+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create output file: %w", err)
134134 }
135135 defer outFile.Close()
136136
@@ -138,14 +138,14 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
138138 limitedReader := io.LimitReader(resp.Body, maxSize)
139139 bytesWritten, err := io.Copy(outFile, limitedReader)
140140 if err != nil {
141− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
141+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
142142 }
143143
144144 // Check if we hit the size limit
145145 if bytesWritten == maxSize {
146146 // Clean up the file since it might be incomplete
147147 os.Remove(filePath)
148− return ai.NewTextErrorResponse(fmt.Sprintf("File too large: exceeded %d bytes limit", maxSize)), nil
148+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File too large: exceeded %d bytes limit", maxSize)), nil
149149 }
150150
151151 contentType := resp.Header.Get("Content-Type")
@@ -154,6 +154,6 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
154154 responseMsg += fmt.Sprintf(" (Content-Type: %s)", contentType)
155155 }
156156
157− return ai.NewTextResponse(responseMsg), nil
157+ return fantasy.NewTextResponse(responseMsg), nil
158158 })
159159}
internal/agent/tools/edit.go+50−50
@@ -10,11 +10,11 @@ import (
1010 "strings"
1111 "time"
1212
13+ "charm.land/fantasy"
1314 "github.com/charmbracelet/crush/internal/csync"
1415 "github.com/charmbracelet/crush/internal/diff"
1516 "github.com/charmbracelet/crush/internal/fsext"
1617 "github.com/charmbracelet/crush/internal/history"
17− "github.com/charmbracelet/fantasy/ai"
1818
1919 "github.com/charmbracelet/crush/internal/lsp"
2020 "github.com/charmbracelet/crush/internal/permission"
@@ -52,20 +52,20 @@ type editContext struct {
5252 workingDir string
5353}
5454
55−func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
56− return ai.NewAgentTool(
55+func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
56+ return fantasy.NewAgentTool(
5757 EditToolName,
5858 string(editDescription),
59− func(ctx context.Context, params EditParams, call ai.ToolCall) (ai.ToolResponse, error) {
59+ func(ctx context.Context, params EditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
6060 if params.FilePath == "" {
61− return ai.NewTextErrorResponse("file_path is required"), nil
61+ return fantasy.NewTextErrorResponse("file_path is required"), nil
6262 }
6363
6464 if !filepath.IsAbs(params.FilePath) {
6565 params.FilePath = filepath.Join(workingDir, params.FilePath)
6666 }
6767
68− var response ai.ToolResponse
68+ var response fantasy.ToolResponse
6969 var err error
7070
7171 editCtx := editContext{ctx, permissions, files, workingDir}
@@ -103,25 +103,25 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
103103 })
104104}
105105
106−func createNewFile(edit editContext, filePath, content string, call ai.ToolCall) (ai.ToolResponse, error) {
106+func createNewFile(edit editContext, filePath, content string, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
107107 fileInfo, err := os.Stat(filePath)
108108 if err == nil {
109109 if fileInfo.IsDir() {
110− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
110+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
111111 }
112− return ai.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
112+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
113113 } else if !os.IsNotExist(err) {
114− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
114+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
115115 }
116116
117117 dir := filepath.Dir(filePath)
118118 if err = os.MkdirAll(dir, 0o755); err != nil {
119− return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
119+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
120120 }
121121
122122 sessionID := GetSessionFromContext(edit.ctx)
123123 if sessionID == "" {
124− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
124+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
125125 }
126126
127127 _, additions, removals := diff.GenerateDiff(
@@ -145,19 +145,19 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
145145 },
146146 )
147147 if !p {
148− return ai.ToolResponse{}, permission.ErrorPermissionDenied
148+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
149149 }
150150
151151 err = os.WriteFile(filePath, []byte(content), 0o644)
152152 if err != nil {
153− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
153+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
154154 }
155155
156156 // File can't be in the history so we create a new file history
157157 _, err = edit.files.Create(edit.ctx, sessionID, filePath, "")
158158 if err != nil {
159159 // Log error but don't fail the operation
160− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
160+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
161161 }
162162
163163 // Add the new content to the file history
@@ -170,8 +170,8 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
170170 recordFileWrite(filePath)
171171 recordFileRead(filePath)
172172
173− return ai.WithResponseMetadata(
174− ai.NewTextResponse("File created: "+filePath),
173+ return fantasy.WithResponseMetadata(
174+ fantasy.NewTextResponse("File created: "+filePath),
175175 EditResponseMetadata{
176176 OldContent: "",
177177 NewContent: content,
@@ -181,27 +181,27 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
181181 ), nil
182182}
183183
184−func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
184+func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
185185 fileInfo, err := os.Stat(filePath)
186186 if err != nil {
187187 if os.IsNotExist(err) {
188− return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
188+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
189189 }
190− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
190+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
191191 }
192192
193193 if fileInfo.IsDir() {
194− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
194+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
195195 }
196196
197197 if getLastReadTime(filePath).IsZero() {
198− return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
198+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
199199 }
200200
201201 modTime := fileInfo.ModTime()
202202 lastRead := getLastReadTime(filePath)
203203 if modTime.After(lastRead) {
204− return ai.NewTextErrorResponse(
204+ return fantasy.NewTextErrorResponse(
205205 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
206206 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
207207 )), nil
@@ -209,7 +209,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
209209
210210 content, err := os.ReadFile(filePath)
211211 if err != nil {
212− return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
212+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
213213 }
214214
215215 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -221,17 +221,17 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
221221 newContent = strings.ReplaceAll(oldContent, oldString, "")
222222 deletionCount = strings.Count(oldContent, oldString)
223223 if deletionCount == 0 {
224− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
224+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
225225 }
226226 } else {
227227 index := strings.Index(oldContent, oldString)
228228 if index == -1 {
229− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
229+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
230230 }
231231
232232 lastIndex := strings.LastIndex(oldContent, oldString)
233233 if index != lastIndex {
234− 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
234+ 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
235235 }
236236
237237 newContent = oldContent[:index] + oldContent[index+len(oldString):]
@@ -241,7 +241,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
241241 sessionID := GetSessionFromContext(edit.ctx)
242242
243243 if sessionID == "" {
244− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
244+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
245245 }
246246
247247 _, additions, removals := diff.GenerateDiff(
@@ -266,7 +266,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
266266 },
267267 )
268268 if !p {
269− return ai.ToolResponse{}, permission.ErrorPermissionDenied
269+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
270270 }
271271
272272 if isCrlf {
@@ -275,7 +275,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
275275
276276 err = os.WriteFile(filePath, []byte(newContent), 0o644)
277277 if err != nil {
278− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
278+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
279279 }
280280
281281 // Check if file exists in history
@@ -284,7 +284,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
284284 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
285285 if err != nil {
286286 // Log error but don't fail the operation
287− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
287+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
288288 }
289289 }
290290 if file.Content != oldContent {
@@ -303,8 +303,8 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
303303 recordFileWrite(filePath)
304304 recordFileRead(filePath)
305305
306− return ai.WithResponseMetadata(
307− ai.NewTextResponse("Content deleted from file: "+filePath),
306+ return fantasy.WithResponseMetadata(
307+ fantasy.NewTextResponse("Content deleted from file: "+filePath),
308308 EditResponseMetadata{
309309 OldContent: oldContent,
310310 NewContent: newContent,
@@ -314,27 +314,27 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
314314 ), nil
315315}
316316
317−func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
317+func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
318318 fileInfo, err := os.Stat(filePath)
319319 if err != nil {
320320 if os.IsNotExist(err) {
321− return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
321+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
322322 }
323− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
323+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
324324 }
325325
326326 if fileInfo.IsDir() {
327− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
327+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
328328 }
329329
330330 if getLastReadTime(filePath).IsZero() {
331− return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
331+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
332332 }
333333
334334 modTime := fileInfo.ModTime()
335335 lastRead := getLastReadTime(filePath)
336336 if modTime.After(lastRead) {
337− return ai.NewTextErrorResponse(
337+ return fantasy.NewTextErrorResponse(
338338 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
339339 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
340340 )), nil
@@ -342,7 +342,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
342342
343343 content, err := os.ReadFile(filePath)
344344 if err != nil {
345− return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
345+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
346346 }
347347
348348 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -354,17 +354,17 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
354354 newContent = strings.ReplaceAll(oldContent, oldString, newString)
355355 replacementCount = strings.Count(oldContent, oldString)
356356 if replacementCount == 0 {
357− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
357+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
358358 }
359359 } else {
360360 index := strings.Index(oldContent, oldString)
361361 if index == -1 {
362− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
362+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
363363 }
364364
365365 lastIndex := strings.LastIndex(oldContent, oldString)
366366 if index != lastIndex {
367− 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
367+ 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
368368 }
369369
370370 newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
@@ -372,12 +372,12 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
372372 }
373373
374374 if oldContent == newContent {
375− return ai.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
375+ return fantasy.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
376376 }
377377 sessionID := GetSessionFromContext(edit.ctx)
378378
379379 if sessionID == "" {
380− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
380+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
381381 }
382382 _, additions, removals := diff.GenerateDiff(
383383 oldContent,
@@ -401,7 +401,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
401401 },
402402 )
403403 if !p {
404− return ai.ToolResponse{}, permission.ErrorPermissionDenied
404+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
405405 }
406406
407407 if isCrlf {
@@ -410,7 +410,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
410410
411411 err = os.WriteFile(filePath, []byte(newContent), 0o644)
412412 if err != nil {
413− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
413+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
414414 }
415415
416416 // Check if file exists in history
@@ -419,7 +419,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
419419 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
420420 if err != nil {
421421 // Log error but don't fail the operation
422− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
422+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
423423 }
424424 }
425425 if file.Content != oldContent {
@@ -438,8 +438,8 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
438438 recordFileWrite(filePath)
439439 recordFileRead(filePath)
440440
441− return ai.WithResponseMetadata(
442− ai.NewTextResponse("Content replaced in file: "+filePath),
441+ return fantasy.WithResponseMetadata(
442+ fantasy.NewTextResponse("Content replaced in file: "+filePath),
443443 EditResponseMetadata{
444444 OldContent: oldContent,
445445 NewContent: newContent,
internal/agent/tools/fetch.go+20−20
@@ -10,10 +10,10 @@ import (
1010 "time"
1111 "unicode/utf8"
1212
13+ "charm.land/fantasy"
1314 md "github.com/JohannesKaufmann/html-to-markdown"
1415 "github.com/PuerkitoBio/goquery"
1516 "github.com/charmbracelet/crush/internal/permission"
16− "github.com/charmbracelet/fantasy/ai"
1717)
1818
1919type FetchParams struct {
@@ -39,7 +39,7 @@ const FetchToolName = "fetch"
3939//go:embed fetch.md
4040var fetchDescription []byte
4141
42−func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) ai.AgentTool {
42+func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
4343 if client == nil {
4444 client = &http.Client{
4545 Timeout: 30 * time.Second,
@@ -51,26 +51,26 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
5151 }
5252 }
5353
54− return ai.NewAgentTool(
54+ return fantasy.NewAgentTool(
5555 FetchToolName,
5656 string(fetchDescription),
57− func(ctx context.Context, params FetchParams, call ai.ToolCall) (ai.ToolResponse, error) {
57+ func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
5858 if params.URL == "" {
59− return ai.NewTextErrorResponse("URL parameter is required"), nil
59+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
6060 }
6161
6262 format := strings.ToLower(params.Format)
6363 if format != "text" && format != "markdown" && format != "html" {
64− return ai.NewTextErrorResponse("Format must be one of: text, markdown, html"), nil
64+ return fantasy.NewTextErrorResponse("Format must be one of: text, markdown, html"), nil
6565 }
6666
6767 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
68− return ai.NewTextErrorResponse("URL must start with http:// or https://"), nil
68+ return fantasy.NewTextErrorResponse("URL must start with http:// or https://"), nil
6969 }
7070
7171 sessionID := GetSessionFromContext(ctx)
7272 if sessionID == "" {
73− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
73+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
7474 }
7575
7676 p := permissions.Request(
@@ -86,7 +86,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
8686 )
8787
8888 if !p {
89− return ai.ToolResponse{}, permission.ErrorPermissionDenied
89+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
9090 }
9191
9292 // Handle timeout with context
@@ -103,32 +103,32 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
103103
104104 req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
105105 if err != nil {
106− return ai.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
106+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
107107 }
108108
109109 req.Header.Set("User-Agent", "crush/1.0")
110110
111111 resp, err := client.Do(req)
112112 if err != nil {
113− return ai.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
113+ return fantasy.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
114114 }
115115 defer resp.Body.Close()
116116
117117 if resp.StatusCode != http.StatusOK {
118− return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
118+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
119119 }
120120
121121 maxSize := int64(5 * 1024 * 1024) // 5MB
122122 body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
123123 if err != nil {
124− return ai.NewTextErrorResponse("Failed to read response body: " + err.Error()), nil
124+ return fantasy.NewTextErrorResponse("Failed to read response body: " + err.Error()), nil
125125 }
126126
127127 content := string(body)
128128
129129 isValidUt8 := utf8.ValidString(content)
130130 if !isValidUt8 {
131− return ai.NewTextErrorResponse("Response content is not valid UTF-8"), nil
131+ return fantasy.NewTextErrorResponse("Response content is not valid UTF-8"), nil
132132 }
133133 contentType := resp.Header.Get("Content-Type")
134134
@@ -137,7 +137,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
137137 if strings.Contains(contentType, "text/html") {
138138 text, err := extractTextFromHTML(content)
139139 if err != nil {
140− return ai.NewTextErrorResponse("Failed to extract text from HTML: " + err.Error()), nil
140+ return fantasy.NewTextErrorResponse("Failed to extract text from HTML: " + err.Error()), nil
141141 }
142142 content = text
143143 }
@@ -146,7 +146,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
146146 if strings.Contains(contentType, "text/html") {
147147 markdown, err := convertHTMLToMarkdown(content)
148148 if err != nil {
149− return ai.NewTextErrorResponse("Failed to convert HTML to Markdown: " + err.Error()), nil
149+ return fantasy.NewTextErrorResponse("Failed to convert HTML to Markdown: " + err.Error()), nil
150150 }
151151 content = markdown
152152 }
@@ -158,14 +158,14 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
158158 if strings.Contains(contentType, "text/html") {
159159 doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
160160 if err != nil {
161− return ai.NewTextErrorResponse("Failed to parse HTML: " + err.Error()), nil
161+ return fantasy.NewTextErrorResponse("Failed to parse HTML: " + err.Error()), nil
162162 }
163163 body, err := doc.Find("body").Html()
164164 if err != nil {
165− return ai.NewTextErrorResponse("Failed to extract body from HTML: " + err.Error()), nil
165+ return fantasy.NewTextErrorResponse("Failed to extract body from HTML: " + err.Error()), nil
166166 }
167167 if body == "" {
168− return ai.NewTextErrorResponse("No body content found in HTML"), nil
168+ return fantasy.NewTextErrorResponse("No body content found in HTML"), nil
169169 }
170170 content = "<html>\n<body>\n" + body + "\n</body>\n</html>"
171171 }
@@ -177,7 +177,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
177177 content += fmt.Sprintf("\n\n[Content truncated to %d bytes]", MaxReadSize)
178178 }
179179
180− return ai.NewTextResponse(content), nil
180+ return fantasy.NewTextResponse(content), nil
181181 })
182182}
183183
internal/agent/tools/glob.go+8−8
@@ -11,8 +11,8 @@ import (
1111 "sort"
1212 "strings"
1313
14+ "charm.land/fantasy"
1415 "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/fantasy/ai"
1616)
1717
1818const GlobToolName = "glob"
@@ -30,13 +30,13 @@ type GlobResponseMetadata struct {
3030 Truncated bool `json:"truncated"`
3131}
3232
33−func NewGlobTool(workingDir string) ai.AgentTool {
34− return ai.NewAgentTool(
33+func NewGlobTool(workingDir string) fantasy.AgentTool {
34+ return fantasy.NewAgentTool(
3535 GlobToolName,
3636 string(globDescription),
37− func(ctx context.Context, params GlobParams, call ai.ToolCall) (ai.ToolResponse, error) {
37+ func(ctx context.Context, params GlobParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
3838 if params.Pattern == "" {
39− return ai.NewTextErrorResponse("pattern is required"), nil
39+ return fantasy.NewTextErrorResponse("pattern is required"), nil
4040 }
4141
4242 searchPath := params.Path
@@ -46,7 +46,7 @@ func NewGlobTool(workingDir string) ai.AgentTool {
4646
4747 files, truncated, err := globFiles(ctx, params.Pattern, searchPath, 100)
4848 if err != nil {
49− return ai.ToolResponse{}, fmt.Errorf("error finding files: %w", err)
49+ return fantasy.ToolResponse{}, fmt.Errorf("error finding files: %w", err)
5050 }
5151
5252 var output string
@@ -59,8 +59,8 @@ func NewGlobTool(workingDir string) ai.AgentTool {
5959 }
6060 }
6161
62− return ai.WithResponseMetadata(
63− ai.NewTextResponse(output),
62+ return fantasy.WithResponseMetadata(
63+ fantasy.NewTextResponse(output),
6464 GlobResponseMetadata{
6565 NumberOfFiles: len(files),
6666 Truncated: truncated,
internal/agent/tools/grep.go+8−8
@@ -16,8 +16,8 @@ import (
1616 "sync"
1717 "time"
1818
19+ "charm.land/fantasy"
1920 "github.com/charmbracelet/crush/internal/fsext"
20− "github.com/charmbracelet/fantasy/ai"
2121)
2222
2323// regexCache provides thread-safe caching of compiled regex patterns
@@ -97,13 +97,13 @@ const (
9797//go:embed grep.md
9898var grepDescription []byte
9999
100−func NewGrepTool(workingDir string) ai.AgentTool {
101− return ai.NewAgentTool(
100+func NewGrepTool(workingDir string) fantasy.AgentTool {
101+ return fantasy.NewAgentTool(
102102 GrepToolName,
103103 string(grepDescription),
104− func(ctx context.Context, params GrepParams, call ai.ToolCall) (ai.ToolResponse, error) {
104+ func(ctx context.Context, params GrepParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
105105 if params.Pattern == "" {
106− return ai.NewTextErrorResponse("pattern is required"), nil
106+ return fantasy.NewTextErrorResponse("pattern is required"), nil
107107 }
108108
109109 // If literal_text is true, escape the pattern
@@ -119,7 +119,7 @@ func NewGrepTool(workingDir string) ai.AgentTool {
119119
120120 matches, truncated, err := searchFiles(ctx, searchPattern, searchPath, params.Include, 100)
121121 if err != nil {
122− return ai.ToolResponse{}, fmt.Errorf("error searching files: %w", err)
122+ return fantasy.ToolResponse{}, fmt.Errorf("error searching files: %w", err)
123123 }
124124
125125 var output strings.Builder
@@ -153,8 +153,8 @@ func NewGrepTool(workingDir string) ai.AgentTool {
153153 }
154154 }
155155
156− return ai.WithResponseMetadata(
157− ai.NewTextResponse(output.String()),
156+ return fantasy.WithResponseMetadata(
157+ fantasy.NewTextResponse(output.String()),
158158 GrepResponseMetadata{
159159 NumberOfMatches: len(matches),
160160 Truncated: truncated,
internal/agent/tools/ls.go+12−12
@@ -9,10 +9,10 @@ import (
99 "path/filepath"
1010 "strings"
1111
12+ "charm.land/fantasy"
1213 "github.com/charmbracelet/crush/internal/config"
1314 "github.com/charmbracelet/crush/internal/fsext"
1415 "github.com/charmbracelet/crush/internal/permission"
15− "github.com/charmbracelet/fantasy/ai"
1616)
1717
1818type LSParams struct {
@@ -47,14 +47,14 @@ const (
4747//go:embed ls.md
4848var lsDescription []byte
4949
50−func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) ai.AgentTool {
51− return ai.NewAgentTool(
50+func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) fantasy.AgentTool {
51+ return fantasy.NewAgentTool(
5252 LSToolName,
5353 string(lsDescription),
54− func(ctx context.Context, params LSParams, call ai.ToolCall) (ai.ToolResponse, error) {
54+ func(ctx context.Context, params LSParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
5555 searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
5656 if err != nil {
57− return ai.ToolResponse{}, fmt.Errorf("error expanding path: %w", err)
57+ return fantasy.ToolResponse{}, fmt.Errorf("error expanding path: %w", err)
5858 }
5959
6060 if !filepath.IsAbs(searchPath) {
@@ -64,12 +64,12 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
6464 // Check if directory is outside working directory and request permission if needed
6565 absWorkingDir, err := filepath.Abs(workingDir)
6666 if err != nil {
67− return ai.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
67+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
6868 }
6969
7070 absSearchPath, err := filepath.Abs(searchPath)
7171 if err != nil {
72− return ai.ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
72+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
7373 }
7474
7575 relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
@@ -77,7 +77,7 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
7777 // Directory is outside working directory, request permission
7878 sessionID := GetSessionFromContext(ctx)
7979 if sessionID == "" {
80− return ai.ToolResponse{}, fmt.Errorf("session ID is required for accessing directories outside working directory")
80+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for accessing directories outside working directory")
8181 }
8282
8383 granted := permissions.Request(
@@ -93,17 +93,17 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
9393 )
9494
9595 if !granted {
96− return ai.ToolResponse{}, permission.ErrorPermissionDenied
96+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
9797 }
9898 }
9999
100100 output, metadata, err := ListDirectoryTree(searchPath, params, lsConfig)
101101 if err != nil {
102− return ai.ToolResponse{}, err
102+ return fantasy.ToolResponse{}, err
103103 }
104104
105− return ai.WithResponseMetadata(
106− ai.NewTextResponse(output),
105+ return fantasy.WithResponseMetadata(
106+ fantasy.NewTextResponse(output),
107107 metadata,
108108 ), nil
109109 })
internal/agent/tools/mcp-tools.go+14−14
@@ -16,13 +16,13 @@ import (
1616 "sync"
1717 "time"
1818
19+ "charm.land/fantasy"
1920 "github.com/charmbracelet/crush/internal/config"
2021 "github.com/charmbracelet/crush/internal/csync"
2122 "github.com/charmbracelet/crush/internal/home"
2223 "github.com/charmbracelet/crush/internal/permission"
2324 "github.com/charmbracelet/crush/internal/pubsub"
2425 "github.com/charmbracelet/crush/internal/version"
25− "github.com/charmbracelet/fantasy/ai"
2626 "github.com/modelcontextprotocol/go-sdk/mcp"
2727)
2828
@@ -92,14 +92,14 @@ type McpTool struct {
9292 tool *mcp.Tool
9393 permissions permission.Service
9494 workingDir string
95− providerOptions ai.ProviderOptions
95+ providerOptions fantasy.ProviderOptions
9696}
9797
98−func (m *McpTool) SetProviderOptions(opts ai.ProviderOptions) {
98+func (m *McpTool) SetProviderOptions(opts fantasy.ProviderOptions) {
9999 m.providerOptions = opts
100100}
101101
102−func (m *McpTool) ProviderOptions() ai.ProviderOptions {
102+func (m *McpTool) ProviderOptions() fantasy.ProviderOptions {
103103 return m.providerOptions
104104}
105105
@@ -115,7 +115,7 @@ func (m *McpTool) MCPToolName() string {
115115 return m.tool.Name
116116}
117117
118−func (b *McpTool) Info() ai.ToolInfo {
118+func (b *McpTool) Info() fantasy.ToolInfo {
119119 input := b.tool.InputSchema.(map[string]any)
120120 required, _ := input["required"].([]string)
121121 if required == nil {
@@ -125,7 +125,7 @@ func (b *McpTool) Info() ai.ToolInfo {
125125 if parameters == nil {
126126 parameters = make(map[string]any)
127127 }
128− return ai.ToolInfo{
128+ return fantasy.ToolInfo{
129129 Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
130130 Description: b.tool.Description,
131131 Parameters: parameters,
@@ -133,22 +133,22 @@ func (b *McpTool) Info() ai.ToolInfo {
133133 }
134134}
135135
136−func runTool(ctx context.Context, name, toolName string, input string) (ai.ToolResponse, error) {
136+func runTool(ctx context.Context, name, toolName string, input string) (fantasy.ToolResponse, error) {
137137 var args map[string]any
138138 if err := json.Unmarshal([]byte(input), &args); err != nil {
139− return ai.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
139+ return fantasy.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
140140 }
141141
142142 c, err := getOrRenewClient(ctx, name)
143143 if err != nil {
144− return ai.NewTextErrorResponse(err.Error()), nil
144+ return fantasy.NewTextErrorResponse(err.Error()), nil
145145 }
146146 result, err := c.CallTool(ctx, &mcp.CallToolParams{
147147 Name: toolName,
148148 Arguments: args,
149149 })
150150 if err != nil {
151− return ai.NewTextErrorResponse(err.Error()), nil
151+ return fantasy.NewTextErrorResponse(err.Error()), nil
152152 }
153153
154154 output := make([]string, 0, len(result.Content))
@@ -159,7 +159,7 @@ func runTool(ctx context.Context, name, toolName string, input string) (ai.ToolR
159159 output = append(output, fmt.Sprintf("%v", v))
160160 }
161161 }
162− return ai.NewTextResponse(strings.Join(output, "\n")), nil
162+ return fantasy.NewTextResponse(strings.Join(output, "\n")), nil
163163}
164164
165165func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
@@ -191,10 +191,10 @@ func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, err
191191 return sess, nil
192192}
193193
194−func (m *McpTool) Run(ctx context.Context, params ai.ToolCall) (ai.ToolResponse, error) {
194+func (m *McpTool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
195195 sessionID := GetSessionFromContext(ctx)
196196 if sessionID == "" {
197− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
197+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
198198 }
199199 permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name)
200200 p := m.permissions.Request(
@@ -209,7 +209,7 @@ func (m *McpTool) Run(ctx context.Context, params ai.ToolCall) (ai.ToolResponse,
209209 },
210210 )
211211 if !p {
212− return ai.ToolResponse{}, permission.ErrorPermissionDenied
212+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
213213 }
214214
215215 return runTool(ctx, m.mcpName, m.tool.Name, params.Input)
internal/agent/tools/multiedit.go+35−35
@@ -10,13 +10,13 @@ import (
1010 "strings"
1111 "time"
1212
13+ "charm.land/fantasy"
1314 "github.com/charmbracelet/crush/internal/csync"
1415 "github.com/charmbracelet/crush/internal/diff"
1516 "github.com/charmbracelet/crush/internal/fsext"
1617 "github.com/charmbracelet/crush/internal/history"
1718 "github.com/charmbracelet/crush/internal/lsp"
1819 "github.com/charmbracelet/crush/internal/permission"
19− "github.com/charmbracelet/fantasy/ai"
2020)
2121
2222type MultiEditOperation struct {
@@ -49,17 +49,17 @@ const MultiEditToolName = "multiedit"
4949//go:embed multiedit.md
5050var multieditDescription []byte
5151
52−func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
53− return ai.NewAgentTool(
52+func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
53+ return fantasy.NewAgentTool(
5454 MultiEditToolName,
5555 string(multieditDescription),
56− func(ctx context.Context, params MultiEditParams, call ai.ToolCall) (ai.ToolResponse, error) {
56+ func(ctx context.Context, params MultiEditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
5757 if params.FilePath == "" {
58− return ai.NewTextErrorResponse("file_path is required"), nil
58+ return fantasy.NewTextErrorResponse("file_path is required"), nil
5959 }
6060
6161 if len(params.Edits) == 0 {
62− return ai.NewTextErrorResponse("at least one edit operation is required"), nil
62+ return fantasy.NewTextErrorResponse("at least one edit operation is required"), nil
6363 }
6464
6565 if !filepath.IsAbs(params.FilePath) {
@@ -68,10 +68,10 @@ func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions pe
6868
6969 // Validate all edits before applying any
7070 if err := validateEdits(params.Edits); err != nil {
71− return ai.NewTextErrorResponse(err.Error()), nil
71+ return fantasy.NewTextErrorResponse(err.Error()), nil
7272 }
7373
74− var response ai.ToolResponse
74+ var response fantasy.ToolResponse
7575 var err error
7676
7777 editCtx := editContext{ctx, permissions, files, workingDir}
@@ -114,24 +114,24 @@ func validateEdits(edits []MultiEditOperation) error {
114114 return nil
115115}
116116
117−func processMultiEditWithCreation(edit editContext, params MultiEditParams, call ai.ToolCall) (ai.ToolResponse, error) {
117+func processMultiEditWithCreation(edit editContext, params MultiEditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
118118 // First edit creates the file
119119 firstEdit := params.Edits[0]
120120 if firstEdit.OldString != "" {
121− return ai.NewTextErrorResponse("first edit must have empty old_string for file creation"), nil
121+ return fantasy.NewTextErrorResponse("first edit must have empty old_string for file creation"), nil
122122 }
123123
124124 // Check if file already exists
125125 if _, err := os.Stat(params.FilePath); err == nil {
126− return ai.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", params.FilePath)), nil
126+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", params.FilePath)), nil
127127 } else if !os.IsNotExist(err) {
128− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
128+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
129129 }
130130
131131 // Create parent directories
132132 dir := filepath.Dir(params.FilePath)
133133 if err := os.MkdirAll(dir, 0o755); err != nil {
134− return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
134+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
135135 }
136136
137137 // Start with the content from the first edit
@@ -142,7 +142,7 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
142142 edit := params.Edits[i]
143143 newContent, err := applyEditToContent(currentContent, edit)
144144 if err != nil {
145− return ai.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
145+ return fantasy.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
146146 }
147147 currentContent = newContent
148148 }
@@ -150,7 +150,7 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
150150 // Get session and message IDs
151151 sessionID := GetSessionFromContext(edit.ctx)
152152 if sessionID == "" {
153− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
153+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
154154 }
155155
156156 // Check permissions
@@ -170,19 +170,19 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
170170 },
171171 })
172172 if !p {
173− return ai.ToolResponse{}, permission.ErrorPermissionDenied
173+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
174174 }
175175
176176 // Write the file
177177 err := os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
178178 if err != nil {
179− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
179+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
180180 }
181181
182182 // Update file history
183183 _, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, "")
184184 if err != nil {
185− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
185+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
186186 }
187187
188188 _, err = edit.files.CreateVersion(edit.ctx, sessionID, params.FilePath, currentContent)
@@ -193,8 +193,8 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
193193 recordFileWrite(params.FilePath)
194194 recordFileRead(params.FilePath)
195195
196− return ai.WithResponseMetadata(
197− ai.NewTextResponse(fmt.Sprintf("File created with %d edits: %s", len(params.Edits), params.FilePath)),
196+ return fantasy.WithResponseMetadata(
197+ fantasy.NewTextResponse(fmt.Sprintf("File created with %d edits: %s", len(params.Edits), params.FilePath)),
198198 MultiEditResponseMetadata{
199199 OldContent: "",
200200 NewContent: currentContent,
@@ -205,30 +205,30 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
205205 ), nil
206206}
207207
208−func processMultiEditExistingFile(edit editContext, params MultiEditParams, call ai.ToolCall) (ai.ToolResponse, error) {
208+func processMultiEditExistingFile(edit editContext, params MultiEditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
209209 // Validate file exists and is readable
210210 fileInfo, err := os.Stat(params.FilePath)
211211 if err != nil {
212212 if os.IsNotExist(err) {
213− return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", params.FilePath)), nil
213+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", params.FilePath)), nil
214214 }
215− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
215+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
216216 }
217217
218218 if fileInfo.IsDir() {
219− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", params.FilePath)), nil
219+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", params.FilePath)), nil
220220 }
221221
222222 // Check if file was read before editing
223223 if getLastReadTime(params.FilePath).IsZero() {
224− return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
224+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
225225 }
226226
227227 // Check if file was modified since last read
228228 modTime := fileInfo.ModTime()
229229 lastRead := getLastReadTime(params.FilePath)
230230 if modTime.After(lastRead) {
231− return ai.NewTextErrorResponse(
231+ return fantasy.NewTextErrorResponse(
232232 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
233233 params.FilePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
234234 )), nil
@@ -237,7 +237,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
237237 // Read current file content
238238 content, err := os.ReadFile(params.FilePath)
239239 if err != nil {
240− return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
240+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
241241 }
242242
243243 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -247,20 +247,20 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
247247 for i, edit := range params.Edits {
248248 newContent, err := applyEditToContent(currentContent, edit)
249249 if err != nil {
250− return ai.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
250+ return fantasy.NewTextErrorResponse(fmt.Sprintf("edit %d failed: %s", i+1, err.Error())), nil
251251 }
252252 currentContent = newContent
253253 }
254254
255255 // Check if content actually changed
256256 if oldContent == currentContent {
257− return ai.NewTextErrorResponse("no changes made - all edits resulted in identical content"), nil
257+ return fantasy.NewTextErrorResponse("no changes made - all edits resulted in identical content"), nil
258258 }
259259
260260 // Get session and message IDs
261261 sessionID := GetSessionFromContext(edit.ctx)
262262 if sessionID == "" {
263− return ai.ToolResponse{}, fmt.Errorf("session ID is required for editing file")
263+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for editing file")
264264 }
265265
266266 // Generate diff and check permissions
@@ -279,7 +279,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
279279 },
280280 })
281281 if !p {
282− return ai.ToolResponse{}, permission.ErrorPermissionDenied
282+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
283283 }
284284
285285 if isCrlf {
@@ -289,7 +289,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
289289 // Write the updated content
290290 err = os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
291291 if err != nil {
292− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
292+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
293293 }
294294
295295 // Update file history
@@ -297,7 +297,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
297297 if err != nil {
298298 _, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, oldContent)
299299 if err != nil {
300− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
300+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
301301 }
302302 }
303303 if file.Content != oldContent {
@@ -317,8 +317,8 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
317317 recordFileWrite(params.FilePath)
318318 recordFileRead(params.FilePath)
319319
320− return ai.WithResponseMetadata(
321− ai.NewTextResponse(fmt.Sprintf("Applied %d edits to file: %s", len(params.Edits), params.FilePath)),
320+ return fantasy.WithResponseMetadata(
321+ fantasy.NewTextResponse(fmt.Sprintf("Applied %d edits to file: %s", len(params.Edits), params.FilePath)),
322322 MultiEditResponseMetadata{
323323 OldContent: oldContent,
324324 NewContent: currentContent,
internal/agent/tools/sourcegraph.go+14−14
@@ -11,7 +11,7 @@ import (
1111 "strings"
1212 "time"
1313
14− "github.com/charmbracelet/fantasy/ai"
14+ "charm.land/fantasy"
1515)
1616
1717type SourcegraphParams struct {
@@ -31,7 +31,7 @@ const SourcegraphToolName = "sourcegraph"
3131//go:embed sourcegraph.md
3232var sourcegraphDescription []byte
3333
34−func NewSourcegraphTool(client *http.Client) ai.AgentTool {
34+func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
3535 if client == nil {
3636 client = &http.Client{
3737 Timeout: 30 * time.Second,
@@ -42,12 +42,12 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
4242 },
4343 }
4444 }
45− return ai.NewAgentTool(
45+ return fantasy.NewAgentTool(
4646 SourcegraphToolName,
4747 string(sourcegraphDescription),
48− func(ctx context.Context, params SourcegraphParams, call ai.ToolCall) (ai.ToolResponse, error) {
48+ func(ctx context.Context, params SourcegraphParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
4949 if params.Query == "" {
50− return ai.NewTextErrorResponse("Query parameter is required"), nil
50+ return fantasy.NewTextErrorResponse("Query parameter is required"), nil
5151 }
5252
5353 if params.Count <= 0 {
@@ -86,7 +86,7 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
8686
8787 graphqlQueryBytes, err := json.Marshal(request)
8888 if err != nil {
89− return ai.ToolResponse{}, fmt.Errorf("failed to marshal GraphQL request: %w", err)
89+ return fantasy.ToolResponse{}, fmt.Errorf("failed to marshal GraphQL request: %w", err)
9090 }
9191 graphqlQuery := string(graphqlQueryBytes)
9292
@@ -97,7 +97,7 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
9797 bytes.NewBuffer([]byte(graphqlQuery)),
9898 )
9999 if err != nil {
100− return ai.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
100+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
101101 }
102102
103103 req.Header.Set("Content-Type", "application/json")
@@ -105,34 +105,34 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
105105
106106 resp, err := client.Do(req)
107107 if err != nil {
108− return ai.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
108+ return fantasy.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
109109 }
110110 defer resp.Body.Close()
111111
112112 if resp.StatusCode != http.StatusOK {
113113 body, _ := io.ReadAll(resp.Body)
114114 if len(body) > 0 {
115− return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d, response: %s", resp.StatusCode, string(body))), nil
115+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d, response: %s", resp.StatusCode, string(body))), nil
116116 }
117117
118− return ai.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
118+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
119119 }
120120 body, err := io.ReadAll(resp.Body)
121121 if err != nil {
122− return ai.ToolResponse{}, fmt.Errorf("failed to read response body: %w", err)
122+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read response body: %w", err)
123123 }
124124
125125 var result map[string]any
126126 if err = json.Unmarshal(body, &result); err != nil {
127− return ai.ToolResponse{}, fmt.Errorf("failed to unmarshal response: %w", err)
127+ return fantasy.ToolResponse{}, fmt.Errorf("failed to unmarshal response: %w", err)
128128 }
129129
130130 formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
131131 if err != nil {
132− return ai.NewTextErrorResponse("Failed to format results: " + err.Error()), nil
132+ return fantasy.NewTextErrorResponse("Failed to format results: " + err.Error()), nil
133133 }
134134
135− return ai.NewTextResponse(formattedResults), nil
135+ return fantasy.NewTextResponse(formattedResults), nil
136136 })
137137}
138138
internal/agent/tools/view.go+19−19
@@ -11,10 +11,10 @@ import (
1111 "strings"
1212 "unicode/utf8"
1313
14+ "charm.land/fantasy"
1415 "github.com/charmbracelet/crush/internal/csync"
1516 "github.com/charmbracelet/crush/internal/lsp"
1617 "github.com/charmbracelet/crush/internal/permission"
17− "github.com/charmbracelet/fantasy/ai"
1818)
1919
2020//go:embed view.md
@@ -50,13 +50,13 @@ const (
5050 MaxLineLength = 2000
5151)
5252
53−func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) ai.AgentTool {
54− return ai.NewAgentTool(
53+func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) fantasy.AgentTool {
54+ return fantasy.NewAgentTool(
5555 ViewToolName,
5656 string(viewDescription),
57− func(ctx context.Context, params ViewParams, call ai.ToolCall) (ai.ToolResponse, error) {
57+ func(ctx context.Context, params ViewParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
5858 if params.FilePath == "" {
59− return ai.NewTextErrorResponse("file_path is required"), nil
59+ return fantasy.NewTextErrorResponse("file_path is required"), nil
6060 }
6161
6262 // Handle relative paths
@@ -68,12 +68,12 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
6868 // Check if file is outside working directory and request permission if needed
6969 absWorkingDir, err := filepath.Abs(workingDir)
7070 if err != nil {
71− return ai.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
71+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
7272 }
7373
7474 absFilePath, err := filepath.Abs(filePath)
7575 if err != nil {
76− return ai.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
76+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
7777 }
7878
7979 relPath, err := filepath.Rel(absWorkingDir, absFilePath)
@@ -81,7 +81,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
8181 // File is outside working directory, request permission
8282 sessionID := GetSessionFromContext(ctx)
8383 if sessionID == "" {
84− return ai.ToolResponse{}, fmt.Errorf("session ID is required for accessing files outside working directory")
84+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for accessing files outside working directory")
8585 }
8686
8787 granted := permissions.Request(
@@ -97,7 +97,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
9797 )
9898
9999 if !granted {
100− return ai.ToolResponse{}, permission.ErrorPermissionDenied
100+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
101101 }
102102 }
103103
@@ -123,24 +123,24 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
123123 }
124124
125125 if len(suggestions) > 0 {
126− return ai.NewTextErrorResponse(fmt.Sprintf("File not found: %s\n\nDid you mean one of these?\n%s",
126+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File not found: %s\n\nDid you mean one of these?\n%s",
127127 filePath, strings.Join(suggestions, "\n"))), nil
128128 }
129129 }
130130
131− return ai.NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
131+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
132132 }
133− return ai.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
133+ return fantasy.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
134134 }
135135
136136 // Check if it's a directory
137137 if fileInfo.IsDir() {
138− return ai.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
138+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
139139 }
140140
141141 // Check file size
142142 if fileInfo.Size() > MaxReadSize {
143− return ai.NewTextErrorResponse(fmt.Sprintf("File is too large (%d bytes). Maximum size is %d bytes",
143+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File is too large (%d bytes). Maximum size is %d bytes",
144144 fileInfo.Size(), MaxReadSize)), nil
145145 }
146146
@@ -153,17 +153,17 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
153153 isImage, imageType := isImageFile(filePath)
154154 // TODO: handle images
155155 if isImage {
156− return ai.NewTextErrorResponse(fmt.Sprintf("This is an image file of type: %s\n", imageType)), nil
156+ return fantasy.NewTextErrorResponse(fmt.Sprintf("This is an image file of type: %s\n", imageType)), nil
157157 }
158158
159159 // Read the file content
160160 content, lineCount, err := readTextFile(filePath, params.Offset, params.Limit)
161161 isValidUt8 := utf8.ValidString(content)
162162 if !isValidUt8 {
163− return ai.NewTextErrorResponse("File content is not valid UTF-8"), nil
163+ return fantasy.NewTextErrorResponse("File content is not valid UTF-8"), nil
164164 }
165165 if err != nil {
166− return ai.ToolResponse{}, fmt.Errorf("error reading file: %w", err)
166+ return fantasy.ToolResponse{}, fmt.Errorf("error reading file: %w", err)
167167 }
168168
169169 notifyLSPs(ctx, lspClients, filePath)
@@ -179,8 +179,8 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
179179 output += "\n</file>\n"
180180 output += getDiagnostics(filePath, lspClients)
181181 recordFileRead(filePath)
182− return ai.WithResponseMetadata(
183− ai.NewTextResponse(output),
182+ return fantasy.WithResponseMetadata(
183+ fantasy.NewTextResponse(output),
184184 ViewResponseMetadata{
185185 FilePath: filePath,
186186 Content: content,
internal/agent/tools/write.go+16−16
@@ -10,11 +10,11 @@ import (
1010 "strings"
1111 "time"
1212
13+ "charm.land/fantasy"
1314 "github.com/charmbracelet/crush/internal/csync"
1415 "github.com/charmbracelet/crush/internal/diff"
1516 "github.com/charmbracelet/crush/internal/fsext"
1617 "github.com/charmbracelet/crush/internal/history"
17− "github.com/charmbracelet/fantasy/ai"
1818
1919 "github.com/charmbracelet/crush/internal/lsp"
2020 "github.com/charmbracelet/crush/internal/permission"
@@ -49,17 +49,17 @@ type WriteResponseMetadata struct {
4949
5050const WriteToolName = "write"
5151
52−func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
53− return ai.NewAgentTool(
52+func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
53+ return fantasy.NewAgentTool(
5454 WriteToolName,
5555 string(writeDescription),
56− func(ctx context.Context, params WriteParams, call ai.ToolCall) (ai.ToolResponse, error) {
56+ func(ctx context.Context, params WriteParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
5757 if params.FilePath == "" {
58− return ai.NewTextErrorResponse("file_path is required"), nil
58+ return fantasy.NewTextErrorResponse("file_path is required"), nil
5959 }
6060
6161 if params.Content == "" {
62− return ai.NewTextErrorResponse("content is required"), nil
62+ return fantasy.NewTextErrorResponse("content is required"), nil
6363 }
6464
6565 filePath := params.FilePath
@@ -70,27 +70,27 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
7070 fileInfo, err := os.Stat(filePath)
7171 if err == nil {
7272 if fileInfo.IsDir() {
73− return ai.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
73+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
7474 }
7575
7676 modTime := fileInfo.ModTime()
7777 lastRead := getLastReadTime(filePath)
7878 if modTime.After(lastRead) {
79− 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.",
79+ 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.",
8080 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
8181 }
8282
8383 oldContent, readErr := os.ReadFile(filePath)
8484 if readErr == nil && string(oldContent) == params.Content {
85− return ai.NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
85+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
8686 }
8787 } else if !os.IsNotExist(err) {
88− return ai.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
88+ return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
8989 }
9090
9191 dir := filepath.Dir(filePath)
9292 if err = os.MkdirAll(dir, 0o755); err != nil {
93− return ai.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
93+ return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
9494 }
9595
9696 oldContent := ""
@@ -103,7 +103,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
103103
104104 sessionID := GetSessionFromContext(ctx)
105105 if sessionID == "" {
106− return ai.ToolResponse{}, fmt.Errorf("session_id is required")
106+ return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
107107 }
108108
109109 diff, additions, removals := diff.GenerateDiff(
@@ -128,12 +128,12 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
128128 },
129129 )
130130 if !p {
131− return ai.ToolResponse{}, permission.ErrorPermissionDenied
131+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
132132 }
133133
134134 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
135135 if err != nil {
136− return ai.ToolResponse{}, fmt.Errorf("error writing file: %w", err)
136+ return fantasy.ToolResponse{}, fmt.Errorf("error writing file: %w", err)
137137 }
138138
139139 // Check if file exists in history
@@ -142,7 +142,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
142142 _, err = files.Create(ctx, sessionID, filePath, oldContent)
143143 if err != nil {
144144 // Log error but don't fail the operation
145− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
145+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
146146 }
147147 }
148148 if file.Content != oldContent {
@@ -166,7 +166,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
166166 result := fmt.Sprintf("File successfully written: %s", filePath)
167167 result = fmt.Sprintf("<result>\n%s\n</result>", result)
168168 result += getDiagnostics(filePath, lspClients)
169− return ai.WithResponseMetadata(ai.NewTextResponse(result),
169+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
170170 WriteResponseMetadata{
171171 Diff: diff,
172172 Additions: additions,
internal/app/app.go+2−2
@@ -9,6 +9,7 @@ import (
99 "sync"
1010 "time"
1111
12+ "charm.land/fantasy"
1213 tea "github.com/charmbracelet/bubbletea/v2"
1314 "github.com/charmbracelet/crush/internal/agent"
1415 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -23,7 +24,6 @@ import (
2324 "github.com/charmbracelet/crush/internal/permission"
2425 "github.com/charmbracelet/crush/internal/pubsub"
2526 "github.com/charmbracelet/crush/internal/session"
26− "github.com/charmbracelet/fantasy/ai"
2727 "github.com/charmbracelet/x/ansi"
2828)
2929
@@ -145,7 +145,7 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
145145 app.Permissions.AutoApproveSession(sess.ID)
146146
147147 type response struct {
148− result *ai.AgentResult
148+ result *fantasy.AgentResult
149149 err error
150150 }
151151 done := make(chan response, 1)
internal/message/content.go+23−23
@@ -7,9 +7,9 @@ import (
77 "strings"
88 "time"
99
10+ "charm.land/fantasy"
11+ "charm.land/fantasy/providers/anthropic"
1012 "github.com/charmbracelet/catwalk/pkg/catwalk"
11− "github.com/charmbracelet/fantasy/ai"
12− "github.com/charmbracelet/fantasy/anthropic"
1313)
1414
1515type MessageRole string
@@ -390,35 +390,35 @@ func (m *Message) AddBinary(mimeType string, data []byte) {
390390 m.Parts = append(m.Parts, BinaryContent{MIMEType: mimeType, Data: data})
391391}
392392
393−func (m *Message) ToAIMessage() []ai.Message {
394− var messages []ai.Message
393+func (m *Message) ToAIMessage() []fantasy.Message {
394+ var messages []fantasy.Message
395395 switch m.Role {
396396 case User:
397− var parts []ai.MessagePart
397+ var parts []fantasy.MessagePart
398398 text := strings.TrimSpace(m.Content().Text)
399399 if text != "" {
400− parts = append(parts, ai.TextPart{Text: text})
400+ parts = append(parts, fantasy.TextPart{Text: text})
401401 }
402402 for _, content := range m.BinaryContent() {
403− parts = append(parts, ai.FilePart{
403+ parts = append(parts, fantasy.FilePart{
404404 Filename: content.Path,
405405 Data: content.Data,
406406 MediaType: content.MIMEType,
407407 })
408408 }
409− messages = append(messages, ai.Message{
410− Role: ai.MessageRoleUser,
409+ messages = append(messages, fantasy.Message{
410+ Role: fantasy.MessageRoleUser,
411411 Content: parts,
412412 })
413413 case Assistant:
414− var parts []ai.MessagePart
414+ var parts []fantasy.MessagePart
415415 text := strings.TrimSpace(m.Content().Text)
416416 if text != "" {
417− parts = append(parts, ai.TextPart{Text: text})
417+ parts = append(parts, fantasy.TextPart{Text: text})
418418 }
419419 reasoning := m.ReasoningContent()
420420 if reasoning.Thinking != "" {
421− reasoningPart := ai.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: ai.ProviderOptions{}}
421+ reasoningPart := fantasy.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: fantasy.ProviderOptions{}}
422422 if reasoning.Signature != "" {
423423 reasoningPart.ProviderOptions["anthropic"] = &anthropic.ReasoningOptionMetadata{
424424 Signature: reasoning.Signature,
@@ -427,42 +427,42 @@ func (m *Message) ToAIMessage() []ai.Message {
427427 parts = append(parts, reasoningPart)
428428 }
429429 for _, call := range m.ToolCalls() {
430− parts = append(parts, ai.ToolCallPart{
430+ parts = append(parts, fantasy.ToolCallPart{
431431 ToolCallID: call.ID,
432432 ToolName: call.Name,
433433 Input: call.Input,
434434 ProviderExecuted: call.ProviderExecuted,
435435 })
436436 }
437− messages = append(messages, ai.Message{
438− Role: ai.MessageRoleAssistant,
437+ messages = append(messages, fantasy.Message{
438+ Role: fantasy.MessageRoleAssistant,
439439 Content: parts,
440440 })
441441 case Tool:
442− var parts []ai.MessagePart
442+ var parts []fantasy.MessagePart
443443 for _, result := range m.ToolResults() {
444− var content ai.ToolResultOutputContent
444+ var content fantasy.ToolResultOutputContent
445445 if result.IsError {
446− content = ai.ToolResultOutputContentError{
446+ content = fantasy.ToolResultOutputContentError{
447447 Error: errors.New(result.Content),
448448 }
449449 } else if result.Data != "" {
450− content = ai.ToolResultOutputContentMedia{
450+ content = fantasy.ToolResultOutputContentMedia{
451451 Data: result.Data,
452452 MediaType: result.MIMEType,
453453 }
454454 } else {
455− content = ai.ToolResultOutputContentText{
455+ content = fantasy.ToolResultOutputContentText{
456456 Text: result.Content,
457457 }
458458 }
459− parts = append(parts, ai.ToolResultPart{
459+ parts = append(parts, fantasy.ToolResultPart{
460460 ToolCallID: result.ToolCallID,
461461 Output: content,
462462 })
463463 }
464− messages = append(messages, ai.Message{
465− Role: ai.MessageRoleTool,
464+ messages = append(messages, fantasy.Message{
465+ Role: fantasy.MessageRoleTool,
466466 Content: parts,
467467 })
468468 }