on main help sign in
refactor: agent tool
b45dcfekujtimiihoxha
.gitignore+1
@@ -49,3 +49,4 @@ Thumbs.db
49manpages/
49manpages/
50completions/
50completions/
51!internal/tui/components/completions/
51!internal/tui/components/completions/
52+.prettierignore
go.mod+1−1
@@ -68,7 +68,7 @@ require (
68
68
69require (
69require (
70 github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
70 github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
71− github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77
71+ github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d
72 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
72 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
73)
73)
74
74
go.sum+6
@@ -53,6 +53,12 @@ github.com/charmbracelet/fang v0.4.2 h1:nWr7Tb82/TTNNGMGG35aTZ1X68loAOQmpb0qxkKX
53github.com/charmbracelet/fang v0.4.2/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
53github.com/charmbracelet/fang v0.4.2/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
54github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77 h1:YHuUqaojkeu00YtQeXPqM/1RNJH/jqGNaQYFwa7JQTk=
54github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77 h1:YHuUqaojkeu00YtQeXPqM/1RNJH/jqGNaQYFwa7JQTk=
55github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
55github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
56+github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c h1:+gCA3Sv7g1jnZ96Em7j9u61H2O/1SkBAZ1LM9yd2bM8=
57+github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
58+github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380 h1:UYWO3cutUTV9ZLOH4hrXFy9hg4E1pVbLZsZLA1OA3+g=
59+github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
60+github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d h1:HS9qu7CgQGPnPcNoHIZXGWGFS8SaxaBXYytLdakm+jY=
61+github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
56github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
62github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
57github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
63github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
58github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
64github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
internal/agent/agent.go+6−5
@@ -175,7 +175,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
175 TopK: call.TopK,
175 TopK: call.TopK,
176 FrequencyPenalty: call.FrequencyPenalty,
176 FrequencyPenalty: call.FrequencyPenalty,
177 // Before each step create the new assistant message
177 // Before each step create the new assistant message
178− PrepareStep: func(options ai.PrepareStepFunctionOptions) (prepared ai.PrepareStepResult, err error) {
178+ PrepareStep: func(callContext context.Context, options ai.PrepareStepFunctionOptions) (_ context.Context, prepared ai.PrepareStepResult, err error) {
179 var assistantMsg message.Message
179 var assistantMsg message.Message
180 assistantMsg, err = a.messages.Create(genCtx, call.SessionID, message.CreateMessageParams{
180 assistantMsg, err = a.messages.Create(genCtx, call.SessionID, message.CreateMessageParams{
181 Role: message.Assistant,
181 Role: message.Assistant,
@@ -184,9 +184,11 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
184 Provider: a.largeModel.ModelCfg.Provider,
184 Provider: a.largeModel.ModelCfg.Provider,
185 })
185 })
186 if err != nil {
186 if err != nil {
187− return prepared, err
187+ return callContext, prepared, err
188 }
188 }
189
189
190+ callContext = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
191+
190 currentAssistant = &assistantMsg
192 currentAssistant = &assistantMsg
191
193
192 prepared.Messages = options.Messages
194 prepared.Messages = options.Messages
@@ -200,7 +202,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
200 for _, queued := range queuedCalls {
202 for _, queued := range queuedCalls {
201 userMessage, createErr := a.createUserMessage(genCtx, queued)
203 userMessage, createErr := a.createUserMessage(genCtx, queued)
202 if createErr != nil {
204 if createErr != nil {
203− return prepared, createErr
205+ return callContext, prepared, createErr
204 }
206 }
205 prepared.Messages = append(prepared.Messages, userMessage.ToAIMessage()...)
207 prepared.Messages = append(prepared.Messages, userMessage.ToAIMessage()...)
206 }
208 }
@@ -220,7 +222,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
220 prepared.Messages[i].ProviderOptions = a.getCacheControlOptions()
222 prepared.Messages[i].ProviderOptions = a.getCacheControlOptions()
221 }
223 }
222 }
224 }
223− return prepared, err
225+ return callContext, prepared, err
224 },
226 },
225 OnReasoningDelta: func(id string, text string) error {
227 OnReasoningDelta: func(id string, text string) error {
226 currentAssistant.AppendReasoningContent(text)
228 currentAssistant.AppendReasoningContent(text)
@@ -307,7 +309,6 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
307 case ai.FinishReasonToolCalls:
309 case ai.FinishReasonToolCalls:
308 finishReason = message.FinishReasonToolUse
310 finishReason = message.FinishReasonToolUse
309 }
311 }
310− slog.Info("OnStepFinish", "reason", stepResult.FinishReason)
311 currentAssistant.AddFinish(finishReason, "", "")
312 currentAssistant.AddFinish(finishReason, "", "")
312 a.updateSessionUsage(a.largeModel, &currentSession, stepResult.Usage)
313 a.updateSessionUsage(a.largeModel, &currentSession, stepResult.Usage)
313 sessionLock.Lock()
314 sessionLock.Lock()
internal/agent/agent-tool.go → internal/agent/agent_tool.gorenamed+9−3
@@ -14,7 +14,7 @@ import (
14 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
15)
15)
16
16
17−//go:embed templates/agentTool.md
17+//go:embed templates/agent_tool.md
18var agentToolDescription []byte
18var agentToolDescription []byte
19
19
20type AgentParams struct {
20type AgentParams struct {
@@ -55,7 +55,13 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
55 return ai.ToolResponse{}, errors.New("session id missing from context")
55 return ai.ToolResponse{}, errors.New("session id missing from context")
56 }
56 }
57
57
58+ agentMessageID := tools.GetMessageFromContext(ctx)
59+ if agentMessageID == "" {
60+ return ai.ToolResponse{}, errors.New("agent message id missing from context")
61+ }
62+
63+ agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, call.ID)
58− session, err := c.sessions.CreateTaskSession(ctx, call.ID, sessionID, "New Agent Session")
64+ session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, "New Agent Session")
59 if err != nil {
65 if err != nil {
60 return ai.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
66 return ai.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
61 }
67 }
@@ -65,7 +71,7 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
65 maxTokens = model.ModelCfg.MaxTokens
71 maxTokens = model.ModelCfg.MaxTokens
66 }
72 }
67 result, err := agent.Run(ctx, SessionAgentCall{
73 result, err := agent.Run(ctx, SessionAgentCall{
68− SessionID: sessionID,
74+ SessionID: session.ID,
69 Prompt: params.Prompt,
75 Prompt: params.Prompt,
70 MaxOutputTokens: maxTokens,
76 MaxOutputTokens: maxTokens,
71 ProviderOptions: c.getProviderOptions(model),
77 ProviderOptions: c.getProviderOptions(model),
internal/agent/common_test.go−19
@@ -1,7 +1,6 @@
1package agent
1package agent
2
2
3import (
3import (
4− "fmt"
5 "net/http"
4 "net/http"
6 "os"
5 "os"
7 "path/filepath"
6 "path/filepath"
@@ -69,18 +68,9 @@ func openaiBuilder(model string) builderFunc {
69
68
70func openRouterBuilder(model string) builderFunc {
69func openRouterBuilder(model string) builderFunc {
71 return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
70 return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
72− tf := func() func() string {
73− id := 0
74− return func() string {
75− id += 1
76− return fmt.Sprintf("%s-%d", t.Name(), id)
77− }
78− }
79 provider := openrouter.New(
71 provider := openrouter.New(
80 openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")),
72 openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")),
81 openrouter.WithHTTPClient(&http.Client{Transport: r}),
73 openrouter.WithHTTPClient(&http.Client{Transport: r}),
82− openrouter.WithLanguageUniqueToolCallIds(),
83− openrouter.WithLanguageModelGenerateIDFunc(tf()),
84 )
74 )
85 return provider.LanguageModel(model)
75 return provider.LanguageModel(model)
86 }
76 }
@@ -88,19 +78,10 @@ func openRouterBuilder(model string) builderFunc {
88
78
89func zAIBuilder(model string) builderFunc {
79func zAIBuilder(model string) builderFunc {
90 return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
80 return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
91− tf := func() func() string {
92− id := 0
93− return func() string {
94− id += 1
95− return fmt.Sprintf("%s-%d", t.Name(), id)
96− }
97− }
98 provider := openaicompat.New(
81 provider := openaicompat.New(
99 "https://api.z.ai/api/coding/paas/v4",
82 "https://api.z.ai/api/coding/paas/v4",
100 openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")),
83 openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")),
101 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
84 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
102− openaicompat.WithLanguageUniqueToolCallIds(),
103− openaicompat.WithLanguageModelGenerateIDFunc(tf()),
104 )
85 )
105 return provider.LanguageModel(model)
86 return provider.LanguageModel(model)
106 }
87 }
internal/agent/coordinator.go−2
@@ -356,7 +356,6 @@ func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[st
356func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) ai.Provider {
356func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) ai.Provider {
357 opts := []openrouter.Option{
357 opts := []openrouter.Option{
358 openrouter.WithAPIKey(apiKey),
358 openrouter.WithAPIKey(apiKey),
359− openrouter.WithLanguageUniqueToolCallIds(),
360 }
359 }
361 if c.cfg.Options.Debug {
360 if c.cfg.Options.Debug {
362 httpClient := log.NewHTTPClient()
361 httpClient := log.NewHTTPClient()
@@ -371,7 +370,6 @@ func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[stri
371func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
370func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
372 opts := []openaicompat.Option{
371 opts := []openaicompat.Option{
373 openaicompat.WithAPIKey(apiKey),
372 openaicompat.WithAPIKey(apiKey),
374− openaicompat.WithLanguageUniqueToolCallIds(),
375 }
373 }
376 if c.cfg.Options.Debug {
374 if c.cfg.Options.Debug {
377 httpClient := log.NewHTTPClient()
375 httpClient := log.NewHTTPClient()
internal/agent/prompt/prompt.go+5−5
@@ -15,11 +15,11 @@ import (
15
15
16// Prompt represents a template-based prompt generator.
16// Prompt represents a template-based prompt generator.
17type Prompt struct {
17type Prompt struct {
18− name string
18+ name string
19− template string
19+ template string
20− now func() time.Time
20+ now func() time.Time
21− platform string
21+ platform string
22− workingDir string
22+ workingDir string
23}
23}
24
24
25type PromptDat struct {
25type PromptDat struct {
internal/agent/prompts.go+2−2
@@ -6,10 +6,10 @@ import (
6 "github.com/charmbracelet/crush/internal/agent/prompt"
6 "github.com/charmbracelet/crush/internal/agent/prompt"
7)
7)
8
8
9−//go:embed templates/coder.gotmpl
9+//go:embed templates/coder.md.tpl
10var coderPromptTmpl []byte
10var coderPromptTmpl []byte
11
11
12−//go:embed templates/task.gotmpl
12+//go:embed templates/task.md.tpl
13var taskPromptTmpl []byte
13var taskPromptTmpl []byte
14
14
15//go:embed templates/initialize.md
15//go:embed templates/initialize.md
internal/agent/templates/agentTool.md → internal/agent/templates/agent_tool.mdrenamed+3−4
@@ -1,16 +1,15 @@
1Launch a new agent that has access to the following tools: GlobTool, GrepTool, LS, View. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you.
1Launch a new agent that has access to the following tools: GlobTool, GrepTool, LS, View. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you.
2−<usage>
3
2
3+<usage>
4- If you are searching for a keyword like "config" or "logger", or for questions like "which file does X?", the Agent tool is strongly recommended
4- If you are searching for a keyword like "config" or "logger", or for questions like "which file does X?", the Agent tool is strongly recommended
5- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly
5- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly
6- If you are searching for a specific class definition like "class Foo", use the GlobTool tool instead, to find the match more quickly
6- If you are searching for a specific class definition like "class Foo", use the GlobTool tool instead, to find the match more quickly
7− </usage>
7+</usage>
8
8
9<usage_notes>
9<usage_notes>
10−
111. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
101. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
122. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
112. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
133. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
123. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
144. The agent's outputs should generally be trusted
134. The agent's outputs should generally be trusted
155. IMPORTANT: The agent can not use Bash, Replace, Edit, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.
145. IMPORTANT: The agent can not use Bash, Replace, Edit, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.
16− </usage_notes>
15+</usage_notes>
internal/agent/templates/coder.gotmpl → internal/agent/templates/coder.md.tplrenamed+12−8
@@ -2,18 +2,19 @@ You are Crush, a powerful AI Assistant that runs in the CLI.
2Use the instructions below and the tools available to you to assist the user.
2Use the instructions below and the tools available to you to assist the user.
3
3
4<memory_instructions>
4<memory_instructions>
5−If the current working directory contains a file called CRUSH.md, it will be automatically added to your context.
5+If the current working directory contains a file used for memory, they will be automatically added to your context.
6
6
7−This file serves multiple purposes:
7+These file serves multiple purposes:
8
8
9- Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time
9- Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time
10- Recording the user's code style preferences (naming conventions, preferred libraries, etc.)
10- Recording the user's code style preferences (naming conventions, preferred libraries, etc.)
11- Maintaining useful information about the codebase structure and organization
11- Maintaining useful information about the codebase structure and organization
12
12
13−When you discover important information that could be useful for the future update/add the info to the CRUSH.md.
13+When you discover important information that could be useful for the future update/add the info the appropriate memory file.
14
14
15−Memory might be added to you during a task if there are nested memory files that relate to the work you are doing.
15+Make sure to follow the memory files instructions while working.
16</memory_instructions>
16</memory_instructions>
17+
17<communication_style>
18<communication_style>
18- Be concise and direct
19- Be concise and direct
19- Keep responses under 4 lines unless details requested
20- Keep responses under 4 lines unless details requested
@@ -65,6 +66,7 @@ assistant: src/foo.c
65user: write tests for new feature
66user: write tests for new feature
66assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
67assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
67</example>
68</example>
69+
68</communication_style>
70</communication_style>
69
71
70<proactiveness>
72<proactiveness>
@@ -86,15 +88,17 @@ When making changes to files, first understand the file's code conventions. Mimi
86</following_conversations>
88</following_conversations>
87
89
88<code_style>
90<code_style>
89−- Follow existing code style and patterns
91+- Follow existing code style and patterns.
90−- Do not add any comments to code you write unless asked to do so
92+- Do not add any comments to code you write unless asked to do so.
93+- Thrive to write only code that is necessary to solve the given issue (less code is always better).
94+- Follow best practices for the language and framework used in the project.
91</code_style>
95</code_style>
92
96
93<doing_tasks>
97<doing_tasks>
94The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
98The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
95
99
96- Use the available search tools to understand the codebase and the user's query.
100- Use the available search tools to understand the codebase and the user's query.
97−- Plan out the implementation
101+- Plan out the implementation (create a todo list)
98- Implement the solution using all tools available to you
102- Implement the solution using all tools available to you
99- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
103- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
100- When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to CRUSH.md so that you will know to run it next time.
104- When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to CRUSH.md so that you will know to run it next time.
@@ -103,7 +107,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
103</doing_tasks>
107</doing_tasks>
104
108
105<tool_use>
109<tool_use>
106−- When doing file search, prefer to use the Agent tool in order to reduce context usage.
110+- When doing file search, prefer to use the Agent tool, give the agent detailed instructions on what to search for and response format details.
107- All tools are executed in parallel when multiple tool calls are sent in a single message. Only send multiple tool calls when they are safe to run in parallel (no dependencies between them).
111- All tools are executed in parallel when multiple tool calls are sent in a single message. Only send multiple tool calls when they are safe to run in parallel (no dependencies between them).
108- The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
112- The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
109</tool_use>
113</tool_use>
internal/agent/templates/initialize.md+1−1
@@ -1,4 +1,4 @@
1−`Please analyze this codebase and create a **CRUSH.md** file containing:
1+Please analyze this codebase and create a **CRUSH.md** file containing:
2
2
3- Build/lint/test commands - especially for running a single test
3- Build/lint/test commands - especially for running a single test
4- Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
4- Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
internal/agent/templates/task.gotmpl → internal/agent/templates/task.md.tplrenamed

The content is the same.

internal/agent/templates/title.md+2
@@ -1,8 +1,10 @@
1you will generate a short title based on the first message a user begins a conversation with
1you will generate a short title based on the first message a user begins a conversation with
2
2
3+<rules>
3- ensure it is not more than 50 characters long
4- ensure it is not more than 50 characters long
4- the title should be a summary of the user's message
5- the title should be a summary of the user's message
5- it should be one line long
6- it should be one line long
6- do not use quotes or colons
7- do not use quotes or colons
7- the entire text you return will be used as the title
8- the entire text you return will be used as the title
8- never return anything that is more than one sentence (one line) long
9- never return anything that is more than one sentence (one line) long
10+</rules>
internal/agent/tools/bash.go+1−1
@@ -44,7 +44,7 @@ const (
44 BashNoOutput = "no output"
44 BashNoOutput = "no output"
45)
45)
46
46
47−//go:embed bash.gotmpl
47+//go:embed bash.tpl
48var bashDescriptionTmpl []byte
48var bashDescriptionTmpl []byte
49
49
50var bashDescriptionTpl = template.Must(
50var bashDescriptionTpl = template.Must(
internal/agent/tools/bash.gotmpl → internal/agent/tools/bash.tplrenamed+1−1
@@ -108,7 +108,7 @@ Important:
108
108
109- Return empty response - user sees gh output
109- Return empty response - user sees gh output
110- Never update git config
110- Never update git config
111− </pull_requests>
111+</pull_requests>
112
112
113<examples>
113<examples>
114Good: pytest /foo/bar/tests
114Good: pytest /foo/bar/tests
internal/agent/tools/edit.md+4−7
@@ -13,10 +13,9 @@ Edits files by replacing text, creating new files, or deleting content. For movi
13</parameters>
13</parameters>
14
14
15<special_cases>
15<special_cases>
16−
17- Create file: provide file_path + new_string, leave old_string empty
16- Create file: provide file_path + new_string, leave old_string empty
18- Delete content: provide file_path + old_string, leave new_string empty
17- Delete content: provide file_path + old_string, leave new_string empty
19− </special_cases>
18+</special_cases>
20
19
21<critical_requirements>
20<critical_requirements>
22UNIQUENESS (when replace_all=false): old_string MUST uniquely identify target instance
21UNIQUENESS (when replace_all=false): old_string MUST uniquely identify target instance
@@ -34,7 +33,7 @@ VERIFICATION: Before using
34- Check how many instances of target text exist
33- Check how many instances of target text exist
35- Gather sufficient context for unique identification
34- Gather sufficient context for unique identification
36- Plan separate calls or use replace_all
35- Plan separate calls or use replace_all
37− </critical_requirements>
36+</critical_requirements>
38
37
39<warnings>
38<warnings>
40Tool fails if:
39Tool fails if:
@@ -44,17 +43,15 @@ Tool fails if:
44</warnings>
43</warnings>
45
44
46<best_practices>
45<best_practices>
47−
48- Ensure edits result in correct, idiomatic code
46- Ensure edits result in correct, idiomatic code
49- Don't leave code in broken state
47- Don't leave code in broken state
50- Use absolute file paths (starting with /)
48- Use absolute file paths (starting with /)
51- Use forward slashes (/) for cross-platform compatibility
49- Use forward slashes (/) for cross-platform compatibility
52- Multiple edits to same file: send all in single message with multiple tool calls
50- Multiple edits to same file: send all in single message with multiple tool calls
53− </best_practices>
51+</best_practices>
54
52
55<windows_notes>
53<windows_notes>
56−
57- Forward slashes work throughout (C:/path/file)
54- Forward slashes work throughout (C:/path/file)
58- File permissions handled automatically
55- File permissions handled automatically
59- Line endings converted automatically (\n ↔ \r\n)
56- Line endings converted automatically (\n ↔ \r\n)
60− </windows_notes>
57+</windows_notes>
internal/agent/tools/glob.md+2−4
@@ -7,13 +7,12 @@ Fast file pattern matching tool that finds files by name/pattern, returning path
7</usage>
7</usage>
8
8
9<pattern_syntax>
9<pattern_syntax>
10−
11- '\*' matches any sequence of non-separator characters
10- '\*' matches any sequence of non-separator characters
12- '\*\*' matches any sequence including separators
11- '\*\*' matches any sequence including separators
13- '?' matches any single non-separator character
12- '?' matches any single non-separator character
14- '[...]' matches any character in brackets
13- '[...]' matches any character in brackets
15- '[!...]' matches any character not in brackets
14- '[!...]' matches any character not in brackets
16− </pattern_syntax>
15+</pattern_syntax>
17
16
18<examples>
17<examples>
19- '*.js' - JavaScript files in current directory
18- '*.js' - JavaScript files in current directory
@@ -29,11 +28,10 @@ Fast file pattern matching tool that finds files by name/pattern, returning path
29</limitations>
28</limitations>
30
29
31<cross_platform>
30<cross_platform>
32−
33- Path separators handled automatically (/ and \ work)
31- Path separators handled automatically (/ and \ work)
34- Uses ripgrep (rg) if available, otherwise Go implementation
32- Uses ripgrep (rg) if available, otherwise Go implementation
35- Patterns should use forward slashes (/) for compatibility
33- Patterns should use forward slashes (/) for compatibility
36− </cross_platform>
34+</cross_platform>
37
35
38<tips>
36<tips>
39- Combine with Grep: find files with Glob, search contents with Grep
37- Combine with Grep: find files with Glob, search contents with Grep
internal/agent/tools/grep.md+4−7
@@ -14,14 +14,13 @@ When literal_text=false (supports standard regex):
14- 'function' searches for literal text "function"
14- 'function' searches for literal text "function"
15- 'log\..\*Error' finds text starting with "log." and ending with "Error"
15- 'log\..\*Error' finds text starting with "log." and ending with "Error"
16- 'import\s+.\*\s+from' finds import statements in JavaScript/TypeScript
16- 'import\s+.\*\s+from' finds import statements in JavaScript/TypeScript
17− </regex_syntax>
17+</regex_syntax>
18
18
19<include_patterns>
19<include_patterns>
20−
21- '\*.js' - Only search JavaScript files
20- '\*.js' - Only search JavaScript files
22- '\*.{ts,tsx}' - Only search TypeScript files
21- '\*.{ts,tsx}' - Only search TypeScript files
23- '\*.go' - Only search Go files
22- '\*.go' - Only search Go files
24− </include_patterns>
23+</include_patterns>
25
24
26<limitations>
25<limitations>
27- Results limited to 100 files (newest first)
26- Results limited to 100 files (newest first)
@@ -31,18 +30,16 @@ When literal_text=false (supports standard regex):
31</limitations>
30</limitations>
32
31
33<ignore_support>
32<ignore_support>
34−
35- Respects .gitignore patterns to skip ignored files/directories
33- Respects .gitignore patterns to skip ignored files/directories
36- Respects .crushignore patterns for additional ignore rules
34- Respects .crushignore patterns for additional ignore rules
37- Both ignore files auto-detected in search root directory
35- Both ignore files auto-detected in search root directory
38− </ignore_support>
36+</ignore_support>
39
37
40<cross_platform>
38<cross_platform>
41−
42- Uses ripgrep (rg) if available for better performance
39- Uses ripgrep (rg) if available for better performance
43- Falls back to Go implementation if ripgrep unavailable
40- Falls back to Go implementation if ripgrep unavailable
44- File paths normalized automatically for compatibility
41- File paths normalized automatically for compatibility
45− </cross_platform>
42+</cross_platform>
46
43
47<tips>
44<tips>
48- For faster searches: use Glob to find relevant files first, then Grep
45- For faster searches: use Glob to find relevant files first, then Grep
internal/agent/tools/ls.md+1−2
@@ -21,12 +21,11 @@ Shows files and subdirectories in tree structure for exploring project organizat
21</limitations>
21</limitations>
22
22
23<cross_platform>
23<cross_platform>
24−
25- Hidden file detection uses Unix convention (files starting with '.')
24- Hidden file detection uses Unix convention (files starting with '.')
26- Windows hidden files (with hidden attribute) not auto-skipped
25- Windows hidden files (with hidden attribute) not auto-skipped
27- Common Windows directories (System32, Program Files) not in default ignore
26- Common Windows directories (System32, Program Files) not in default ignore
28- Path separators handled automatically (/ and \ work)
27- Path separators handled automatically (/ and \ work)
29− </cross_platform>
28+</cross_platform>
30
29
31<tips>
30<tips>
32- Use Glob for finding files by name patterns instead of browsing
31- Use Glob for finding files by name patterns instead of browsing
internal/agent/tools/multiedit.md+3−6
@@ -21,11 +21,10 @@ Makes multiple edits to a single file in one operation. Built on Edit tool for e
21</operation>
21</operation>
22
22
23<critical_requirements>
23<critical_requirements>
24−
251. All edits follow same requirements as single Edit tool
241. All edits follow same requirements as single Edit tool
262. Edits are atomic - either all succeed or none applied
252. Edits are atomic - either all succeed or none applied
273. Plan edits carefully to avoid conflicts between sequential operations
263. Plan edits carefully to avoid conflicts between sequential operations
28− </critical_requirements>
27+</critical_requirements>
29
28
30<warnings>
29<warnings>
31- Tool fails if old_string doesn't match file contents exactly (including whitespace)
30- Tool fails if old_string doesn't match file contents exactly (including whitespace)
@@ -34,17 +33,15 @@ Makes multiple edits to a single file in one operation. Built on Edit tool for e
34</warnings>
33</warnings>
35
34
36<best_practices>
35<best_practices>
37−
38- Ensure all edits result in correct, idiomatic code
36- Ensure all edits result in correct, idiomatic code
39- Don't leave code in broken state
37- Don't leave code in broken state
40- Use absolute file paths (starting with /)
38- Use absolute file paths (starting with /)
41- Use replace_all for renaming variables across file
39- Use replace_all for renaming variables across file
42- Avoid adding emojis unless user explicitly requests
40- Avoid adding emojis unless user explicitly requests
43− </best_practices>
41+</best_practices>
44
42
45<new_file_creation>
43<new_file_creation>
46−
47- Provide new file path (including directory if needed)
44- Provide new file path (including directory if needed)
48- First edit: empty old_string, new file contents as new_string
45- First edit: empty old_string, new file contents as new_string
49- Subsequent edits: normal edit operations on created content
46- Subsequent edits: normal edit operations on created content
50− </new_file_creation>
47+</new_file_creation>
internal/agent/tools/sourcegraph.md+2−4
@@ -7,7 +7,6 @@ Search code across public repositories using Sourcegraph's GraphQL API.
7</usage>
7</usage>
8
8
9<basic_syntax>
9<basic_syntax>
10−
11- "fmt.Println" - exact matches
10- "fmt.Println" - exact matches
12- "file:.go fmt.Println" - limit to Go files
11- "file:.go fmt.Println" - limit to Go files
13- "repo:^github\.com/golang/go$ fmt.Println" - specific repos
12- "repo:^github\.com/golang/go$ fmt.Println" - specific repos
@@ -16,7 +15,7 @@ Search code across public repositories using Sourcegraph's GraphQL API.
16- "fmt\.(Print|Printf|Println)" - regex patterns
15- "fmt\.(Print|Printf|Println)" - regex patterns
17- "\"exact phrase\"" - exact phrase matching
16- "\"exact phrase\"" - exact phrase matching
18- "-file:test" or "-repo:forks" - exclude matches
17- "-file:test" or "-repo:forks" - exclude matches
19− </basic_syntax>
18+</basic_syntax>
20
19
21<key_filters>
20<key_filters>
22Repository: repo:name, repo:^exact$, repo:org/repo@branch, -repo:exclude, fork:yes, archived:yes, visibility:public
21Repository: repo:name, repo:^exact$, repo:org/repo@branch, -repo:exclude, fork:yes, archived:yes, visibility:public
@@ -35,12 +34,11 @@ Result: select:repo, select:file, select:content, count:100, timeout:30s
35</examples>
34</examples>
36
35
37<boolean_operators>
36<boolean_operators>
38−
39- "term1 AND term2" - both terms
37- "term1 AND term2" - both terms
40- "term1 OR term2" - either term
38- "term1 OR term2" - either term
41- "term1 NOT term2" - term1 but not term2
39- "term1 NOT term2" - term1 but not term2
42- "term1 and (term2 or term3)" - grouping with parentheses
40- "term1 and (term2 or term3)" - grouping with parentheses
43− </boolean_operators>
41+</boolean_operators>
44
42
45<limitations>
43<limitations>
46- Only searches public repositories
44- Only searches public repositories
internal/agent/tools/tools.go+13
@@ -11,6 +11,7 @@ type (
11
11
12const (
12const (
13 SessionIDContextKey sessionIDContextKey = "session_id"
13 SessionIDContextKey sessionIDContextKey = "session_id"
14+ MessageIDContextKey messageIDContextKey = "message_id"
14)
15)
15
16
16func GetSessionFromContext(ctx context.Context) string {
17func GetSessionFromContext(ctx context.Context) string {
@@ -24,3 +25,15 @@ func GetSessionFromContext(ctx context.Context) string {
24 }
25 }
25 return s
26 return s
26}
27}
28+
29+func GetMessageFromContext(ctx context.Context) string {
30+ messageID := ctx.Value(MessageIDContextKey)
31+ if messageID == nil {
32+ return ""
33+ }
34+ s, ok := messageID.(string)
35+ if !ok {
36+ return ""
37+ }
38+ return s
39+}
internal/agent/tools/view.md+1−2
@@ -23,11 +23,10 @@ Reads and displays file contents with line numbers for examining code, logs, or
23</limitations>
23</limitations>
24
24
25<cross_platform>
25<cross_platform>
26−
27- Handles Windows (CRLF) and Unix (LF) line endings
26- Handles Windows (CRLF) and Unix (LF) line endings
28- Works with forward slashes (/) and backslashes (\)
27- Works with forward slashes (/) and backslashes (\)
29- Auto-detects text encoding for common formats
28- Auto-detects text encoding for common formats
30− </cross_platform>
29+</cross_platform>
31
30
32<tips>
31<tips>
33- Use with Glob to find files first
32- Use with Glob to find files first
internal/agent/tools/write.md+1−2
@@ -19,9 +19,8 @@ Creates or updates files in filesystem for saving/modifying text content.
19</limitations>
19</limitations>
20
20
21<cross_platform>
21<cross_platform>
22−
23- Use forward slashes (/) for compatibility
22- Use forward slashes (/) for compatibility
24− </cross_platform>
23+</cross_platform>
25
24
26<tips>
25<tips>
27- Use View tool first to examine existing files before modifying
26- Use View tool first to examine existing files before modifying
internal/session/session.go+27
@@ -3,6 +3,8 @@ package session
3import (
3import (
4 "context"
4 "context"
5 "database/sql"
5 "database/sql"
6+ "fmt"
7+ "strings"
6
8
7 "github.com/charmbracelet/crush/internal/db"
9 "github.com/charmbracelet/crush/internal/db"
8 "github.com/charmbracelet/crush/internal/event"
10 "github.com/charmbracelet/crush/internal/event"
@@ -32,6 +34,11 @@ type Service interface {
32 List(ctx context.Context) ([]Session, error)
34 List(ctx context.Context) ([]Session, error)
33 Save(ctx context.Context, session Session) (Session, error)
35 Save(ctx context.Context, session Session) (Session, error)
34 Delete(ctx context.Context, id string) error
36 Delete(ctx context.Context, id string) error
37+
38+ // Agent tool session management
39+ CreateAgentToolSessionID(messageID, toolCallID string) string
40+ ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool)
41+ IsAgentToolSession(sessionID string) bool
35}
42}
36
43
37type service struct {
44type service struct {
@@ -157,3 +164,23 @@ func NewService(q db.Querier) Service {
157 q,
164 q,
158 }
165 }
159}
166}
167+
168+// CreateAgentToolSessionID creates a session ID for agent tool sessions using the format "messageID$$toolCallID"
169+func (s *service) CreateAgentToolSessionID(messageID, toolCallID string) string {
170+ return fmt.Sprintf("%s$$%s", messageID, toolCallID)
171+}
172+
173+// ParseAgentToolSessionID parses an agent tool session ID into its components
174+func (s *service) ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool) {
175+ parts := strings.Split(sessionID, "$$")
176+ if len(parts) != 2 {
177+ return "", "", false
178+ }
179+ return parts[0], parts[1], true
180+}
181+
182+// IsAgentToolSession checks if a session ID follows the agent tool session format
183+func (s *service) IsAgentToolSession(sessionID string) bool {
184+ _, _, ok := s.ParseAgentToolSessionID(sessionID)
185+ return ok
186+}
internal/tui/components/chat/chat.go+10−2
@@ -261,12 +261,19 @@ func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message])
261 if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 {
261 if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 {
262 return nil
262 return nil
263 }
263 }
264+
265+ // Check if this is an agent tool session and parse it
266+ childSessionID := event.Payload.SessionID
267+ parentMessageID, toolCallID, ok := m.app.Sessions.ParseAgentToolSessionID(childSessionID)
268+ if !ok {
269+ return nil
270+ }
264 items := m.listCmp.Items()
271 items := m.listCmp.Items()
265 toolCallInx := NotFound
272 toolCallInx := NotFound
266 var toolCall messages.ToolCallCmp
273 var toolCall messages.ToolCallCmp
267 for i := len(items) - 1; i >= 0; i-- {
274 for i := len(items) - 1; i >= 0; i-- {
268 if msg, ok := items[i].(messages.ToolCallCmp); ok {
275 if msg, ok := items[i].(messages.ToolCallCmp); ok {
269− if msg.GetToolCall().ID == event.Payload.SessionID {
276+ if msg.ParentMessageID() == parentMessageID && msg.GetToolCall().ID == toolCallID {
270 toolCallInx = i
277 toolCallInx = i
271 toolCall = msg
278 toolCall = msg
272 }
279 }
@@ -613,7 +620,8 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
613 uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions, options...))
620 uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions, options...))
614 // If this tool call is the agent tool, fetch nested tool calls
621 // If this tool call is the agent tool, fetch nested tool calls
615 if tc.Name == agent.AgentToolName {
622 if tc.Name == agent.AgentToolName {
623+ agentToolSessionID := m.app.Sessions.CreateAgentToolSessionID(msg.ID, tc.ID)
616− nestedMessages, _ := m.app.Messages.List(context.Background(), tc.ID)
624+ nestedMessages, _ := m.app.Messages.List(context.Background(), agentToolSessionID)
617 nestedToolResultMap := m.buildToolResultMap(nestedMessages)
625 nestedToolResultMap := m.buildToolResultMap(nestedMessages)
618 nestedUIMessages := m.convertMessagesToUI(nestedMessages, nestedToolResultMap)
626 nestedUIMessages := m.convertMessagesToUI(nestedMessages, nestedToolResultMap)
619 nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages))
627 nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages))
internal/tui/components/chat/sidebar/sidebar.go+7−7
@@ -563,13 +563,6 @@ func (s *sidebarCmp) currentModelBlock() string {
563 if model.CanReason {
563 if model.CanReason {
564 reasoningInfoStyle := t.S().Subtle.PaddingLeft(2)
564 reasoningInfoStyle := t.S().Subtle.PaddingLeft(2)
565 switch modelProvider.Type {
565 switch modelProvider.Type {
566− case catwalk.TypeOpenAI:
567− reasoningEffort := model.DefaultReasoningEffort
568− if selectedModel.ReasoningEffort != "" {
569− reasoningEffort = selectedModel.ReasoningEffort
570− }
571− formatter := cases.Title(language.English, cases.NoLower)
572− parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
573 case catwalk.TypeAnthropic:
566 case catwalk.TypeAnthropic:
574 formatter := cases.Title(language.English, cases.NoLower)
567 formatter := cases.Title(language.English, cases.NoLower)
575 if selectedModel.Think {
568 if selectedModel.Think {
@@ -577,6 +570,13 @@ func (s *sidebarCmp) currentModelBlock() string {
577 } else {
570 } else {
578 parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking off")))
571 parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking off")))
579 }
572 }
573+ default:
574+ reasoningEffort := model.DefaultReasoningEffort
575+ if selectedModel.ReasoningEffort != "" {
576+ reasoningEffort = selectedModel.ReasoningEffort
577+ }
578+ formatter := cases.Title(language.English, cases.NoLower)
579+ parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
580 }
580 }
581 }
581 }
582 if s.session.ID != "" {
582 if s.session.ID != "" {
internal/tui/components/dialogs/commands/commands.go+1−1
@@ -326,7 +326,7 @@ func (c *commandDialogCmp) defaultCommands() []Command {
326 }
326 }
327
327
328 // OpenAI models: reasoning effort dialog
328 // OpenAI models: reasoning effort dialog
329− if providerCfg.Type == catwalk.TypeOpenAI && model.HasReasoningEffort {
329+ if model.HasReasoningEffort {
330 commands = append(commands, Command{
330 commands = append(commands, Command{
331 ID: "select_reasoning_effort",
331 ID: "select_reasoning_effort",
332 Title: "Select Reasoning Effort",
332 Title: "Select Reasoning Effort",
internal/tui/page/chat/chat.go+1−3
@@ -9,7 +9,6 @@ import (
9 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/key"
10 "github.com/charmbracelet/bubbles/v2/spinner"
10 "github.com/charmbracelet/bubbles/v2/spinner"
11 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
12− "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/app"
12 "github.com/charmbracelet/crush/internal/app"
14 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/history"
14 "github.com/charmbracelet/crush/internal/history"
@@ -563,8 +562,7 @@ func (p *chatPage) openReasoningDialog() tea.Cmd {
563 model := cfg.GetModelByType(agentCfg.Model)
562 model := cfg.GetModelByType(agentCfg.Model)
564 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
563 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
565
564
566− if providerCfg != nil && model != nil &&
565+ if providerCfg != nil && model != nil && model.HasReasoningEffort {
567− providerCfg.Type == catwalk.TypeOpenAI && model.HasReasoningEffort {
568 // Return the OpenDialogMsg directly so it bubbles up to the main TUI
566 // Return the OpenDialogMsg directly so it bubbles up to the main TUI
569 return dialogs.OpenDialogMsg{
567 return dialogs.OpenDialogMsg{
570 Model: reasoning.NewReasoningDialog(),
568 Model: reasoning.NewReasoningDialog(),