on main help sign in
chore: upgrade to latest fantasy
d2c6789kujtimiihoxha
go.mod+17−1
@@ -3,6 +3,7 @@ module github.com/charmbracelet/crush
3go 1.25.0
3go 1.25.0
4
4
5require (
5require (
6+ charm.land/fantasy v0.0.0
6 github.com/JohannesKaufmann/html-to-markdown v1.6.0
7 github.com/JohannesKaufmann/html-to-markdown v1.6.0
7 github.com/MakeNowJust/heredoc v1.0.0
8 github.com/MakeNowJust/heredoc v1.0.0
8 github.com/PuerkitoBio/goquery v1.10.3
9 github.com/PuerkitoBio/goquery v1.10.3
@@ -15,7 +16,6 @@ require (
15 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619
16 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619
16 github.com/charmbracelet/catwalk v0.6.5-0.20251015124117-3bfe9eb6d20f
17 github.com/charmbracelet/catwalk v0.6.5-0.20251015124117-3bfe9eb6d20f
17 github.com/charmbracelet/fang v0.4.3
18 github.com/charmbracelet/fang v0.4.3
18− github.com/charmbracelet/fantasy v0.0.0-20251015132833-5bc4cc524d70
19 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
19 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
20 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea
20 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea
21 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
21 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
@@ -47,6 +47,8 @@ require (
47 mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
47 mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
48)
48)
49
49
50+replace charm.land/fantasy => ../../fantasy/main/
51+
50require (
52require (
51 cloud.google.com/go v0.116.0 // indirect
53 cloud.google.com/go v0.116.0 // indirect
52 cloud.google.com/go/auth v0.12.1 // indirect
54 cloud.google.com/go/auth v0.12.1 // indirect
@@ -56,6 +58,20 @@ require (
56 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
58 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
57 github.com/andybalholm/cascadia v1.3.3 // indirect
59 github.com/andybalholm/cascadia v1.3.3 // indirect
58 github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
60 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
59 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
75 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
60 github.com/aymerick/douceur v0.2.0 // indirect
76 github.com/aymerick/douceur v0.2.0 // indirect
61 github.com/bahlo/generic-list-go v0.2.0 // indirect
77 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
36github.com/anthropics/anthropic-sdk-go v1.12.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
36github.com/anthropics/anthropic-sdk-go v1.12.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
37github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
37github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
38github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
38github.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=
39github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
67github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
40github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
68github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
41github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
69github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
@@ -61,8 +89,6 @@ github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqI
61github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
89github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
62github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY=
90github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY=
63github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
91github.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=
66github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
92github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
67github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
93github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
68github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
94github.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 (
10 "sync"
10 "sync"
11 "time"
11 "time"
12
12
13+ "charm.land/fantasy"
14+ "charm.land/fantasy/providers/anthropic"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
15 "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/crush/internal/agent/tools"
16 "github.com/charmbracelet/crush/internal/agent/tools"
15 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/config"
@@ -17,8 +19,6 @@ import (
17 "github.com/charmbracelet/crush/internal/message"
19 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/permission"
19 "github.com/charmbracelet/crush/internal/session"
21 "github.com/charmbracelet/crush/internal/session"
20− "github.com/charmbracelet/fantasy/ai"
21− "github.com/charmbracelet/fantasy/anthropic"
22)
22)
23
23
24//go:embed templates/title.md
24//go:embed templates/title.md
@@ -30,7 +30,7 @@ var summaryPrompt []byte
30type SessionAgentCall struct {
30type SessionAgentCall struct {
31 SessionID string
31 SessionID string
32 Prompt string
32 Prompt string
33− ProviderOptions ai.ProviderOptions
33+ ProviderOptions fantasy.ProviderOptions
34 Attachments []message.Attachment
34 Attachments []message.Attachment
35 MaxOutputTokens int64
35 MaxOutputTokens int64
36 Temperature *float64
36 Temperature *float64
@@ -41,9 +41,9 @@ type SessionAgentCall struct {
41}
41}
42
42
43type SessionAgent interface {
43type SessionAgent interface {
44− Run(context.Context, SessionAgentCall) (*ai.AgentResult, error)
44+ Run(context.Context, SessionAgentCall) (*fantasy.AgentResult, error)
45 SetModels(large Model, small Model)
45 SetModels(large Model, small Model)
46− SetTools(tools []ai.AgentTool)
46+ SetTools(tools []fantasy.AgentTool)
47 Cancel(sessionID string)
47 Cancel(sessionID string)
48 CancelAll()
48 CancelAll()
49 IsSessionBusy(sessionID string) bool
49 IsSessionBusy(sessionID string) bool
@@ -55,7 +55,7 @@ type SessionAgent interface {
55}
55}
56
56
57type Model struct {
57type Model struct {
58− Model ai.LanguageModel
58+ Model fantasy.LanguageModel
59 CatwalkCfg catwalk.Model
59 CatwalkCfg catwalk.Model
60 ModelCfg config.SelectedModel
60 ModelCfg config.SelectedModel
61}
61}
@@ -64,7 +64,7 @@ type sessionAgent struct {
64 largeModel Model
64 largeModel Model
65 smallModel Model
65 smallModel Model
66 systemPrompt string
66 systemPrompt string
67− tools []ai.AgentTool
67+ tools []fantasy.AgentTool
68 sessions session.Service
68 sessions session.Service
69 messages message.Service
69 messages message.Service
70 disableAutoSummarize bool
70 disableAutoSummarize bool
@@ -80,7 +80,7 @@ type SessionAgentOptions struct {
80 DisableAutoSummarize bool
80 DisableAutoSummarize bool
81 Sessions session.Service
81 Sessions session.Service
82 Messages message.Service
82 Messages message.Service
83− Tools []ai.AgentTool
83+ Tools []fantasy.AgentTool
84}
84}
85
85
86func NewSessionAgent(
86func NewSessionAgent(
@@ -99,7 +99,7 @@ func NewSessionAgent(
99 }
99 }
100}
100}
101
101
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) {
103 if call.Prompt == "" {
103 if call.Prompt == "" {
104 return nil, ErrEmptyPrompt
104 return nil, ErrEmptyPrompt
105 }
105 }
@@ -123,10 +123,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
123 a.tools[len(a.tools)-1].SetProviderOptions(a.getCacheControlOptions())
123 a.tools[len(a.tools)-1].SetProviderOptions(a.getCacheControlOptions())
124 }
124 }
125
125
126− agent := ai.NewAgent(
126+ agent := fantasy.NewAgent(
127 a.largeModel.Model,
127 a.largeModel.Model,
128− ai.WithSystemPrompt(a.systemPrompt),
128+ fantasy.WithSystemPrompt(a.systemPrompt),
129− ai.WithTools(a.tools...),
129+ fantasy.WithTools(a.tools...),
130 )
130 )
131
131
132 sessionLock := sync.Mutex{}
132 sessionLock := sync.Mutex{}
@@ -169,7 +169,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
169
169
170 var currentAssistant *message.Message
170 var currentAssistant *message.Message
171 var shouldSummarize bool
171 var shouldSummarize bool
172− result, err := agent.Stream(genCtx, ai.AgentStreamCall{
172+ result, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
173 Prompt: call.Prompt,
173 Prompt: call.Prompt,
174 Files: files,
174 Files: files,
175 Messages: history,
175 Messages: history,
@@ -181,7 +181,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
181 TopK: call.TopK,
181 TopK: call.TopK,
182 FrequencyPenalty: call.FrequencyPenalty,
182 FrequencyPenalty: call.FrequencyPenalty,
183 // Before each step create the new assistant message
183 // 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) {
185 var assistantMsg message.Message
185 var assistantMsg message.Message
186 assistantMsg, err = a.messages.Create(callContext, call.SessionID, message.CreateMessageParams{
186 assistantMsg, err = a.messages.Create(callContext, call.SessionID, message.CreateMessageParams{
187 Role: message.Assistant,
187 Role: message.Assistant,
@@ -217,7 +217,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
217 systemMessageUpdated := false
217 systemMessageUpdated := false
218 for i, msg := range prepared.Messages {
218 for i, msg := range prepared.Messages {
219 // only add cache control to the last message
219 // only add cache control to the last message
220− if msg.Role == ai.MessageRoleSystem {
220+ if msg.Role == fantasy.MessageRoleSystem {
221 lastSystemRoleInx = i
221 lastSystemRoleInx = i
222 } else if !systemMessageUpdated {
222 } else if !systemMessageUpdated {
223 prepared.Messages[lastSystemRoleInx].ProviderOptions = a.getCacheControlOptions()
223 prepared.Messages[lastSystemRoleInx].ProviderOptions = a.getCacheControlOptions()
@@ -234,7 +234,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
234 currentAssistant.AppendReasoningContent(text)
234 currentAssistant.AppendReasoningContent(text)
235 return a.messages.Update(genCtx, *currentAssistant)
235 return a.messages.Update(genCtx, *currentAssistant)
236 },
236 },
237− OnReasoningEnd: func(id string, reasoning ai.ReasoningContent) error {
237+ OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
238 // handle anthropic signature
238 // handle anthropic signature
239 if anthropicData, ok := reasoning.ProviderMetadata[anthropic.Name]; ok {
239 if anthropicData, ok := reasoning.ProviderMetadata[anthropic.Name]; ok {
240 if reasoning, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok {
240 if reasoning, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok {
@@ -258,10 +258,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
258 currentAssistant.AddToolCall(toolCall)
258 currentAssistant.AddToolCall(toolCall)
259 return a.messages.Update(genCtx, *currentAssistant)
259 return a.messages.Update(genCtx, *currentAssistant)
260 },
260 },
261− OnRetry: func(err *ai.APICallError, delay time.Duration) {
261+ OnRetry: func(err *fantasy.APICallError, delay time.Duration) {
262 // TODO: implement
262 // TODO: implement
263 },
263 },
264− OnToolCall: func(tc ai.ToolCallContent) error {
264+ OnToolCall: func(tc fantasy.ToolCallContent) error {
265 toolCall := message.ToolCall{
265 toolCall := message.ToolCall{
266 ID: tc.ToolCallID,
266 ID: tc.ToolCallID,
267 Name: tc.ToolName,
267 Name: tc.ToolName,
@@ -272,22 +272,22 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
272 currentAssistant.AddToolCall(toolCall)
272 currentAssistant.AddToolCall(toolCall)
273 return a.messages.Update(genCtx, *currentAssistant)
273 return a.messages.Update(genCtx, *currentAssistant)
274 },
274 },
275− OnToolResult: func(result ai.ToolResultContent) error {
275+ OnToolResult: func(result fantasy.ToolResultContent) error {
276 var resultContent string
276 var resultContent string
277 isError := false
277 isError := false
278 switch result.Result.GetType() {
278 switch result.Result.GetType() {
279− case ai.ToolResultContentTypeText:
279+ case fantasy.ToolResultContentTypeText:
280− r, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentText](result.Result)
280+ r, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Result)
281 if ok {
281 if ok {
282 resultContent = r.Text
282 resultContent = r.Text
283 }
283 }
284− case ai.ToolResultContentTypeError:
284+ case fantasy.ToolResultContentTypeError:
285− r, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentError](result.Result)
285+ r, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Result)
286 if ok {
286 if ok {
287 isError = true
287 isError = true
288 resultContent = r.Error.Error()
288 resultContent = r.Error.Error()
289 }
289 }
290− case ai.ToolResultContentTypeMedia:
290+ case fantasy.ToolResultContentTypeMedia:
291 // TODO: handle this message type
291 // TODO: handle this message type
292 }
292 }
293 toolResult := message.ToolResult{
293 toolResult := message.ToolResult{
@@ -308,14 +308,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
308 }
308 }
309 return nil
309 return nil
310 },
310 },
311− OnStepFinish: func(stepResult ai.StepResult) error {
311+ OnStepFinish: func(stepResult fantasy.StepResult) error {
312 finishReason := message.FinishReasonUnknown
312 finishReason := message.FinishReasonUnknown
313 switch stepResult.FinishReason {
313 switch stepResult.FinishReason {
314− case ai.FinishReasonLength:
314+ case fantasy.FinishReasonLength:
315 finishReason = message.FinishReasonMaxTokens
315 finishReason = message.FinishReasonMaxTokens
316− case ai.FinishReasonStop:
316+ case fantasy.FinishReasonStop:
317 finishReason = message.FinishReasonEndTurn
317 finishReason = message.FinishReasonEndTurn
318− case ai.FinishReasonToolCalls:
318+ case fantasy.FinishReasonToolCalls:
319 finishReason = message.FinishReasonToolUse
319 finishReason = message.FinishReasonToolUse
320 }
320 }
321 currentAssistant.AddFinish(finishReason, "", "")
321 currentAssistant.AddFinish(finishReason, "", "")
@@ -328,8 +328,8 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
328 }
328 }
329 return a.messages.Update(genCtx, *currentAssistant)
329 return a.messages.Update(genCtx, *currentAssistant)
330 },
330 },
331− StopWhen: []ai.StopCondition{
331+ StopWhen: []fantasy.StopCondition{
332− func(_ []ai.StepResult) bool {
332+ func(_ []fantasy.StepResult) bool {
333 contextWindow := a.largeModel.CatwalkCfg.ContextWindow
333 contextWindow := a.largeModel.CatwalkCfg.ContextWindow
334 tokens := currentSession.CompletionTokens + currentSession.PromptTokens
334 tokens := currentSession.CompletionTokens + currentSession.PromptTokens
335 percentage := (float64(tokens) / float64(contextWindow)) * 100
335 percentage := (float64(tokens) / float64(contextWindow)) * 100
@@ -461,8 +461,8 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
461 defer a.activeRequests.Del(sessionID)
461 defer a.activeRequests.Del(sessionID)
462 defer cancel()
462 defer cancel()
463
463
464− agent := ai.NewAgent(a.largeModel.Model,
464+ agent := fantasy.NewAgent(a.largeModel.Model,
465− ai.WithSystemPrompt(string(summaryPrompt)),
465+ fantasy.WithSystemPrompt(string(summaryPrompt)),
466 )
466 )
467 summaryMessage, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
467 summaryMessage, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
468 Role: message.Assistant,
468 Role: message.Assistant,
@@ -474,14 +474,14 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
474 return err
474 return err
475 }
475 }
476
476
477− resp, err := agent.Stream(genCtx, ai.AgentStreamCall{
477+ resp, err := agent.Stream(genCtx, fantasy.AgentStreamCall{
478 Prompt: "Provide a detailed summary of our conversation above.",
478 Prompt: "Provide a detailed summary of our conversation above.",
479 Messages: aiMsgs,
479 Messages: aiMsgs,
480 OnReasoningDelta: func(id string, text string) error {
480 OnReasoningDelta: func(id string, text string) error {
481 summaryMessage.AppendReasoningContent(text)
481 summaryMessage.AppendReasoningContent(text)
482 return a.messages.Update(genCtx, summaryMessage)
482 return a.messages.Update(genCtx, summaryMessage)
483 },
483 },
484− OnReasoningEnd: func(id string, reasoning ai.ReasoningContent) error {
484+ OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
485 // handle anthropic signature
485 // handle anthropic signature
486 if anthropicData, ok := reasoning.ProviderMetadata["anthropic"]; ok {
486 if anthropicData, ok := reasoning.ProviderMetadata["anthropic"]; ok {
487 if signature, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok && signature.Signature != "" {
487 if signature, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok && signature.Signature != "" {
@@ -523,8 +523,8 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string) error {
523 return err
523 return err
524}
524}
525
525
526−func (a *sessionAgent) getCacheControlOptions() ai.ProviderOptions {
526+func (a *sessionAgent) getCacheControlOptions() fantasy.ProviderOptions {
527− return ai.ProviderOptions{
527+ return fantasy.ProviderOptions{
528 anthropic.Name: &anthropic.ProviderCacheControlOptions{
528 anthropic.Name: &anthropic.ProviderCacheControlOptions{
529 CacheControl: anthropic.CacheControl{Type: "ephemeral"},
529 CacheControl: anthropic.CacheControl{Type: "ephemeral"},
530 },
530 },
@@ -548,8 +548,8 @@ func (a *sessionAgent) createUserMessage(ctx context.Context, call SessionAgentC
548 return msg, nil
548 return msg, nil
549}
549}
550
550
551−func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...message.Attachment) ([]ai.Message, []ai.FilePart) {
551+func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...message.Attachment) ([]fantasy.Message, []fantasy.FilePart) {
552− var history []ai.Message
552+ var history []fantasy.Message
553 for _, m := range msgs {
553 for _, m := range msgs {
554 if len(m.Parts) == 0 {
554 if len(m.Parts) == 0 {
555 continue
555 continue
@@ -561,9 +561,9 @@ func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...mess
561 history = append(history, m.ToAIMessage()...)
561 history = append(history, m.ToAIMessage()...)
562 }
562 }
563
563
564− var files []ai.FilePart
564+ var files []fantasy.FilePart
565 for _, attachment := range attachments {
565 for _, attachment := range attachments {
566− files = append(files, ai.FilePart{
566+ files = append(files, fantasy.FilePart{
567 Filename: attachment.FileName,
567 Filename: attachment.FileName,
568 Data: attachment.Content,
568 Data: attachment.Content,
569 MediaType: attachment.MimeType,
569 MediaType: attachment.MimeType,
@@ -605,12 +605,12 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
605 maxOutput = a.smallModel.CatwalkCfg.DefaultMaxTokens
605 maxOutput = a.smallModel.CatwalkCfg.DefaultMaxTokens
606 }
606 }
607
607
608− agent := ai.NewAgent(a.smallModel.Model,
608+ agent := fantasy.NewAgent(a.smallModel.Model,
609− ai.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
609+ fantasy.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
610− ai.WithMaxOutputTokens(maxOutput),
610+ fantasy.WithMaxOutputTokens(maxOutput),
611 )
611 )
612
612
613− resp, err := agent.Stream(ctx, ai.AgentStreamCall{
613+ resp, err := agent.Stream(ctx, fantasy.AgentStreamCall{
614 Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", prompt),
614 Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", prompt),
615 })
615 })
616 if err != nil {
616 if err != nil {
@@ -642,7 +642,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
642 }
642 }
643}
643}
644
644
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) {
646 modelConfig := model.CatwalkCfg
646 modelConfig := model.CatwalkCfg
647 cost := modelConfig.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
647 cost := modelConfig.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
648 modelConfig.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
648 modelConfig.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
@@ -727,7 +727,7 @@ func (a *sessionAgent) SetModels(large Model, small Model) {
727 a.smallModel = small
727 a.smallModel = small
728}
728}
729
729
730−func (a *sessionAgent) SetTools(tools []ai.AgentTool) {
730+func (a *sessionAgent) SetTools(tools []fantasy.AgentTool) {
731 a.tools = tools
731 a.tools = tools
732}
732}
733
733
internal/agent/agent_test.go+2−2
@@ -6,10 +6,10 @@ import (
6 "strings"
6 "strings"
7 "testing"
7 "testing"
8
8
9+ "charm.land/fantasy"
9 "github.com/charmbracelet/crush/internal/agent/tools"
10 "github.com/charmbracelet/crush/internal/agent/tools"
10 "github.com/charmbracelet/crush/internal/message"
11 "github.com/charmbracelet/crush/internal/message"
11 "github.com/charmbracelet/crush/internal/shell"
12 "github.com/charmbracelet/crush/internal/shell"
12− "github.com/charmbracelet/fantasy/ai"
13 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
14 "github.com/stretchr/testify/require"
15 "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
15 "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
@@ -24,7 +24,7 @@ var modelPairs = []modelPair{
24 {"zai-glm4.6", zAIBuilder("glm-4.6"), zAIBuilder("glm-4.5-air")},
24 {"zai-glm4.6", zAIBuilder("glm-4.6"), zAIBuilder("glm-4.5-air")},
25}
25}
26
26
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) {
28 large, err := pair.largeModel(t, r)
28 large, err := pair.largeModel(t, r)
29 require.NoError(t, err)
29 require.NoError(t, err)
30 small, err := pair.smallModel(t, r)
30 small, err := pair.smallModel(t, r)
internal/agent/agent_tool.go+15−15
@@ -7,7 +7,7 @@ import (
7 "errors"
7 "errors"
8 "fmt"
8 "fmt"
9
9
10− "github.com/charmbracelet/fantasy/ai"
10+ "charm.land/fantasy"
11
11
12 "github.com/charmbracelet/crush/internal/agent/prompt"
12 "github.com/charmbracelet/crush/internal/agent/prompt"
13 "github.com/charmbracelet/crush/internal/agent/tools"
13 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -25,7 +25,7 @@ const (
25 AgentToolName = "agent"
25 AgentToolName = "agent"
26)
26)
27
27
28−func (c *coordinator) agentTool() (ai.AgentTool, error) {
28+func (c *coordinator) agentTool() (fantasy.AgentTool, error) {
29 agentCfg, ok := c.cfg.Agents[config.AgentTask]
29 agentCfg, ok := c.cfg.Agents[config.AgentTask]
30 if !ok {
30 if !ok {
31 return nil, errors.New("task agent not configured")
31 return nil, errors.New("task agent not configured")
@@ -39,31 +39,31 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
39 if err != nil {
39 if err != nil {
40 return nil, err
40 return nil, err
41 }
41 }
42− return ai.NewAgentTool(
42+ return fantasy.NewAgentTool(
43 AgentToolName,
43 AgentToolName,
44 string(agentToolDescription),
44 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) {
46 if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
46 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
48 }
48 }
49 if params.Prompt == "" {
49 if params.Prompt == "" {
50− return ai.NewTextErrorResponse("prompt is required"), nil
50+ return fantasy.NewTextErrorResponse("prompt is required"), nil
51 }
51 }
52
52
53 sessionID := tools.GetSessionFromContext(ctx)
53 sessionID := tools.GetSessionFromContext(ctx)
54 if sessionID == "" {
54 if sessionID == "" {
55− return ai.ToolResponse{}, errors.New("session id missing from context")
55+ return fantasy.ToolResponse{}, errors.New("session id missing from context")
56 }
56 }
57
57
58 agentMessageID := tools.GetMessageFromContext(ctx)
58 agentMessageID := tools.GetMessageFromContext(ctx)
59 if agentMessageID == "" {
59 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")
61 }
61 }
62
62
63 agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, call.ID)
63 agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, call.ID)
64 session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, "New Agent Session")
64 session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, "New Agent Session")
65 if err != nil {
65 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)
67 }
67 }
68 model := agent.Model()
68 model := agent.Model()
69 maxTokens := model.CatwalkCfg.DefaultMaxTokens
69 maxTokens := model.CatwalkCfg.DefaultMaxTokens
@@ -73,7 +73,7 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
73
73
74 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
74 providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
75 if !ok {
75 if !ok {
76− return ai.ToolResponse{}, errors.New("model provider not configured")
76+ return fantasy.ToolResponse{}, errors.New("model provider not configured")
77 }
77 }
78 result, err := agent.Run(ctx, SessionAgentCall{
78 result, err := agent.Run(ctx, SessionAgentCall{
79 SessionID: session.ID,
79 SessionID: session.ID,
@@ -87,23 +87,23 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
87 PresencePenalty: model.ModelCfg.PresencePenalty,
87 PresencePenalty: model.ModelCfg.PresencePenalty,
88 })
88 })
89 if err != nil {
89 if err != nil {
90− return ai.NewTextErrorResponse("error generating response"), nil
90+ return fantasy.NewTextErrorResponse("error generating response"), nil
91 }
91 }
92 updatedSession, err := c.sessions.Get(ctx, session.ID)
92 updatedSession, err := c.sessions.Get(ctx, session.ID)
93 if err != nil {
93 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)
95 }
95 }
96 parentSession, err := c.sessions.Get(ctx, sessionID)
96 parentSession, err := c.sessions.Get(ctx, sessionID)
97 if err != nil {
97 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)
99 }
99 }
100
100
101 parentSession.Cost += updatedSession.Cost
101 parentSession.Cost += updatedSession.Cost
102
102
103 _, err = c.sessions.Save(ctx, parentSession)
103 _, err = c.sessions.Save(ctx, parentSession)
104 if err != nil {
104 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)
106 }
106 }
107− return ai.NewTextResponse(result.Response.Content.Text()), nil
107+ return fantasy.NewTextResponse(result.Response.Content.Text()), nil
108 }), nil
108 }), nil
109}
109}
internal/agent/common_test.go+13−13
@@ -7,6 +7,11 @@ import (
7 "testing"
7 "testing"
8 "time"
8 "time"
9
9
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"
10 "github.com/charmbracelet/catwalk/pkg/catwalk"
15 "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/crush/internal/agent/prompt"
16 "github.com/charmbracelet/crush/internal/agent/prompt"
12 "github.com/charmbracelet/crush/internal/agent/tools"
17 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -18,11 +23,6 @@ import (
18 "github.com/charmbracelet/crush/internal/message"
23 "github.com/charmbracelet/crush/internal/message"
19 "github.com/charmbracelet/crush/internal/permission"
24 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/session"
25 "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"
26 "github.com/stretchr/testify/require"
26 "github.com/stretchr/testify/require"
27 "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
27 "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
28
28
@@ -38,7 +38,7 @@ type env struct {
38 lspClients *csync.Map[string, *lsp.Client]
38 lspClients *csync.Map[string, *lsp.Client]
39}
39}
40
40
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)
42
42
43type modelPair struct {
43type modelPair struct {
44 name string
44 name string
@@ -47,7 +47,7 @@ type modelPair struct {
47}
47}
48
48
49func anthropicBuilder(model string) builderFunc {
49func 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) {
51 provider := anthropic.New(
51 provider := anthropic.New(
52 anthropic.WithAPIKey(os.Getenv("CRUSH_ANTHROPIC_API_KEY")),
52 anthropic.WithAPIKey(os.Getenv("CRUSH_ANTHROPIC_API_KEY")),
53 anthropic.WithHTTPClient(&http.Client{Transport: r}),
53 anthropic.WithHTTPClient(&http.Client{Transport: r}),
@@ -57,7 +57,7 @@ func anthropicBuilder(model string) builderFunc {
57}
57}
58
58
59func openaiBuilder(model string) builderFunc {
59func 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) {
61 provider := openai.New(
61 provider := openai.New(
62 openai.WithAPIKey(os.Getenv("CRUSH_OPENAI_API_KEY")),
62 openai.WithAPIKey(os.Getenv("CRUSH_OPENAI_API_KEY")),
63 openai.WithHTTPClient(&http.Client{Transport: r}),
63 openai.WithHTTPClient(&http.Client{Transport: r}),
@@ -67,7 +67,7 @@ func openaiBuilder(model string) builderFunc {
67}
67}
68
68
69func openRouterBuilder(model string) builderFunc {
69func 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) {
71 provider := openrouter.New(
71 provider := openrouter.New(
72 openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")),
72 openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")),
73 openrouter.WithHTTPClient(&http.Client{Transport: r}),
73 openrouter.WithHTTPClient(&http.Client{Transport: r}),
@@ -77,7 +77,7 @@ func openRouterBuilder(model string) builderFunc {
77}
77}
78
78
79func zAIBuilder(model string) builderFunc {
79func 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) {
81 provider := openaicompat.New(
81 provider := openaicompat.New(
82 openaicompat.WithBaseURL("https://api.z.ai/api/coding/paas/v4"),
82 openaicompat.WithBaseURL("https://api.z.ai/api/coding/paas/v4"),
83 openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")),
83 openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")),
@@ -114,7 +114,7 @@ func testEnv(t *testing.T) env {
114 }
114 }
115}
115}
116
116
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 {
118 largeModel := Model{
118 largeModel := Model{
119 Model: large,
119 Model: large,
120 CatwalkCfg: catwalk.Model{
120 CatwalkCfg: catwalk.Model{
@@ -133,7 +133,7 @@ func testSessionAgent(env env, large, small ai.LanguageModel, systemPrompt strin
133 return agent
133 return agent
134}
134}
135
135
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) {
137 fixedTime := func() time.Time {
137 fixedTime := func() time.Time {
138 t, _ := time.Parse("1/2/2006", "1/1/2025")
138 t, _ := time.Parse("1/2/2006", "1/1/2025")
139 return t
139 return t
@@ -155,7 +155,7 @@ func coderAgent(r *recorder.Recorder, env env, large, small ai.LanguageModel) (S
155 if err != nil {
155 if err != nil {
156 return nil, err
156 return nil, err
157 }
157 }
158− allTools := []ai.AgentTool{
158+ allTools := []fantasy.AgentTool{
159 tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution),
159 tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution),
160 tools.NewDownloadTool(env.permissions, env.workingDir, r.GetDefaultClient()),
160 tools.NewDownloadTool(env.permissions, env.workingDir, r.GetDefaultClient()),
161 tools.NewEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
161 tools.NewEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
internal/agent/coordinator.go+25−24
@@ -11,6 +11,7 @@ import (
11 "slices"
11 "slices"
12 "strings"
12 "strings"
13
13
14+ "charm.land/fantasy"
14 "github.com/charmbracelet/catwalk/pkg/catwalk"
15 "github.com/charmbracelet/catwalk/pkg/catwalk"
15 "github.com/charmbracelet/crush/internal/agent/prompt"
16 "github.com/charmbracelet/crush/internal/agent/prompt"
16 "github.com/charmbracelet/crush/internal/agent/tools"
17 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -22,20 +23,20 @@ import (
22 "github.com/charmbracelet/crush/internal/message"
23 "github.com/charmbracelet/crush/internal/message"
23 "github.com/charmbracelet/crush/internal/permission"
24 "github.com/charmbracelet/crush/internal/permission"
24 "github.com/charmbracelet/crush/internal/session"
25 "github.com/charmbracelet/crush/internal/session"
25− "github.com/charmbracelet/fantasy/ai"
26+
26− "github.com/charmbracelet/fantasy/anthropic"
27+ "charm.land/fantasy/providers/anthropic"
27− "github.com/charmbracelet/fantasy/azure"
28+ "charm.land/fantasy/providers/azure"
28− "github.com/charmbracelet/fantasy/google"
29+ "charm.land/fantasy/providers/google"
29− "github.com/charmbracelet/fantasy/openai"
30+ "charm.land/fantasy/providers/openai"
30− "github.com/charmbracelet/fantasy/openaicompat"
31+ "charm.land/fantasy/providers/openaicompat"
31− "github.com/charmbracelet/fantasy/openrouter"
32+ "charm.land/fantasy/providers/openrouter"
32 "github.com/qjebbs/go-jsons"
33 "github.com/qjebbs/go-jsons"
33)
34)
34
35
35type Coordinator interface {
36type Coordinator interface {
36 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
37 // INFO: (kujtim) this is not used yet we will use this when we have multiple agents
37 // SetMainAgent(string)
38 // 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)
39 Cancel(sessionID string)
40 Cancel(sessionID string)
40 CancelAll()
41 CancelAll()
41 IsSessionBusy(sessionID string) bool
42 IsSessionBusy(sessionID string) bool
@@ -98,7 +99,7 @@ func NewCoordinator(
98}
99}
99
100
100// Run implements Coordinator.
101// 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) {
102 model := c.currentAgent.Model()
103 model := c.currentAgent.Model()
103 maxTokens := model.CatwalkCfg.DefaultMaxTokens
104 maxTokens := model.CatwalkCfg.DefaultMaxTokens
104 if model.ModelCfg.MaxTokens != 0 {
105 if model.ModelCfg.MaxTokens != 0 {
@@ -130,8 +131,8 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
130 })
131 })
131}
132}
132
133
133−func getProviderOptions(model Model, tp catwalk.Type) ai.ProviderOptions {
134+func getProviderOptions(model Model, tp catwalk.Type) fantasy.ProviderOptions {
134− options := ai.ProviderOptions{}
135+ options := fantasy.ProviderOptions{}
135
136
136 cfgOpts := []byte("{}")
137 cfgOpts := []byte("{}")
137 catwalkOpts := []byte("{}")
138 catwalkOpts := []byte("{}")
@@ -233,7 +234,7 @@ func getProviderOptions(model Model, tp catwalk.Type) ai.ProviderOptions {
233 return options
234 return options
234}
235}
235
236
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) {
237 modelOptions := getProviderOptions(model, tp)
238 modelOptions := getProviderOptions(model, tp)
238 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
239 temp := cmp.Or(model.ModelCfg.Temperature, model.CatwalkCfg.Options.Temperature)
239 topP := cmp.Or(model.ModelCfg.TopP, model.CatwalkCfg.Options.TopP)
240 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
261 return NewSessionAgent(SessionAgentOptions{large, small, systemPrompt, c.cfg.Options.DisableAutoSummarize, c.sessions, c.messages, tools}), nil
262 return NewSessionAgent(SessionAgentOptions{large, small, systemPrompt, c.cfg.Options.DisableAutoSummarize, c.sessions, c.messages, tools}), nil
262}
263}
263
264
264−func (c *coordinator) buildTools(agent config.Agent) ([]ai.AgentTool, error) {
265+func (c *coordinator) buildTools(agent config.Agent) ([]fantasy.AgentTool, error) {
265− var allTools []ai.AgentTool
266+ var allTools []fantasy.AgentTool
266 if slices.Contains(agent.AllowedTools, AgentToolName) {
267 if slices.Contains(agent.AllowedTools, AgentToolName) {
267 agentTool, err := c.agentTool()
268 agentTool, err := c.agentTool()
268 if err != nil {
269 if err != nil {
@@ -285,7 +286,7 @@ func (c *coordinator) buildTools(agent config.Agent) ([]ai.AgentTool, error) {
285 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
286 tools.NewWriteTool(c.lspClients, c.permissions, c.history, c.cfg.WorkingDir()),
286 )
287 )
287
288
288− var filteredTools []ai.AgentTool
289+ var filteredTools []fantasy.AgentTool
289 for _, tool := range allTools {
290 for _, tool := range allTools {
290 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
291 if slices.Contains(agent.AllowedTools, tool.Info().Name) {
291 filteredTools = append(filteredTools, tool)
292 filteredTools = append(filteredTools, tool)
@@ -394,7 +395,7 @@ func (c *coordinator) buildAgentModels() (Model, Model, error) {
394 }, nil
395 }, nil
395}
396}
396
397
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 {
398 hasBearerAuth := false
399 hasBearerAuth := false
399 for key := range headers {
400 for key := range headers {
400 if strings.ToLower(key) == "authorization" {
401 if strings.ToLower(key) == "authorization" {
@@ -429,7 +430,7 @@ func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map
429 return anthropic.New(opts...)
430 return anthropic.New(opts...)
430}
431}
431
432
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 {
433 opts := []openai.Option{
434 opts := []openai.Option{
434 openai.WithAPIKey(apiKey),
435 openai.WithAPIKey(apiKey),
435 }
436 }
@@ -446,7 +447,7 @@ func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[st
446 return openai.New(opts...)
447 return openai.New(opts...)
447}
448}
448
449
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 {
450 opts := []openrouter.Option{
451 opts := []openrouter.Option{
451 openrouter.WithAPIKey(apiKey),
452 openrouter.WithAPIKey(apiKey),
452 }
453 }
@@ -460,7 +461,7 @@ func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[stri
460 return openrouter.New(opts...)
461 return openrouter.New(opts...)
461}
462}
462
463
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 {
464 opts := []openaicompat.Option{
465 opts := []openaicompat.Option{
465 openaicompat.WithBaseURL(baseURL),
466 openaicompat.WithBaseURL(baseURL),
466 openaicompat.WithAPIKey(apiKey),
467 openaicompat.WithAPIKey(apiKey),
@@ -476,7 +477,7 @@ func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers
476 return openaicompat.New(opts...)
477 return openaicompat.New(opts...)
477}
478}
478
479
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 {
480 opts := []azure.Option{
481 opts := []azure.Option{
481 azure.WithBaseURL(baseURL),
482 azure.WithBaseURL(baseURL),
482 azure.WithAPIKey(apiKey),
483 azure.WithAPIKey(apiKey),
@@ -498,7 +499,7 @@ func (c *coordinator) buildAzureProvider(baseURL, apiKey string, headers map[str
498 return azure.New(opts...)
499 return azure.New(opts...)
499}
500}
500
501
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 {
502 opts := []google.Option{
503 opts := []google.Option{
503 google.WithBaseURL(baseURL),
504 google.WithBaseURL(baseURL),
504 google.WithGeminiAPIKey(apiKey),
505 google.WithGeminiAPIKey(apiKey),
@@ -513,7 +514,7 @@ func (c *coordinator) buildGoogleProvider(baseURL, apiKey string, headers map[st
513 return google.New(opts...)
514 return google.New(opts...)
514}
515}
515
516
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 {
517 opts := []google.Option{}
518 opts := []google.Option{}
518 if c.cfg.Options.Debug {
519 if c.cfg.Options.Debug {
519 httpClient := log.NewHTTPClient()
520 httpClient := log.NewHTTPClient()
@@ -550,7 +551,7 @@ func (c *coordinator) isAnthropicThinking(model config.SelectedModel) bool {
550 return false
551 return false
551}
552}
552
553
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) {
554 headers := providerCfg.ExtraHeaders
555 headers := providerCfg.ExtraHeaders
555
556
556 // handle special headers for anthropic
557 // handle special headers for anthropic
@@ -561,7 +562,7 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
561 // TODO: make sure we have
562 // TODO: make sure we have
562 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
563 apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
563 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
564 baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
564− var provider ai.Provider
565+ var provider fantasy.Provider
565 switch providerCfg.Type {
566 switch providerCfg.Type {
566 case openai.Name:
567 case openai.Name:
567 provider = c.buildOpenaiProvider(baseURL, apiKey, headers)
568 provider = c.buildOpenaiProvider(baseURL, apiKey, headers)
internal/agent/tools/bash.go+10−10
@@ -9,10 +9,10 @@ import (
9 "strings"
9 "strings"
10 "time"
10 "time"
11
11
12+ "charm.land/fantasy"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/permission"
14 "github.com/charmbracelet/crush/internal/permission"
14 "github.com/charmbracelet/crush/internal/shell"
15 "github.com/charmbracelet/crush/internal/shell"
15− "github.com/charmbracelet/fantasy/ai"
16)
16)
17
17
18type BashParams struct {
18type BashParams struct {
@@ -177,14 +177,14 @@ func blockFuncs() []shell.BlockFunc {
177 }
177 }
178}
178}
179
179
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 {
181 // Set up command blocking on the persistent shell
181 // Set up command blocking on the persistent shell
182 persistentShell := shell.GetPersistentShell(workingDir)
182 persistentShell := shell.GetPersistentShell(workingDir)
183 persistentShell.SetBlockFuncs(blockFuncs())
183 persistentShell.SetBlockFuncs(blockFuncs())
184− return ai.NewAgentTool(
184+ return fantasy.NewAgentTool(
185 BashToolName,
185 BashToolName,
186 string(bashDescription(attribution)),
186 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) {
188 if params.Timeout > MaxTimeout {
188 if params.Timeout > MaxTimeout {
189 params.Timeout = MaxTimeout
189 params.Timeout = MaxTimeout
190 } else if params.Timeout <= 0 {
190 } else if params.Timeout <= 0 {
@@ -192,7 +192,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
192 }
192 }
193
193
194 if params.Command == "" {
194 if params.Command == "" {
195− return ai.NewTextErrorResponse("missing command"), nil
195+ return fantasy.NewTextErrorResponse("missing command"), nil
196 }
196 }
197
197
198 isSafeReadOnly := false
198 isSafeReadOnly := false
@@ -209,7 +209,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
209
209
210 sessionID := GetSessionFromContext(ctx)
210 sessionID := GetSessionFromContext(ctx)
211 if sessionID == "" {
211 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")
213 }
213 }
214 if !isSafeReadOnly {
214 if !isSafeReadOnly {
215 shell := shell.GetPersistentShell(workingDir)
215 shell := shell.GetPersistentShell(workingDir)
@@ -228,7 +228,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
228 },
228 },
229 )
229 )
230 if !p {
230 if !p {
231− return ai.ToolResponse{}, permission.ErrorPermissionDenied
231+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
232 }
232 }
233 }
233 }
234 startTime := time.Now()
234 startTime := time.Now()
@@ -246,7 +246,7 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
246 interrupted := shell.IsInterrupt(err)
246 interrupted := shell.IsInterrupt(err)
247 exitCode := shell.ExitCode(err)
247 exitCode := shell.ExitCode(err)
248 if exitCode == 0 && !interrupted && err != nil {
248 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)
250 }
250 }
251
251
252 stdout = truncateOutput(stdout)
252 stdout = truncateOutput(stdout)
@@ -287,10 +287,10 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution
287 WorkingDirectory: currentWorkingDir,
287 WorkingDirectory: currentWorkingDir,
288 }
288 }
289 if stdout == "" {
289 if stdout == "" {
290− return ai.WithResponseMetadata(ai.NewTextResponse(BashNoOutput), metadata), nil
290+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(BashNoOutput), metadata), nil
291 }
291 }
292 stdout += fmt.Sprintf("\n\n<cwd>%s</cwd>", currentWorkingDir)
292 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
294 })
294 })
295}
295}
296
296
internal/agent/tools/diagnostics.go+6−6
@@ -9,9 +9,9 @@ import (
9 "strings"
9 "strings"
10 "time"
10 "time"
11
11
12+ "charm.land/fantasy"
12 "github.com/charmbracelet/crush/internal/csync"
13 "github.com/charmbracelet/crush/internal/csync"
13 "github.com/charmbracelet/crush/internal/lsp"
14 "github.com/charmbracelet/crush/internal/lsp"
14− "github.com/charmbracelet/fantasy/ai"
15 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
15 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
16)
16)
17
17
@@ -24,17 +24,17 @@ const DiagnosticsToolName = "diagnostics"
24//go:embed diagnostics.md
24//go:embed diagnostics.md
25var diagnosticsDescription []byte
25var diagnosticsDescription []byte
26
26
27−func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) ai.AgentTool {
27+func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) fantasy.AgentTool {
28− return ai.NewAgentTool(
28+ return fantasy.NewAgentTool(
29 DiagnosticsToolName,
29 DiagnosticsToolName,
30 string(diagnosticsDescription),
30 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) {
32 if lspClients.Len() == 0 {
32 if lspClients.Len() == 0 {
33− return ai.NewTextErrorResponse("no LSP clients available"), nil
33+ return fantasy.NewTextErrorResponse("no LSP clients available"), nil
34 }
34 }
35 notifyLSPs(ctx, lspClients, params.FilePath)
35 notifyLSPs(ctx, lspClients, params.FilePath)
36 output := getDiagnostics(params.FilePath, lspClients)
36 output := getDiagnostics(params.FilePath, lspClients)
37− return ai.NewTextResponse(output), nil
37+ return fantasy.NewTextResponse(output), nil
38 })
38 })
39}
39}
40
40
internal/agent/tools/download.go+18−18
@@ -11,8 +11,8 @@ import (
11 "strings"
11 "strings"
12 "time"
12 "time"
13
13
14+ "charm.land/fantasy"
14 "github.com/charmbracelet/crush/internal/permission"
15 "github.com/charmbracelet/crush/internal/permission"
15− "github.com/charmbracelet/fantasy/ai"
16)
16)
17
17
18type DownloadParams struct {
18type DownloadParams struct {
@@ -32,7 +32,7 @@ const DownloadToolName = "download"
32//go:embed download.md
32//go:embed download.md
33var downloadDescription []byte
33var downloadDescription []byte
34
34
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 {
36 if client == nil {
36 if client == nil {
37 client = &http.Client{
37 client = &http.Client{
38 Timeout: 5 * time.Minute, // Default 5 minute timeout for downloads
38 Timeout: 5 * time.Minute, // Default 5 minute timeout for downloads
@@ -43,20 +43,20 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
43 },
43 },
44 }
44 }
45 }
45 }
46− return ai.NewAgentTool(
46+ return fantasy.NewAgentTool(
47 DownloadToolName,
47 DownloadToolName,
48 string(downloadDescription),
48 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) {
50 if params.URL == "" {
50 if params.URL == "" {
51− return ai.NewTextErrorResponse("URL parameter is required"), nil
51+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
52 }
52 }
53
53
54 if params.FilePath == "" {
54 if params.FilePath == "" {
55− return ai.NewTextErrorResponse("file_path parameter is required"), nil
55+ return fantasy.NewTextErrorResponse("file_path parameter is required"), nil
56 }
56 }
57
57
58 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
58 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
60 }
60 }
61
61
62 // Convert relative path to absolute path
62 // Convert relative path to absolute path
@@ -69,7 +69,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
69
69
70 sessionID := GetSessionFromContext(ctx)
70 sessionID := GetSessionFromContext(ctx)
71 if sessionID == "" {
71 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")
73 }
73 }
74
74
75 p := permissions.Request(
75 p := permissions.Request(
@@ -84,7 +84,7 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
84 )
84 )
85
85
86 if !p {
86 if !p {
87− return ai.ToolResponse{}, permission.ErrorPermissionDenied
87+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
88 }
88 }
89
89
90 // Handle timeout with context
90 // Handle timeout with context
@@ -101,36 +101,36 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
101
101
102 req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
102 req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
103 if err != nil {
103 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)
105 }
105 }
106
106
107 req.Header.Set("User-Agent", "crush/1.0")
107 req.Header.Set("User-Agent", "crush/1.0")
108
108
109 resp, err := client.Do(req)
109 resp, err := client.Do(req)
110 if err != nil {
110 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)
112 }
112 }
113 defer resp.Body.Close()
113 defer resp.Body.Close()
114
114
115 if resp.StatusCode != http.StatusOK {
115 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
117 }
117 }
118
118
119 // Check content length if available
119 // Check content length if available
120 maxSize := int64(100 * 1024 * 1024) // 100MB
120 maxSize := int64(100 * 1024 * 1024) // 100MB
121 if resp.ContentLength > maxSize {
121 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
123 }
123 }
124
124
125 // Create parent directories if they don't exist
125 // Create parent directories if they don't exist
126 if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
126 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)
128 }
128 }
129
129
130 // Create the output file
130 // Create the output file
131 outFile, err := os.Create(filePath)
131 outFile, err := os.Create(filePath)
132 if err != nil {
132 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)
134 }
134 }
135 defer outFile.Close()
135 defer outFile.Close()
136
136
@@ -138,14 +138,14 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
138 limitedReader := io.LimitReader(resp.Body, maxSize)
138 limitedReader := io.LimitReader(resp.Body, maxSize)
139 bytesWritten, err := io.Copy(outFile, limitedReader)
139 bytesWritten, err := io.Copy(outFile, limitedReader)
140 if err != nil {
140 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)
142 }
142 }
143
143
144 // Check if we hit the size limit
144 // Check if we hit the size limit
145 if bytesWritten == maxSize {
145 if bytesWritten == maxSize {
146 // Clean up the file since it might be incomplete
146 // Clean up the file since it might be incomplete
147 os.Remove(filePath)
147 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
149 }
149 }
150
150
151 contentType := resp.Header.Get("Content-Type")
151 contentType := resp.Header.Get("Content-Type")
@@ -154,6 +154,6 @@ func NewDownloadTool(permissions permission.Service, workingDir string, client *
154 responseMsg += fmt.Sprintf(" (Content-Type: %s)", contentType)
154 responseMsg += fmt.Sprintf(" (Content-Type: %s)", contentType)
155 }
155 }
156
156
157− return ai.NewTextResponse(responseMsg), nil
157+ return fantasy.NewTextResponse(responseMsg), nil
158 })
158 })
159}
159}
internal/agent/tools/edit.go+50−50
@@ -10,11 +10,11 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "charm.land/fantasy"
13 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/history"
17− "github.com/charmbracelet/fantasy/ai"
18
18
19 "github.com/charmbracelet/crush/internal/lsp"
19 "github.com/charmbracelet/crush/internal/lsp"
20 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/permission"
@@ -52,20 +52,20 @@ type editContext struct {
52 workingDir string
52 workingDir string
53}
53}
54
54
55−func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
55+func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
56− return ai.NewAgentTool(
56+ return fantasy.NewAgentTool(
57 EditToolName,
57 EditToolName,
58 string(editDescription),
58 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) {
60 if params.FilePath == "" {
60 if params.FilePath == "" {
61− return ai.NewTextErrorResponse("file_path is required"), nil
61+ return fantasy.NewTextErrorResponse("file_path is required"), nil
62 }
62 }
63
63
64 if !filepath.IsAbs(params.FilePath) {
64 if !filepath.IsAbs(params.FilePath) {
65 params.FilePath = filepath.Join(workingDir, params.FilePath)
65 params.FilePath = filepath.Join(workingDir, params.FilePath)
66 }
66 }
67
67
68− var response ai.ToolResponse
68+ var response fantasy.ToolResponse
69 var err error
69 var err error
70
70
71 editCtx := editContext{ctx, permissions, files, workingDir}
71 editCtx := editContext{ctx, permissions, files, workingDir}
@@ -103,25 +103,25 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
103 })
103 })
104}
104}
105
105
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) {
107 fileInfo, err := os.Stat(filePath)
107 fileInfo, err := os.Stat(filePath)
108 if err == nil {
108 if err == nil {
109 if fileInfo.IsDir() {
109 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
111 }
111 }
112− return ai.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
112+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
113 } else if !os.IsNotExist(err) {
113 } 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)
115 }
115 }
116
116
117 dir := filepath.Dir(filePath)
117 dir := filepath.Dir(filePath)
118 if err = os.MkdirAll(dir, 0o755); err != nil {
118 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)
120 }
120 }
121
121
122 sessionID := GetSessionFromContext(edit.ctx)
122 sessionID := GetSessionFromContext(edit.ctx)
123 if sessionID == "" {
123 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")
125 }
125 }
126
126
127 _, additions, removals := diff.GenerateDiff(
127 _, additions, removals := diff.GenerateDiff(
@@ -145,19 +145,19 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
145 },
145 },
146 )
146 )
147 if !p {
147 if !p {
148− return ai.ToolResponse{}, permission.ErrorPermissionDenied
148+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
149 }
149 }
150
150
151 err = os.WriteFile(filePath, []byte(content), 0o644)
151 err = os.WriteFile(filePath, []byte(content), 0o644)
152 if err != nil {
152 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)
154 }
154 }
155
155
156 // File can't be in the history so we create a new file history
156 // File can't be in the history so we create a new file history
157 _, err = edit.files.Create(edit.ctx, sessionID, filePath, "")
157 _, err = edit.files.Create(edit.ctx, sessionID, filePath, "")
158 if err != nil {
158 if err != nil {
159 // Log error but don't fail the operation
159 // 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)
161 }
161 }
162
162
163 // Add the new content to the file history
163 // Add the new content to the file history
@@ -170,8 +170,8 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
170 recordFileWrite(filePath)
170 recordFileWrite(filePath)
171 recordFileRead(filePath)
171 recordFileRead(filePath)
172
172
173− return ai.WithResponseMetadata(
173+ return fantasy.WithResponseMetadata(
174− ai.NewTextResponse("File created: "+filePath),
174+ fantasy.NewTextResponse("File created: "+filePath),
175 EditResponseMetadata{
175 EditResponseMetadata{
176 OldContent: "",
176 OldContent: "",
177 NewContent: content,
177 NewContent: content,
@@ -181,27 +181,27 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
181 ), nil
181 ), nil
182}
182}
183
183
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) {
185 fileInfo, err := os.Stat(filePath)
185 fileInfo, err := os.Stat(filePath)
186 if err != nil {
186 if err != nil {
187 if os.IsNotExist(err) {
187 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
189 }
189 }
190− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
190+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
191 }
191 }
192
192
193 if fileInfo.IsDir() {
193 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
195 }
195 }
196
196
197 if getLastReadTime(filePath).IsZero() {
197 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
199 }
199 }
200
200
201 modTime := fileInfo.ModTime()
201 modTime := fileInfo.ModTime()
202 lastRead := getLastReadTime(filePath)
202 lastRead := getLastReadTime(filePath)
203 if modTime.After(lastRead) {
203 if modTime.After(lastRead) {
204− return ai.NewTextErrorResponse(
204+ return fantasy.NewTextErrorResponse(
205 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
205 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
206 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
206 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
207 )), nil
207 )), nil
@@ -209,7 +209,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
209
209
210 content, err := os.ReadFile(filePath)
210 content, err := os.ReadFile(filePath)
211 if err != nil {
211 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)
213 }
213 }
214
214
215 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
215 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -221,17 +221,17 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
221 newContent = strings.ReplaceAll(oldContent, oldString, "")
221 newContent = strings.ReplaceAll(oldContent, oldString, "")
222 deletionCount = strings.Count(oldContent, oldString)
222 deletionCount = strings.Count(oldContent, oldString)
223 if deletionCount == 0 {
223 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
225 }
225 }
226 } else {
226 } else {
227 index := strings.Index(oldContent, oldString)
227 index := strings.Index(oldContent, oldString)
228 if index == -1 {
228 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
230 }
230 }
231
231
232 lastIndex := strings.LastIndex(oldContent, oldString)
232 lastIndex := strings.LastIndex(oldContent, oldString)
233 if index != lastIndex {
233 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
235 }
235 }
236
236
237 newContent = oldContent[:index] + oldContent[index+len(oldString):]
237 newContent = oldContent[:index] + oldContent[index+len(oldString):]
@@ -241,7 +241,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
241 sessionID := GetSessionFromContext(edit.ctx)
241 sessionID := GetSessionFromContext(edit.ctx)
242
242
243 if sessionID == "" {
243 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")
245 }
245 }
246
246
247 _, additions, removals := diff.GenerateDiff(
247 _, additions, removals := diff.GenerateDiff(
@@ -266,7 +266,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
266 },
266 },
267 )
267 )
268 if !p {
268 if !p {
269− return ai.ToolResponse{}, permission.ErrorPermissionDenied
269+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
270 }
270 }
271
271
272 if isCrlf {
272 if isCrlf {
@@ -275,7 +275,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
275
275
276 err = os.WriteFile(filePath, []byte(newContent), 0o644)
276 err = os.WriteFile(filePath, []byte(newContent), 0o644)
277 if err != nil {
277 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)
279 }
279 }
280
280
281 // Check if file exists in history
281 // Check if file exists in history
@@ -284,7 +284,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
284 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
284 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
285 if err != nil {
285 if err != nil {
286 // Log error but don't fail the operation
286 // 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)
288 }
288 }
289 }
289 }
290 if file.Content != oldContent {
290 if file.Content != oldContent {
@@ -303,8 +303,8 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
303 recordFileWrite(filePath)
303 recordFileWrite(filePath)
304 recordFileRead(filePath)
304 recordFileRead(filePath)
305
305
306− return ai.WithResponseMetadata(
306+ return fantasy.WithResponseMetadata(
307− ai.NewTextResponse("Content deleted from file: "+filePath),
307+ fantasy.NewTextResponse("Content deleted from file: "+filePath),
308 EditResponseMetadata{
308 EditResponseMetadata{
309 OldContent: oldContent,
309 OldContent: oldContent,
310 NewContent: newContent,
310 NewContent: newContent,
@@ -314,27 +314,27 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
314 ), nil
314 ), nil
315}
315}
316
316
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) {
318 fileInfo, err := os.Stat(filePath)
318 fileInfo, err := os.Stat(filePath)
319 if err != nil {
319 if err != nil {
320 if os.IsNotExist(err) {
320 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
322 }
322 }
323− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
323+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
324 }
324 }
325
325
326 if fileInfo.IsDir() {
326 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
328 }
328 }
329
329
330 if getLastReadTime(filePath).IsZero() {
330 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
332 }
332 }
333
333
334 modTime := fileInfo.ModTime()
334 modTime := fileInfo.ModTime()
335 lastRead := getLastReadTime(filePath)
335 lastRead := getLastReadTime(filePath)
336 if modTime.After(lastRead) {
336 if modTime.After(lastRead) {
337− return ai.NewTextErrorResponse(
337+ return fantasy.NewTextErrorResponse(
338 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
338 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
339 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
339 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
340 )), nil
340 )), nil
@@ -342,7 +342,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
342
342
343 content, err := os.ReadFile(filePath)
343 content, err := os.ReadFile(filePath)
344 if err != nil {
344 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)
346 }
346 }
347
347
348 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
348 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -354,17 +354,17 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
354 newContent = strings.ReplaceAll(oldContent, oldString, newString)
354 newContent = strings.ReplaceAll(oldContent, oldString, newString)
355 replacementCount = strings.Count(oldContent, oldString)
355 replacementCount = strings.Count(oldContent, oldString)
356 if replacementCount == 0 {
356 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
358 }
358 }
359 } else {
359 } else {
360 index := strings.Index(oldContent, oldString)
360 index := strings.Index(oldContent, oldString)
361 if index == -1 {
361 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
363 }
363 }
364
364
365 lastIndex := strings.LastIndex(oldContent, oldString)
365 lastIndex := strings.LastIndex(oldContent, oldString)
366 if index != lastIndex {
366 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
368 }
368 }
369
369
370 newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
370 newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
@@ -372,12 +372,12 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
372 }
372 }
373
373
374 if oldContent == newContent {
374 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
376 }
376 }
377 sessionID := GetSessionFromContext(edit.ctx)
377 sessionID := GetSessionFromContext(edit.ctx)
378
378
379 if sessionID == "" {
379 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")
381 }
381 }
382 _, additions, removals := diff.GenerateDiff(
382 _, additions, removals := diff.GenerateDiff(
383 oldContent,
383 oldContent,
@@ -401,7 +401,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
401 },
401 },
402 )
402 )
403 if !p {
403 if !p {
404− return ai.ToolResponse{}, permission.ErrorPermissionDenied
404+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
405 }
405 }
406
406
407 if isCrlf {
407 if isCrlf {
@@ -410,7 +410,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
410
410
411 err = os.WriteFile(filePath, []byte(newContent), 0o644)
411 err = os.WriteFile(filePath, []byte(newContent), 0o644)
412 if err != nil {
412 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)
414 }
414 }
415
415
416 // Check if file exists in history
416 // Check if file exists in history
@@ -419,7 +419,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
419 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
419 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
420 if err != nil {
420 if err != nil {
421 // Log error but don't fail the operation
421 // 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)
423 }
423 }
424 }
424 }
425 if file.Content != oldContent {
425 if file.Content != oldContent {
@@ -438,8 +438,8 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
438 recordFileWrite(filePath)
438 recordFileWrite(filePath)
439 recordFileRead(filePath)
439 recordFileRead(filePath)
440
440
441− return ai.WithResponseMetadata(
441+ return fantasy.WithResponseMetadata(
442− ai.NewTextResponse("Content replaced in file: "+filePath),
442+ fantasy.NewTextResponse("Content replaced in file: "+filePath),
443 EditResponseMetadata{
443 EditResponseMetadata{
444 OldContent: oldContent,
444 OldContent: oldContent,
445 NewContent: newContent,
445 NewContent: newContent,
internal/agent/tools/fetch.go+20−20
@@ -10,10 +10,10 @@ import (
10 "time"
10 "time"
11 "unicode/utf8"
11 "unicode/utf8"
12
12
13+ "charm.land/fantasy"
13 md "github.com/JohannesKaufmann/html-to-markdown"
14 md "github.com/JohannesKaufmann/html-to-markdown"
14 "github.com/PuerkitoBio/goquery"
15 "github.com/PuerkitoBio/goquery"
15 "github.com/charmbracelet/crush/internal/permission"
16 "github.com/charmbracelet/crush/internal/permission"
16− "github.com/charmbracelet/fantasy/ai"
17)
17)
18
18
19type FetchParams struct {
19type FetchParams struct {
@@ -39,7 +39,7 @@ const FetchToolName = "fetch"
39//go:embed fetch.md
39//go:embed fetch.md
40var fetchDescription []byte
40var fetchDescription []byte
41
41
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 {
43 if client == nil {
43 if client == nil {
44 client = &http.Client{
44 client = &http.Client{
45 Timeout: 30 * time.Second,
45 Timeout: 30 * time.Second,
@@ -51,26 +51,26 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
51 }
51 }
52 }
52 }
53
53
54− return ai.NewAgentTool(
54+ return fantasy.NewAgentTool(
55 FetchToolName,
55 FetchToolName,
56 string(fetchDescription),
56 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) {
58 if params.URL == "" {
58 if params.URL == "" {
59− return ai.NewTextErrorResponse("URL parameter is required"), nil
59+ return fantasy.NewTextErrorResponse("URL parameter is required"), nil
60 }
60 }
61
61
62 format := strings.ToLower(params.Format)
62 format := strings.ToLower(params.Format)
63 if format != "text" && format != "markdown" && format != "html" {
63 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
65 }
65 }
66
66
67 if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") {
67 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
69 }
69 }
70
70
71 sessionID := GetSessionFromContext(ctx)
71 sessionID := GetSessionFromContext(ctx)
72 if sessionID == "" {
72 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")
74 }
74 }
75
75
76 p := permissions.Request(
76 p := permissions.Request(
@@ -86,7 +86,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
86 )
86 )
87
87
88 if !p {
88 if !p {
89− return ai.ToolResponse{}, permission.ErrorPermissionDenied
89+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
90 }
90 }
91
91
92 // Handle timeout with context
92 // Handle timeout with context
@@ -103,32 +103,32 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
103
103
104 req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
104 req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
105 if err != nil {
105 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)
107 }
107 }
108
108
109 req.Header.Set("User-Agent", "crush/1.0")
109 req.Header.Set("User-Agent", "crush/1.0")
110
110
111 resp, err := client.Do(req)
111 resp, err := client.Do(req)
112 if err != nil {
112 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)
114 }
114 }
115 defer resp.Body.Close()
115 defer resp.Body.Close()
116
116
117 if resp.StatusCode != http.StatusOK {
117 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
119 }
119 }
120
120
121 maxSize := int64(5 * 1024 * 1024) // 5MB
121 maxSize := int64(5 * 1024 * 1024) // 5MB
122 body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
122 body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
123 if err != nil {
123 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
125 }
125 }
126
126
127 content := string(body)
127 content := string(body)
128
128
129 isValidUt8 := utf8.ValidString(content)
129 isValidUt8 := utf8.ValidString(content)
130 if !isValidUt8 {
130 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
132 }
132 }
133 contentType := resp.Header.Get("Content-Type")
133 contentType := resp.Header.Get("Content-Type")
134
134
@@ -137,7 +137,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
137 if strings.Contains(contentType, "text/html") {
137 if strings.Contains(contentType, "text/html") {
138 text, err := extractTextFromHTML(content)
138 text, err := extractTextFromHTML(content)
139 if err != nil {
139 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
141 }
141 }
142 content = text
142 content = text
143 }
143 }
@@ -146,7 +146,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
146 if strings.Contains(contentType, "text/html") {
146 if strings.Contains(contentType, "text/html") {
147 markdown, err := convertHTMLToMarkdown(content)
147 markdown, err := convertHTMLToMarkdown(content)
148 if err != nil {
148 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
150 }
150 }
151 content = markdown
151 content = markdown
152 }
152 }
@@ -158,14 +158,14 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
158 if strings.Contains(contentType, "text/html") {
158 if strings.Contains(contentType, "text/html") {
159 doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
159 doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
160 if err != nil {
160 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
162 }
162 }
163 body, err := doc.Find("body").Html()
163 body, err := doc.Find("body").Html()
164 if err != nil {
164 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
166 }
166 }
167 if body == "" {
167 if body == "" {
168− return ai.NewTextErrorResponse("No body content found in HTML"), nil
168+ return fantasy.NewTextErrorResponse("No body content found in HTML"), nil
169 }
169 }
170 content = "<html>\n<body>\n" + body + "\n</body>\n</html>"
170 content = "<html>\n<body>\n" + body + "\n</body>\n</html>"
171 }
171 }
@@ -177,7 +177,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
177 content += fmt.Sprintf("\n\n[Content truncated to %d bytes]", MaxReadSize)
177 content += fmt.Sprintf("\n\n[Content truncated to %d bytes]", MaxReadSize)
178 }
178 }
179
179
180− return ai.NewTextResponse(content), nil
180+ return fantasy.NewTextResponse(content), nil
181 })
181 })
182}
182}
183
183
internal/agent/tools/glob.go+8−8
@@ -11,8 +11,8 @@ import (
11 "sort"
11 "sort"
12 "strings"
12 "strings"
13
13
14+ "charm.land/fantasy"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/fantasy/ai"
16)
16)
17
17
18const GlobToolName = "glob"
18const GlobToolName = "glob"
@@ -30,13 +30,13 @@ type GlobResponseMetadata struct {
30 Truncated bool `json:"truncated"`
30 Truncated bool `json:"truncated"`
31}
31}
32
32
33−func NewGlobTool(workingDir string) ai.AgentTool {
33+func NewGlobTool(workingDir string) fantasy.AgentTool {
34− return ai.NewAgentTool(
34+ return fantasy.NewAgentTool(
35 GlobToolName,
35 GlobToolName,
36 string(globDescription),
36 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) {
38 if params.Pattern == "" {
38 if params.Pattern == "" {
39− return ai.NewTextErrorResponse("pattern is required"), nil
39+ return fantasy.NewTextErrorResponse("pattern is required"), nil
40 }
40 }
41
41
42 searchPath := params.Path
42 searchPath := params.Path
@@ -46,7 +46,7 @@ func NewGlobTool(workingDir string) ai.AgentTool {
46
46
47 files, truncated, err := globFiles(ctx, params.Pattern, searchPath, 100)
47 files, truncated, err := globFiles(ctx, params.Pattern, searchPath, 100)
48 if err != nil {
48 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)
50 }
50 }
51
51
52 var output string
52 var output string
@@ -59,8 +59,8 @@ func NewGlobTool(workingDir string) ai.AgentTool {
59 }
59 }
60 }
60 }
61
61
62− return ai.WithResponseMetadata(
62+ return fantasy.WithResponseMetadata(
63− ai.NewTextResponse(output),
63+ fantasy.NewTextResponse(output),
64 GlobResponseMetadata{
64 GlobResponseMetadata{
65 NumberOfFiles: len(files),
65 NumberOfFiles: len(files),
66 Truncated: truncated,
66 Truncated: truncated,
internal/agent/tools/grep.go+8−8
@@ -16,8 +16,8 @@ import (
16 "sync"
16 "sync"
17 "time"
17 "time"
18
18
19+ "charm.land/fantasy"
19 "github.com/charmbracelet/crush/internal/fsext"
20 "github.com/charmbracelet/crush/internal/fsext"
20− "github.com/charmbracelet/fantasy/ai"
21)
21)
22
22
23// regexCache provides thread-safe caching of compiled regex patterns
23// regexCache provides thread-safe caching of compiled regex patterns
@@ -97,13 +97,13 @@ const (
97//go:embed grep.md
97//go:embed grep.md
98var grepDescription []byte
98var grepDescription []byte
99
99
100−func NewGrepTool(workingDir string) ai.AgentTool {
100+func NewGrepTool(workingDir string) fantasy.AgentTool {
101− return ai.NewAgentTool(
101+ return fantasy.NewAgentTool(
102 GrepToolName,
102 GrepToolName,
103 string(grepDescription),
103 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) {
105 if params.Pattern == "" {
105 if params.Pattern == "" {
106− return ai.NewTextErrorResponse("pattern is required"), nil
106+ return fantasy.NewTextErrorResponse("pattern is required"), nil
107 }
107 }
108
108
109 // If literal_text is true, escape the pattern
109 // If literal_text is true, escape the pattern
@@ -119,7 +119,7 @@ func NewGrepTool(workingDir string) ai.AgentTool {
119
119
120 matches, truncated, err := searchFiles(ctx, searchPattern, searchPath, params.Include, 100)
120 matches, truncated, err := searchFiles(ctx, searchPattern, searchPath, params.Include, 100)
121 if err != nil {
121 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)
123 }
123 }
124
124
125 var output strings.Builder
125 var output strings.Builder
@@ -153,8 +153,8 @@ func NewGrepTool(workingDir string) ai.AgentTool {
153 }
153 }
154 }
154 }
155
155
156− return ai.WithResponseMetadata(
156+ return fantasy.WithResponseMetadata(
157− ai.NewTextResponse(output.String()),
157+ fantasy.NewTextResponse(output.String()),
158 GrepResponseMetadata{
158 GrepResponseMetadata{
159 NumberOfMatches: len(matches),
159 NumberOfMatches: len(matches),
160 Truncated: truncated,
160 Truncated: truncated,
internal/agent/tools/ls.go+12−12
@@ -9,10 +9,10 @@ import (
9 "path/filepath"
9 "path/filepath"
10 "strings"
10 "strings"
11
11
12+ "charm.land/fantasy"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/fsext"
14 "github.com/charmbracelet/crush/internal/fsext"
14 "github.com/charmbracelet/crush/internal/permission"
15 "github.com/charmbracelet/crush/internal/permission"
15− "github.com/charmbracelet/fantasy/ai"
16)
16)
17
17
18type LSParams struct {
18type LSParams struct {
@@ -47,14 +47,14 @@ const (
47//go:embed ls.md
47//go:embed ls.md
48var lsDescription []byte
48var lsDescription []byte
49
49
50−func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) ai.AgentTool {
50+func NewLsTool(permissions permission.Service, workingDir string, lsConfig config.ToolLs) fantasy.AgentTool {
51− return ai.NewAgentTool(
51+ return fantasy.NewAgentTool(
52 LSToolName,
52 LSToolName,
53 string(lsDescription),
53 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) {
55 searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
55 searchPath, err := fsext.Expand(cmp.Or(params.Path, workingDir))
56 if err != nil {
56 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)
58 }
58 }
59
59
60 if !filepath.IsAbs(searchPath) {
60 if !filepath.IsAbs(searchPath) {
@@ -64,12 +64,12 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
64 // Check if directory is outside working directory and request permission if needed
64 // Check if directory is outside working directory and request permission if needed
65 absWorkingDir, err := filepath.Abs(workingDir)
65 absWorkingDir, err := filepath.Abs(workingDir)
66 if err != nil {
66 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)
68 }
68 }
69
69
70 absSearchPath, err := filepath.Abs(searchPath)
70 absSearchPath, err := filepath.Abs(searchPath)
71 if err != nil {
71 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)
73 }
73 }
74
74
75 relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
75 relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
@@ -77,7 +77,7 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
77 // Directory is outside working directory, request permission
77 // Directory is outside working directory, request permission
78 sessionID := GetSessionFromContext(ctx)
78 sessionID := GetSessionFromContext(ctx)
79 if sessionID == "" {
79 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")
81 }
81 }
82
82
83 granted := permissions.Request(
83 granted := permissions.Request(
@@ -93,17 +93,17 @@ func NewLsTool(permissions permission.Service, workingDir string, lsConfig confi
93 )
93 )
94
94
95 if !granted {
95 if !granted {
96− return ai.ToolResponse{}, permission.ErrorPermissionDenied
96+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
97 }
97 }
98 }
98 }
99
99
100 output, metadata, err := ListDirectoryTree(searchPath, params, lsConfig)
100 output, metadata, err := ListDirectoryTree(searchPath, params, lsConfig)
101 if err != nil {
101 if err != nil {
102− return ai.ToolResponse{}, err
102+ return fantasy.ToolResponse{}, err
103 }
103 }
104
104
105− return ai.WithResponseMetadata(
105+ return fantasy.WithResponseMetadata(
106− ai.NewTextResponse(output),
106+ fantasy.NewTextResponse(output),
107 metadata,
107 metadata,
108 ), nil
108 ), nil
109 })
109 })
internal/agent/tools/mcp-tools.go+14−14
@@ -16,13 +16,13 @@ import (
16 "sync"
16 "sync"
17 "time"
17 "time"
18
18
19+ "charm.land/fantasy"
19 "github.com/charmbracelet/crush/internal/config"
20 "github.com/charmbracelet/crush/internal/config"
20 "github.com/charmbracelet/crush/internal/csync"
21 "github.com/charmbracelet/crush/internal/csync"
21 "github.com/charmbracelet/crush/internal/home"
22 "github.com/charmbracelet/crush/internal/home"
22 "github.com/charmbracelet/crush/internal/permission"
23 "github.com/charmbracelet/crush/internal/permission"
23 "github.com/charmbracelet/crush/internal/pubsub"
24 "github.com/charmbracelet/crush/internal/pubsub"
24 "github.com/charmbracelet/crush/internal/version"
25 "github.com/charmbracelet/crush/internal/version"
25− "github.com/charmbracelet/fantasy/ai"
26 "github.com/modelcontextprotocol/go-sdk/mcp"
26 "github.com/modelcontextprotocol/go-sdk/mcp"
27)
27)
28
28
@@ -92,14 +92,14 @@ type McpTool struct {
92 tool *mcp.Tool
92 tool *mcp.Tool
93 permissions permission.Service
93 permissions permission.Service
94 workingDir string
94 workingDir string
95− providerOptions ai.ProviderOptions
95+ providerOptions fantasy.ProviderOptions
96}
96}
97
97
98−func (m *McpTool) SetProviderOptions(opts ai.ProviderOptions) {
98+func (m *McpTool) SetProviderOptions(opts fantasy.ProviderOptions) {
99 m.providerOptions = opts
99 m.providerOptions = opts
100}
100}
101
101
102−func (m *McpTool) ProviderOptions() ai.ProviderOptions {
102+func (m *McpTool) ProviderOptions() fantasy.ProviderOptions {
103 return m.providerOptions
103 return m.providerOptions
104}
104}
105
105
@@ -115,7 +115,7 @@ func (m *McpTool) MCPToolName() string {
115 return m.tool.Name
115 return m.tool.Name
116}
116}
117
117
118−func (b *McpTool) Info() ai.ToolInfo {
118+func (b *McpTool) Info() fantasy.ToolInfo {
119 input := b.tool.InputSchema.(map[string]any)
119 input := b.tool.InputSchema.(map[string]any)
120 required, _ := input["required"].([]string)
120 required, _ := input["required"].([]string)
121 if required == nil {
121 if required == nil {
@@ -125,7 +125,7 @@ func (b *McpTool) Info() ai.ToolInfo {
125 if parameters == nil {
125 if parameters == nil {
126 parameters = make(map[string]any)
126 parameters = make(map[string]any)
127 }
127 }
128− return ai.ToolInfo{
128+ return fantasy.ToolInfo{
129 Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
129 Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
130 Description: b.tool.Description,
130 Description: b.tool.Description,
131 Parameters: parameters,
131 Parameters: parameters,
@@ -133,22 +133,22 @@ func (b *McpTool) Info() ai.ToolInfo {
133 }
133 }
134}
134}
135
135
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) {
137 var args map[string]any
137 var args map[string]any
138 if err := json.Unmarshal([]byte(input), &args); err != nil {
138 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
140 }
140 }
141
141
142 c, err := getOrRenewClient(ctx, name)
142 c, err := getOrRenewClient(ctx, name)
143 if err != nil {
143 if err != nil {
144− return ai.NewTextErrorResponse(err.Error()), nil
144+ return fantasy.NewTextErrorResponse(err.Error()), nil
145 }
145 }
146 result, err := c.CallTool(ctx, &mcp.CallToolParams{
146 result, err := c.CallTool(ctx, &mcp.CallToolParams{
147 Name: toolName,
147 Name: toolName,
148 Arguments: args,
148 Arguments: args,
149 })
149 })
150 if err != nil {
150 if err != nil {
151− return ai.NewTextErrorResponse(err.Error()), nil
151+ return fantasy.NewTextErrorResponse(err.Error()), nil
152 }
152 }
153
153
154 output := make([]string, 0, len(result.Content))
154 output := make([]string, 0, len(result.Content))
@@ -159,7 +159,7 @@ func runTool(ctx context.Context, name, toolName string, input string) (ai.ToolR
159 output = append(output, fmt.Sprintf("%v", v))
159 output = append(output, fmt.Sprintf("%v", v))
160 }
160 }
161 }
161 }
162− return ai.NewTextResponse(strings.Join(output, "\n")), nil
162+ return fantasy.NewTextResponse(strings.Join(output, "\n")), nil
163}
163}
164
164
165func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
165func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
@@ -191,10 +191,10 @@ func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, err
191 return sess, nil
191 return sess, nil
192}
192}
193
193
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) {
195 sessionID := GetSessionFromContext(ctx)
195 sessionID := GetSessionFromContext(ctx)
196 if sessionID == "" {
196 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")
198 }
198 }
199 permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name)
199 permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name)
200 p := m.permissions.Request(
200 p := m.permissions.Request(
@@ -209,7 +209,7 @@ func (m *McpTool) Run(ctx context.Context, params ai.ToolCall) (ai.ToolResponse,
209 },
209 },
210 )
210 )
211 if !p {
211 if !p {
212− return ai.ToolResponse{}, permission.ErrorPermissionDenied
212+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
213 }
213 }
214
214
215 return runTool(ctx, m.mcpName, m.tool.Name, params.Input)
215 return runTool(ctx, m.mcpName, m.tool.Name, params.Input)
internal/agent/tools/multiedit.go+35−35
@@ -10,13 +10,13 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "charm.land/fantasy"
13 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/lsp"
18 "github.com/charmbracelet/crush/internal/lsp"
18 "github.com/charmbracelet/crush/internal/permission"
19 "github.com/charmbracelet/crush/internal/permission"
19− "github.com/charmbracelet/fantasy/ai"
20)
20)
21
21
22type MultiEditOperation struct {
22type MultiEditOperation struct {
@@ -49,17 +49,17 @@ const MultiEditToolName = "multiedit"
49//go:embed multiedit.md
49//go:embed multiedit.md
50var multieditDescription []byte
50var multieditDescription []byte
51
51
52−func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
52+func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
53− return ai.NewAgentTool(
53+ return fantasy.NewAgentTool(
54 MultiEditToolName,
54 MultiEditToolName,
55 string(multieditDescription),
55 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) {
57 if params.FilePath == "" {
57 if params.FilePath == "" {
58− return ai.NewTextErrorResponse("file_path is required"), nil
58+ return fantasy.NewTextErrorResponse("file_path is required"), nil
59 }
59 }
60
60
61 if len(params.Edits) == 0 {
61 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
63 }
63 }
64
64
65 if !filepath.IsAbs(params.FilePath) {
65 if !filepath.IsAbs(params.FilePath) {
@@ -68,10 +68,10 @@ func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions pe
68
68
69 // Validate all edits before applying any
69 // Validate all edits before applying any
70 if err := validateEdits(params.Edits); err != nil {
70 if err := validateEdits(params.Edits); err != nil {
71− return ai.NewTextErrorResponse(err.Error()), nil
71+ return fantasy.NewTextErrorResponse(err.Error()), nil
72 }
72 }
73
73
74− var response ai.ToolResponse
74+ var response fantasy.ToolResponse
75 var err error
75 var err error
76
76
77 editCtx := editContext{ctx, permissions, files, workingDir}
77 editCtx := editContext{ctx, permissions, files, workingDir}
@@ -114,24 +114,24 @@ func validateEdits(edits []MultiEditOperation) error {
114 return nil
114 return nil
115}
115}
116
116
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) {
118 // First edit creates the file
118 // First edit creates the file
119 firstEdit := params.Edits[0]
119 firstEdit := params.Edits[0]
120 if firstEdit.OldString != "" {
120 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
122 }
122 }
123
123
124 // Check if file already exists
124 // Check if file already exists
125 if _, err := os.Stat(params.FilePath); err == nil {
125 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
127 } else if !os.IsNotExist(err) {
127 } 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)
129 }
129 }
130
130
131 // Create parent directories
131 // Create parent directories
132 dir := filepath.Dir(params.FilePath)
132 dir := filepath.Dir(params.FilePath)
133 if err := os.MkdirAll(dir, 0o755); err != nil {
133 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)
135 }
135 }
136
136
137 // Start with the content from the first edit
137 // Start with the content from the first edit
@@ -142,7 +142,7 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
142 edit := params.Edits[i]
142 edit := params.Edits[i]
143 newContent, err := applyEditToContent(currentContent, edit)
143 newContent, err := applyEditToContent(currentContent, edit)
144 if err != nil {
144 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
146 }
146 }
147 currentContent = newContent
147 currentContent = newContent
148 }
148 }
@@ -150,7 +150,7 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
150 // Get session and message IDs
150 // Get session and message IDs
151 sessionID := GetSessionFromContext(edit.ctx)
151 sessionID := GetSessionFromContext(edit.ctx)
152 if sessionID == "" {
152 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")
154 }
154 }
155
155
156 // Check permissions
156 // Check permissions
@@ -170,19 +170,19 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
170 },
170 },
171 })
171 })
172 if !p {
172 if !p {
173− return ai.ToolResponse{}, permission.ErrorPermissionDenied
173+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
174 }
174 }
175
175
176 // Write the file
176 // Write the file
177 err := os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
177 err := os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
178 if err != nil {
178 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)
180 }
180 }
181
181
182 // Update file history
182 // Update file history
183 _, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, "")
183 _, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, "")
184 if err != nil {
184 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)
186 }
186 }
187
187
188 _, err = edit.files.CreateVersion(edit.ctx, sessionID, params.FilePath, currentContent)
188 _, err = edit.files.CreateVersion(edit.ctx, sessionID, params.FilePath, currentContent)
@@ -193,8 +193,8 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
193 recordFileWrite(params.FilePath)
193 recordFileWrite(params.FilePath)
194 recordFileRead(params.FilePath)
194 recordFileRead(params.FilePath)
195
195
196− return ai.WithResponseMetadata(
196+ return fantasy.WithResponseMetadata(
197− ai.NewTextResponse(fmt.Sprintf("File created with %d edits: %s", len(params.Edits), params.FilePath)),
197+ fantasy.NewTextResponse(fmt.Sprintf("File created with %d edits: %s", len(params.Edits), params.FilePath)),
198 MultiEditResponseMetadata{
198 MultiEditResponseMetadata{
199 OldContent: "",
199 OldContent: "",
200 NewContent: currentContent,
200 NewContent: currentContent,
@@ -205,30 +205,30 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
205 ), nil
205 ), nil
206}
206}
207
207
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) {
209 // Validate file exists and is readable
209 // Validate file exists and is readable
210 fileInfo, err := os.Stat(params.FilePath)
210 fileInfo, err := os.Stat(params.FilePath)
211 if err != nil {
211 if err != nil {
212 if os.IsNotExist(err) {
212 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
214 }
214 }
215− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
215+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
216 }
216 }
217
217
218 if fileInfo.IsDir() {
218 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
220 }
220 }
221
221
222 // Check if file was read before editing
222 // Check if file was read before editing
223 if getLastReadTime(params.FilePath).IsZero() {
223 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
225 }
225 }
226
226
227 // Check if file was modified since last read
227 // Check if file was modified since last read
228 modTime := fileInfo.ModTime()
228 modTime := fileInfo.ModTime()
229 lastRead := getLastReadTime(params.FilePath)
229 lastRead := getLastReadTime(params.FilePath)
230 if modTime.After(lastRead) {
230 if modTime.After(lastRead) {
231− return ai.NewTextErrorResponse(
231+ return fantasy.NewTextErrorResponse(
232 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
232 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
233 params.FilePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
233 params.FilePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
234 )), nil
234 )), nil
@@ -237,7 +237,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
237 // Read current file content
237 // Read current file content
238 content, err := os.ReadFile(params.FilePath)
238 content, err := os.ReadFile(params.FilePath)
239 if err != nil {
239 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)
241 }
241 }
242
242
243 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
243 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -247,20 +247,20 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
247 for i, edit := range params.Edits {
247 for i, edit := range params.Edits {
248 newContent, err := applyEditToContent(currentContent, edit)
248 newContent, err := applyEditToContent(currentContent, edit)
249 if err != nil {
249 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
251 }
251 }
252 currentContent = newContent
252 currentContent = newContent
253 }
253 }
254
254
255 // Check if content actually changed
255 // Check if content actually changed
256 if oldContent == currentContent {
256 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
258 }
258 }
259
259
260 // Get session and message IDs
260 // Get session and message IDs
261 sessionID := GetSessionFromContext(edit.ctx)
261 sessionID := GetSessionFromContext(edit.ctx)
262 if sessionID == "" {
262 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")
264 }
264 }
265
265
266 // Generate diff and check permissions
266 // Generate diff and check permissions
@@ -279,7 +279,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
279 },
279 },
280 })
280 })
281 if !p {
281 if !p {
282− return ai.ToolResponse{}, permission.ErrorPermissionDenied
282+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
283 }
283 }
284
284
285 if isCrlf {
285 if isCrlf {
@@ -289,7 +289,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
289 // Write the updated content
289 // Write the updated content
290 err = os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
290 err = os.WriteFile(params.FilePath, []byte(currentContent), 0o644)
291 if err != nil {
291 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)
293 }
293 }
294
294
295 // Update file history
295 // Update file history
@@ -297,7 +297,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
297 if err != nil {
297 if err != nil {
298 _, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, oldContent)
298 _, err = edit.files.Create(edit.ctx, sessionID, params.FilePath, oldContent)
299 if err != nil {
299 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)
301 }
301 }
302 }
302 }
303 if file.Content != oldContent {
303 if file.Content != oldContent {
@@ -317,8 +317,8 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
317 recordFileWrite(params.FilePath)
317 recordFileWrite(params.FilePath)
318 recordFileRead(params.FilePath)
318 recordFileRead(params.FilePath)
319
319
320− return ai.WithResponseMetadata(
320+ return fantasy.WithResponseMetadata(
321− ai.NewTextResponse(fmt.Sprintf("Applied %d edits to file: %s", len(params.Edits), params.FilePath)),
321+ fantasy.NewTextResponse(fmt.Sprintf("Applied %d edits to file: %s", len(params.Edits), params.FilePath)),
322 MultiEditResponseMetadata{
322 MultiEditResponseMetadata{
323 OldContent: oldContent,
323 OldContent: oldContent,
324 NewContent: currentContent,
324 NewContent: currentContent,
internal/agent/tools/sourcegraph.go+14−14
@@ -11,7 +11,7 @@ import (
11 "strings"
11 "strings"
12 "time"
12 "time"
13
13
14− "github.com/charmbracelet/fantasy/ai"
14+ "charm.land/fantasy"
15)
15)
16
16
17type SourcegraphParams struct {
17type SourcegraphParams struct {
@@ -31,7 +31,7 @@ const SourcegraphToolName = "sourcegraph"
31//go:embed sourcegraph.md
31//go:embed sourcegraph.md
32var sourcegraphDescription []byte
32var sourcegraphDescription []byte
33
33
34−func NewSourcegraphTool(client *http.Client) ai.AgentTool {
34+func NewSourcegraphTool(client *http.Client) fantasy.AgentTool {
35 if client == nil {
35 if client == nil {
36 client = &http.Client{
36 client = &http.Client{
37 Timeout: 30 * time.Second,
37 Timeout: 30 * time.Second,
@@ -42,12 +42,12 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
42 },
42 },
43 }
43 }
44 }
44 }
45− return ai.NewAgentTool(
45+ return fantasy.NewAgentTool(
46 SourcegraphToolName,
46 SourcegraphToolName,
47 string(sourcegraphDescription),
47 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) {
49 if params.Query == "" {
49 if params.Query == "" {
50− return ai.NewTextErrorResponse("Query parameter is required"), nil
50+ return fantasy.NewTextErrorResponse("Query parameter is required"), nil
51 }
51 }
52
52
53 if params.Count <= 0 {
53 if params.Count <= 0 {
@@ -86,7 +86,7 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
86
86
87 graphqlQueryBytes, err := json.Marshal(request)
87 graphqlQueryBytes, err := json.Marshal(request)
88 if err != nil {
88 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)
90 }
90 }
91 graphqlQuery := string(graphqlQueryBytes)
91 graphqlQuery := string(graphqlQueryBytes)
92
92
@@ -97,7 +97,7 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
97 bytes.NewBuffer([]byte(graphqlQuery)),
97 bytes.NewBuffer([]byte(graphqlQuery)),
98 )
98 )
99 if err != nil {
99 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)
101 }
101 }
102
102
103 req.Header.Set("Content-Type", "application/json")
103 req.Header.Set("Content-Type", "application/json")
@@ -105,34 +105,34 @@ func NewSourcegraphTool(client *http.Client) ai.AgentTool {
105
105
106 resp, err := client.Do(req)
106 resp, err := client.Do(req)
107 if err != nil {
107 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)
109 }
109 }
110 defer resp.Body.Close()
110 defer resp.Body.Close()
111
111
112 if resp.StatusCode != http.StatusOK {
112 if resp.StatusCode != http.StatusOK {
113 body, _ := io.ReadAll(resp.Body)
113 body, _ := io.ReadAll(resp.Body)
114 if len(body) > 0 {
114 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
116 }
116 }
117
117
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
119 }
119 }
120 body, err := io.ReadAll(resp.Body)
120 body, err := io.ReadAll(resp.Body)
121 if err != nil {
121 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)
123 }
123 }
124
124
125 var result map[string]any
125 var result map[string]any
126 if err = json.Unmarshal(body, &result); err != nil {
126 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)
128 }
128 }
129
129
130 formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
130 formattedResults, err := formatSourcegraphResults(result, params.ContextWindow)
131 if err != nil {
131 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
133 }
133 }
134
134
135− return ai.NewTextResponse(formattedResults), nil
135+ return fantasy.NewTextResponse(formattedResults), nil
136 })
136 })
137}
137}
138
138
internal/agent/tools/view.go+19−19
@@ -11,10 +11,10 @@ import (
11 "strings"
11 "strings"
12 "unicode/utf8"
12 "unicode/utf8"
13
13
14+ "charm.land/fantasy"
14 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/lsp"
16 "github.com/charmbracelet/crush/internal/lsp"
16 "github.com/charmbracelet/crush/internal/permission"
17 "github.com/charmbracelet/crush/internal/permission"
17− "github.com/charmbracelet/fantasy/ai"
18)
18)
19
19
20//go:embed view.md
20//go:embed view.md
@@ -50,13 +50,13 @@ const (
50 MaxLineLength = 2000
50 MaxLineLength = 2000
51)
51)
52
52
53−func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) ai.AgentTool {
53+func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) fantasy.AgentTool {
54− return ai.NewAgentTool(
54+ return fantasy.NewAgentTool(
55 ViewToolName,
55 ViewToolName,
56 string(viewDescription),
56 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) {
58 if params.FilePath == "" {
58 if params.FilePath == "" {
59− return ai.NewTextErrorResponse("file_path is required"), nil
59+ return fantasy.NewTextErrorResponse("file_path is required"), nil
60 }
60 }
61
61
62 // Handle relative paths
62 // Handle relative paths
@@ -68,12 +68,12 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
68 // Check if file is outside working directory and request permission if needed
68 // Check if file is outside working directory and request permission if needed
69 absWorkingDir, err := filepath.Abs(workingDir)
69 absWorkingDir, err := filepath.Abs(workingDir)
70 if err != nil {
70 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)
72 }
72 }
73
73
74 absFilePath, err := filepath.Abs(filePath)
74 absFilePath, err := filepath.Abs(filePath)
75 if err != nil {
75 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)
77 }
77 }
78
78
79 relPath, err := filepath.Rel(absWorkingDir, absFilePath)
79 relPath, err := filepath.Rel(absWorkingDir, absFilePath)
@@ -81,7 +81,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
81 // File is outside working directory, request permission
81 // File is outside working directory, request permission
82 sessionID := GetSessionFromContext(ctx)
82 sessionID := GetSessionFromContext(ctx)
83 if sessionID == "" {
83 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")
85 }
85 }
86
86
87 granted := permissions.Request(
87 granted := permissions.Request(
@@ -97,7 +97,7 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
97 )
97 )
98
98
99 if !granted {
99 if !granted {
100− return ai.ToolResponse{}, permission.ErrorPermissionDenied
100+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
101 }
101 }
102 }
102 }
103
103
@@ -123,24 +123,24 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
123 }
123 }
124
124
125 if len(suggestions) > 0 {
125 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",
127 filePath, strings.Join(suggestions, "\n"))), nil
127 filePath, strings.Join(suggestions, "\n"))), nil
128 }
128 }
129 }
129 }
130
130
131− return ai.NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
131+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File not found: %s", filePath)), nil
132 }
132 }
133− return ai.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
133+ return fantasy.ToolResponse{}, fmt.Errorf("error accessing file: %w", err)
134 }
134 }
135
135
136 // Check if it's a directory
136 // Check if it's a directory
137 if fileInfo.IsDir() {
137 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
139 }
139 }
140
140
141 // Check file size
141 // Check file size
142 if fileInfo.Size() > MaxReadSize {
142 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",
144 fileInfo.Size(), MaxReadSize)), nil
144 fileInfo.Size(), MaxReadSize)), nil
145 }
145 }
146
146
@@ -153,17 +153,17 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
153 isImage, imageType := isImageFile(filePath)
153 isImage, imageType := isImageFile(filePath)
154 // TODO: handle images
154 // TODO: handle images
155 if isImage {
155 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
157 }
157 }
158
158
159 // Read the file content
159 // Read the file content
160 content, lineCount, err := readTextFile(filePath, params.Offset, params.Limit)
160 content, lineCount, err := readTextFile(filePath, params.Offset, params.Limit)
161 isValidUt8 := utf8.ValidString(content)
161 isValidUt8 := utf8.ValidString(content)
162 if !isValidUt8 {
162 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
164 }
164 }
165 if err != nil {
165 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)
167 }
167 }
168
168
169 notifyLSPs(ctx, lspClients, filePath)
169 notifyLSPs(ctx, lspClients, filePath)
@@ -179,8 +179,8 @@ func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
179 output += "\n</file>\n"
179 output += "\n</file>\n"
180 output += getDiagnostics(filePath, lspClients)
180 output += getDiagnostics(filePath, lspClients)
181 recordFileRead(filePath)
181 recordFileRead(filePath)
182− return ai.WithResponseMetadata(
182+ return fantasy.WithResponseMetadata(
183− ai.NewTextResponse(output),
183+ fantasy.NewTextResponse(output),
184 ViewResponseMetadata{
184 ViewResponseMetadata{
185 FilePath: filePath,
185 FilePath: filePath,
186 Content: content,
186 Content: content,
internal/agent/tools/write.go+16−16
@@ -10,11 +10,11 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "charm.land/fantasy"
13 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/history"
17− "github.com/charmbracelet/fantasy/ai"
18
18
19 "github.com/charmbracelet/crush/internal/lsp"
19 "github.com/charmbracelet/crush/internal/lsp"
20 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/permission"
@@ -49,17 +49,17 @@ type WriteResponseMetadata struct {
49
49
50const WriteToolName = "write"
50const WriteToolName = "write"
51
51
52−func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
52+func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
53− return ai.NewAgentTool(
53+ return fantasy.NewAgentTool(
54 WriteToolName,
54 WriteToolName,
55 string(writeDescription),
55 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) {
57 if params.FilePath == "" {
57 if params.FilePath == "" {
58− return ai.NewTextErrorResponse("file_path is required"), nil
58+ return fantasy.NewTextErrorResponse("file_path is required"), nil
59 }
59 }
60
60
61 if params.Content == "" {
61 if params.Content == "" {
62− return ai.NewTextErrorResponse("content is required"), nil
62+ return fantasy.NewTextErrorResponse("content is required"), nil
63 }
63 }
64
64
65 filePath := params.FilePath
65 filePath := params.FilePath
@@ -70,27 +70,27 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
70 fileInfo, err := os.Stat(filePath)
70 fileInfo, err := os.Stat(filePath)
71 if err == nil {
71 if err == nil {
72 if fileInfo.IsDir() {
72 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
74 }
74 }
75
75
76 modTime := fileInfo.ModTime()
76 modTime := fileInfo.ModTime()
77 lastRead := getLastReadTime(filePath)
77 lastRead := getLastReadTime(filePath)
78 if modTime.After(lastRead) {
78 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.",
80 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
80 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
81 }
81 }
82
82
83 oldContent, readErr := os.ReadFile(filePath)
83 oldContent, readErr := os.ReadFile(filePath)
84 if readErr == nil && string(oldContent) == params.Content {
84 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
86 }
86 }
87 } else if !os.IsNotExist(err) {
87 } 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)
89 }
89 }
90
90
91 dir := filepath.Dir(filePath)
91 dir := filepath.Dir(filePath)
92 if err = os.MkdirAll(dir, 0o755); err != nil {
92 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)
94 }
94 }
95
95
96 oldContent := ""
96 oldContent := ""
@@ -103,7 +103,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
103
103
104 sessionID := GetSessionFromContext(ctx)
104 sessionID := GetSessionFromContext(ctx)
105 if sessionID == "" {
105 if sessionID == "" {
106− return ai.ToolResponse{}, fmt.Errorf("session_id is required")
106+ return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
107 }
107 }
108
108
109 diff, additions, removals := diff.GenerateDiff(
109 diff, additions, removals := diff.GenerateDiff(
@@ -128,12 +128,12 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
128 },
128 },
129 )
129 )
130 if !p {
130 if !p {
131− return ai.ToolResponse{}, permission.ErrorPermissionDenied
131+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
132 }
132 }
133
133
134 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
134 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
135 if err != nil {
135 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)
137 }
137 }
138
138
139 // Check if file exists in history
139 // Check if file exists in history
@@ -142,7 +142,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
142 _, err = files.Create(ctx, sessionID, filePath, oldContent)
142 _, err = files.Create(ctx, sessionID, filePath, oldContent)
143 if err != nil {
143 if err != nil {
144 // Log error but don't fail the operation
144 // 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)
146 }
146 }
147 }
147 }
148 if file.Content != oldContent {
148 if file.Content != oldContent {
@@ -166,7 +166,7 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
166 result := fmt.Sprintf("File successfully written: %s", filePath)
166 result := fmt.Sprintf("File successfully written: %s", filePath)
167 result = fmt.Sprintf("<result>\n%s\n</result>", result)
167 result = fmt.Sprintf("<result>\n%s\n</result>", result)
168 result += getDiagnostics(filePath, lspClients)
168 result += getDiagnostics(filePath, lspClients)
169− return ai.WithResponseMetadata(ai.NewTextResponse(result),
169+ return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
170 WriteResponseMetadata{
170 WriteResponseMetadata{
171 Diff: diff,
171 Diff: diff,
172 Additions: additions,
172 Additions: additions,
internal/app/app.go+2−2
@@ -9,6 +9,7 @@ import (
9 "sync"
9 "sync"
10 "time"
10 "time"
11
11
12+ "charm.land/fantasy"
12 tea "github.com/charmbracelet/bubbletea/v2"
13 tea "github.com/charmbracelet/bubbletea/v2"
13 "github.com/charmbracelet/crush/internal/agent"
14 "github.com/charmbracelet/crush/internal/agent"
14 "github.com/charmbracelet/crush/internal/agent/tools"
15 "github.com/charmbracelet/crush/internal/agent/tools"
@@ -23,7 +24,6 @@ import (
23 "github.com/charmbracelet/crush/internal/permission"
24 "github.com/charmbracelet/crush/internal/permission"
24 "github.com/charmbracelet/crush/internal/pubsub"
25 "github.com/charmbracelet/crush/internal/pubsub"
25 "github.com/charmbracelet/crush/internal/session"
26 "github.com/charmbracelet/crush/internal/session"
26− "github.com/charmbracelet/fantasy/ai"
27 "github.com/charmbracelet/x/ansi"
27 "github.com/charmbracelet/x/ansi"
28)
28)
29
29
@@ -145,7 +145,7 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
145 app.Permissions.AutoApproveSession(sess.ID)
145 app.Permissions.AutoApproveSession(sess.ID)
146
146
147 type response struct {
147 type response struct {
148− result *ai.AgentResult
148+ result *fantasy.AgentResult
149 err error
149 err error
150 }
150 }
151 done := make(chan response, 1)
151 done := make(chan response, 1)
internal/message/content.go+23−23
@@ -7,9 +7,9 @@ import (
7 "strings"
7 "strings"
8 "time"
8 "time"
9
9
10+ "charm.land/fantasy"
11+ "charm.land/fantasy/providers/anthropic"
10 "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
11− "github.com/charmbracelet/fantasy/ai"
12− "github.com/charmbracelet/fantasy/anthropic"
13)
13)
14
14
15type MessageRole string
15type MessageRole string
@@ -390,35 +390,35 @@ func (m *Message) AddBinary(mimeType string, data []byte) {
390 m.Parts = append(m.Parts, BinaryContent{MIMEType: mimeType, Data: data})
390 m.Parts = append(m.Parts, BinaryContent{MIMEType: mimeType, Data: data})
391}
391}
392
392
393−func (m *Message) ToAIMessage() []ai.Message {
393+func (m *Message) ToAIMessage() []fantasy.Message {
394− var messages []ai.Message
394+ var messages []fantasy.Message
395 switch m.Role {
395 switch m.Role {
396 case User:
396 case User:
397− var parts []ai.MessagePart
397+ var parts []fantasy.MessagePart
398 text := strings.TrimSpace(m.Content().Text)
398 text := strings.TrimSpace(m.Content().Text)
399 if text != "" {
399 if text != "" {
400− parts = append(parts, ai.TextPart{Text: text})
400+ parts = append(parts, fantasy.TextPart{Text: text})
401 }
401 }
402 for _, content := range m.BinaryContent() {
402 for _, content := range m.BinaryContent() {
403− parts = append(parts, ai.FilePart{
403+ parts = append(parts, fantasy.FilePart{
404 Filename: content.Path,
404 Filename: content.Path,
405 Data: content.Data,
405 Data: content.Data,
406 MediaType: content.MIMEType,
406 MediaType: content.MIMEType,
407 })
407 })
408 }
408 }
409− messages = append(messages, ai.Message{
409+ messages = append(messages, fantasy.Message{
410− Role: ai.MessageRoleUser,
410+ Role: fantasy.MessageRoleUser,
411 Content: parts,
411 Content: parts,
412 })
412 })
413 case Assistant:
413 case Assistant:
414− var parts []ai.MessagePart
414+ var parts []fantasy.MessagePart
415 text := strings.TrimSpace(m.Content().Text)
415 text := strings.TrimSpace(m.Content().Text)
416 if text != "" {
416 if text != "" {
417− parts = append(parts, ai.TextPart{Text: text})
417+ parts = append(parts, fantasy.TextPart{Text: text})
418 }
418 }
419 reasoning := m.ReasoningContent()
419 reasoning := m.ReasoningContent()
420 if reasoning.Thinking != "" {
420 if reasoning.Thinking != "" {
421− reasoningPart := ai.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: ai.ProviderOptions{}}
421+ reasoningPart := fantasy.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: fantasy.ProviderOptions{}}
422 if reasoning.Signature != "" {
422 if reasoning.Signature != "" {
423 reasoningPart.ProviderOptions["anthropic"] = &anthropic.ReasoningOptionMetadata{
423 reasoningPart.ProviderOptions["anthropic"] = &anthropic.ReasoningOptionMetadata{
424 Signature: reasoning.Signature,
424 Signature: reasoning.Signature,
@@ -427,42 +427,42 @@ func (m *Message) ToAIMessage() []ai.Message {
427 parts = append(parts, reasoningPart)
427 parts = append(parts, reasoningPart)
428 }
428 }
429 for _, call := range m.ToolCalls() {
429 for _, call := range m.ToolCalls() {
430− parts = append(parts, ai.ToolCallPart{
430+ parts = append(parts, fantasy.ToolCallPart{
431 ToolCallID: call.ID,
431 ToolCallID: call.ID,
432 ToolName: call.Name,
432 ToolName: call.Name,
433 Input: call.Input,
433 Input: call.Input,
434 ProviderExecuted: call.ProviderExecuted,
434 ProviderExecuted: call.ProviderExecuted,
435 })
435 })
436 }
436 }
437− messages = append(messages, ai.Message{
437+ messages = append(messages, fantasy.Message{
438− Role: ai.MessageRoleAssistant,
438+ Role: fantasy.MessageRoleAssistant,
439 Content: parts,
439 Content: parts,
440 })
440 })
441 case Tool:
441 case Tool:
442− var parts []ai.MessagePart
442+ var parts []fantasy.MessagePart
443 for _, result := range m.ToolResults() {
443 for _, result := range m.ToolResults() {
444− var content ai.ToolResultOutputContent
444+ var content fantasy.ToolResultOutputContent
445 if result.IsError {
445 if result.IsError {
446− content = ai.ToolResultOutputContentError{
446+ content = fantasy.ToolResultOutputContentError{
447 Error: errors.New(result.Content),
447 Error: errors.New(result.Content),
448 }
448 }
449 } else if result.Data != "" {
449 } else if result.Data != "" {
450− content = ai.ToolResultOutputContentMedia{
450+ content = fantasy.ToolResultOutputContentMedia{
451 Data: result.Data,
451 Data: result.Data,
452 MediaType: result.MIMEType,
452 MediaType: result.MIMEType,
453 }
453 }
454 } else {
454 } else {
455− content = ai.ToolResultOutputContentText{
455+ content = fantasy.ToolResultOutputContentText{
456 Text: result.Content,
456 Text: result.Content,
457 }
457 }
458 }
458 }
459− parts = append(parts, ai.ToolResultPart{
459+ parts = append(parts, fantasy.ToolResultPart{
460 ToolCallID: result.ToolCallID,
460 ToolCallID: result.ToolCallID,
461 Output: content,
461 Output: content,
462 })
462 })
463 }
463 }
464− messages = append(messages, ai.Message{
464+ messages = append(messages, fantasy.Message{
465− Role: ai.MessageRoleTool,
465+ Role: fantasy.MessageRoleTool,
466 Content: parts,
466 Content: parts,
467 })
467 })
468 }
468 }