From b45dcfe41fa16475d58eb8c4fd73a9e3829d6698 Mon Sep 17 00:00:00 2001 From: kujtimiihoxha Date: Fri, 3 Oct 2025 10:42:13 +0200 Subject: [PATCH] refactor: agent tool --- .gitignore | 1 + go.mod | 2 +- go.sum | 6 +++++ internal/agent/agent.go | 11 ++++---- .../agent/{agent-tool.go => agent_tool.go} | 12 ++++++--- internal/agent/common_test.go | 19 ------------- internal/agent/coordinator.go | 2 -- internal/agent/prompt/prompt.go | 10 +++---- internal/agent/prompts.go | 4 +-- .../templates/{agentTool.md => agent_tool.md} | 7 +++-- .../templates/{coder.gotmpl => coder.md.tpl} | 20 ++++++++------ internal/agent/templates/initialize.md | 2 +- .../templates/{task.gotmpl => task.md.tpl} | 0 internal/agent/templates/title.md | 2 ++ internal/agent/tools/bash.go | 2 +- .../agent/tools/{bash.gotmpl => bash.tpl} | 2 +- internal/agent/tools/edit.md | 11 +++----- internal/agent/tools/glob.md | 6 ++--- internal/agent/tools/grep.md | 11 +++----- internal/agent/tools/ls.md | 3 +-- internal/agent/tools/multiedit.md | 9 +++---- internal/agent/tools/sourcegraph.md | 6 ++--- internal/agent/tools/tools.go | 13 +++++++++ internal/agent/tools/view.md | 3 +-- internal/agent/tools/write.md | 3 +-- internal/session/session.go | 27 +++++++++++++++++++ internal/tui/components/chat/chat.go | 12 +++++++-- .../tui/components/chat/sidebar/sidebar.go | 14 +++++----- .../components/dialogs/commands/commands.go | 2 +- internal/tui/page/chat/chat.go | 4 +-- 30 files changed, 127 insertions(+), 99 deletions(-) rename internal/agent/{agent-tool.go => agent_tool.go} (86%) rename internal/agent/templates/{agentTool.md => agent_tool.md} (98%) rename internal/agent/templates/{coder.gotmpl => coder.md.tpl} (90%) rename internal/agent/templates/{task.gotmpl => task.md.tpl} (100%) rename internal/agent/tools/{bash.gotmpl => bash.tpl} (99%) diff --git a/.gitignore b/.gitignore index 1bd8e7f96d876b03ce3711854b5a050c1419b0e5..c9561513b4f6e5f301aaea4a2bf99a0abde4f00d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ Thumbs.db manpages/ completions/ !internal/tui/components/completions/ +.prettierignore diff --git a/go.mod b/go.mod index 51abfa8ad2d4b58613d45dc809be8d3f558bed38..6db70ed4cd386fa1f4cf216a7a366cd33476067e 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( require ( github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect - github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77 + github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d github.com/go-viper/mapstructure/v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e8f33b513a5d42b6441404d5ced3913c6e124746..9817cc105873a0084fb76dd341fb7d59f76f8d27 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,12 @@ github.com/charmbracelet/fang v0.4.2 h1:nWr7Tb82/TTNNGMGG35aTZ1X68loAOQmpb0qxkKX github.com/charmbracelet/fang v0.4.2/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg= github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77 h1:YHuUqaojkeu00YtQeXPqM/1RNJH/jqGNaQYFwa7JQTk= github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw= +github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c h1:+gCA3Sv7g1jnZ96Em7j9u61H2O/1SkBAZ1LM9yd2bM8= +github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw= +github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380 h1:UYWO3cutUTV9ZLOH4hrXFy9hg4E1pVbLZsZLA1OA3+g= +github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw= +github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d h1:HS9qu7CgQGPnPcNoHIZXGWGFS8SaxaBXYytLdakm+jY= +github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw= github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w= github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk= github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 38ded19e1e17ccdc3d3f38c7734e77fef07d7159..ce197465fff9696241536b16cf3d0c3ce9508852 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -175,7 +175,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen TopK: call.TopK, FrequencyPenalty: call.FrequencyPenalty, // Before each step create the new assistant message - PrepareStep: func(options ai.PrepareStepFunctionOptions) (prepared ai.PrepareStepResult, err error) { + PrepareStep: func(callContext context.Context, options ai.PrepareStepFunctionOptions) (_ context.Context, prepared ai.PrepareStepResult, err error) { var assistantMsg message.Message assistantMsg, err = a.messages.Create(genCtx, call.SessionID, message.CreateMessageParams{ Role: message.Assistant, @@ -184,9 +184,11 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen Provider: a.largeModel.ModelCfg.Provider, }) if err != nil { - return prepared, err + return callContext, prepared, err } + callContext = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID) + currentAssistant = &assistantMsg prepared.Messages = options.Messages @@ -200,7 +202,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen for _, queued := range queuedCalls { userMessage, createErr := a.createUserMessage(genCtx, queued) if createErr != nil { - return prepared, createErr + return callContext, prepared, createErr } prepared.Messages = append(prepared.Messages, userMessage.ToAIMessage()...) } @@ -220,7 +222,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen prepared.Messages[i].ProviderOptions = a.getCacheControlOptions() } } - return prepared, err + return callContext, prepared, err }, OnReasoningDelta: func(id string, text string) error { currentAssistant.AppendReasoningContent(text) @@ -307,7 +309,6 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen case ai.FinishReasonToolCalls: finishReason = message.FinishReasonToolUse } - slog.Info("OnStepFinish", "reason", stepResult.FinishReason) currentAssistant.AddFinish(finishReason, "", "") a.updateSessionUsage(a.largeModel, ¤tSession, stepResult.Usage) sessionLock.Lock() diff --git a/internal/agent/agent-tool.go b/internal/agent/agent_tool.go similarity index 86% rename from internal/agent/agent-tool.go rename to internal/agent/agent_tool.go index 82ba6ef41f5144117e0f8bff12104b97d287da5f..23defbffb1606b3fa0c1fe3020ca84f6216b8183 100644 --- a/internal/agent/agent-tool.go +++ b/internal/agent/agent_tool.go @@ -14,7 +14,7 @@ import ( "github.com/charmbracelet/crush/internal/config" ) -//go:embed templates/agentTool.md +//go:embed templates/agent_tool.md var agentToolDescription []byte type AgentParams struct { @@ -55,7 +55,13 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) { return ai.ToolResponse{}, errors.New("session id missing from context") } - session, err := c.sessions.CreateTaskSession(ctx, call.ID, sessionID, "New Agent Session") + agentMessageID := tools.GetMessageFromContext(ctx) + if agentMessageID == "" { + return ai.ToolResponse{}, errors.New("agent message id missing from context") + } + + agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, call.ID) + session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, "New Agent Session") if err != nil { return ai.ToolResponse{}, fmt.Errorf("error creating session: %s", err) } @@ -65,7 +71,7 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) { maxTokens = model.ModelCfg.MaxTokens } result, err := agent.Run(ctx, SessionAgentCall{ - SessionID: sessionID, + SessionID: session.ID, Prompt: params.Prompt, MaxOutputTokens: maxTokens, ProviderOptions: c.getProviderOptions(model), diff --git a/internal/agent/common_test.go b/internal/agent/common_test.go index 558dad9bb0158c3d2374fe10ca002906b8567b80..c81939a17ee825f82afdf8a6e99f1416c9facb48 100644 --- a/internal/agent/common_test.go +++ b/internal/agent/common_test.go @@ -1,7 +1,6 @@ package agent import ( - "fmt" "net/http" "os" "path/filepath" @@ -69,18 +68,9 @@ func openaiBuilder(model string) builderFunc { func openRouterBuilder(model string) builderFunc { return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) { - tf := func() func() string { - id := 0 - return func() string { - id += 1 - return fmt.Sprintf("%s-%d", t.Name(), id) - } - } provider := openrouter.New( openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")), openrouter.WithHTTPClient(&http.Client{Transport: r}), - openrouter.WithLanguageUniqueToolCallIds(), - openrouter.WithLanguageModelGenerateIDFunc(tf()), ) return provider.LanguageModel(model) } @@ -88,19 +78,10 @@ func openRouterBuilder(model string) builderFunc { func zAIBuilder(model string) builderFunc { return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) { - tf := func() func() string { - id := 0 - return func() string { - id += 1 - return fmt.Sprintf("%s-%d", t.Name(), id) - } - } provider := openaicompat.New( "https://api.z.ai/api/coding/paas/v4", openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")), openaicompat.WithHTTPClient(&http.Client{Transport: r}), - openaicompat.WithLanguageUniqueToolCallIds(), - openaicompat.WithLanguageModelGenerateIDFunc(tf()), ) return provider.LanguageModel(model) } diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index ac56bd56baebdda471decf0f6e64e5d431d27e66..78b153bc341f293beba8cd33d83470f0a8322d9a 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -356,7 +356,6 @@ func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[st func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) ai.Provider { opts := []openrouter.Option{ openrouter.WithAPIKey(apiKey), - openrouter.WithLanguageUniqueToolCallIds(), } if c.cfg.Options.Debug { httpClient := log.NewHTTPClient() @@ -371,7 +370,6 @@ func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[stri func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) ai.Provider { opts := []openaicompat.Option{ openaicompat.WithAPIKey(apiKey), - openaicompat.WithLanguageUniqueToolCallIds(), } if c.cfg.Options.Debug { httpClient := log.NewHTTPClient() diff --git a/internal/agent/prompt/prompt.go b/internal/agent/prompt/prompt.go index eb2d1f461481aa46ef7f7a60833f38f2096bf1b6..1c6188cca4b29592c34dc8715defc3503d7ce87c 100644 --- a/internal/agent/prompt/prompt.go +++ b/internal/agent/prompt/prompt.go @@ -15,11 +15,11 @@ import ( // Prompt represents a template-based prompt generator. type Prompt struct { - name string - template string - now func() time.Time - platform string - workingDir string + name string + template string + now func() time.Time + platform string + workingDir string } type PromptDat struct { diff --git a/internal/agent/prompts.go b/internal/agent/prompts.go index f0f430e60e8bf35b0ab26bf4e62fc67f26d4b6f0..01be6561a9e74c71eba78991f942c77a68d8d879 100644 --- a/internal/agent/prompts.go +++ b/internal/agent/prompts.go @@ -6,10 +6,10 @@ import ( "github.com/charmbracelet/crush/internal/agent/prompt" ) -//go:embed templates/coder.gotmpl +//go:embed templates/coder.md.tpl var coderPromptTmpl []byte -//go:embed templates/task.gotmpl +//go:embed templates/task.md.tpl var taskPromptTmpl []byte //go:embed templates/initialize.md diff --git a/internal/agent/templates/agentTool.md b/internal/agent/templates/agent_tool.md similarity index 98% rename from internal/agent/templates/agentTool.md rename to internal/agent/templates/agent_tool.md index 50a98afec87653609c9fb7c523a60a46220c6c92..dd5f8f46cc0b66fb2e4f8fd73bf3059952b8f083 100644 --- a/internal/agent/templates/agentTool.md +++ b/internal/agent/templates/agent_tool.md @@ -1,16 +1,15 @@ Launch 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. - + - 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 - 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 - If you are searching for a specific class definition like "class Foo", use the GlobTool tool instead, to find the match more quickly - + - 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses 2. 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. 3. 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. 4. The agent's outputs should generally be trusted 5. 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. - + diff --git a/internal/agent/templates/coder.gotmpl b/internal/agent/templates/coder.md.tpl similarity index 90% rename from internal/agent/templates/coder.gotmpl rename to internal/agent/templates/coder.md.tpl index b4bacd7a9cfe187e763323871eaae5f520329a77..c03700f35c4597e973537e774f4efba02cee4598 100644 --- a/internal/agent/templates/coder.gotmpl +++ b/internal/agent/templates/coder.md.tpl @@ -2,18 +2,19 @@ You are Crush, a powerful AI Assistant that runs in the CLI. Use the instructions below and the tools available to you to assist the user. -If the current working directory contains a file called CRUSH.md, it will be automatically added to your context. +If the current working directory contains a file used for memory, they will be automatically added to your context. -This file serves multiple purposes: +These file serves multiple purposes: - Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time - Recording the user's code style preferences (naming conventions, preferred libraries, etc.) - Maintaining useful information about the codebase structure and organization -When you discover important information that could be useful for the future update/add the info to the CRUSH.md. +When you discover important information that could be useful for the future update/add the info the appropriate memory file. -Memory might be added to you during a task if there are nested memory files that relate to the work you are doing. +Make sure to follow the memory files instructions while working. + - Be concise and direct - Keep responses under 4 lines unless details requested @@ -65,6 +66,7 @@ assistant: src/foo.c user: write tests for new feature assistant: [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] + @@ -86,15 +88,17 @@ When making changes to files, first understand the file's code conventions. Mimi -- Follow existing code style and patterns -- Do not add any comments to code you write unless asked to do so +- Follow existing code style and patterns. +- Do not add any comments to code you write unless asked to do so. +- Thrive to write only code that is necessary to solve the given issue (less code is always better). +- Follow best practices for the language and framework used in the project. The 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: - Use the available search tools to understand the codebase and the user's query. -- Plan out the implementation +- Plan out the implementation (create a todo list) - Implement the solution using all tools available to you - 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. - 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 -- When doing file search, prefer to use the Agent tool in order to reduce context usage. +- When doing file search, prefer to use the Agent tool, give the agent detailed instructions on what to search for and response format details. - 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). - 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. diff --git a/internal/agent/templates/initialize.md b/internal/agent/templates/initialize.md index edada7f77b9c497873d71b6f5b1fd12fbeef387f..a4d7500251ff8fb7156bbb962c98264ca3e62ed8 100644 --- a/internal/agent/templates/initialize.md +++ b/internal/agent/templates/initialize.md @@ -1,4 +1,4 @@ -`Please analyze this codebase and create a **CRUSH.md** file containing: +Please analyze this codebase and create a **CRUSH.md** file containing: - Build/lint/test commands - especially for running a single test - Code style guidelines including imports, formatting, types, naming conventions, error handling, etc. diff --git a/internal/agent/templates/task.gotmpl b/internal/agent/templates/task.md.tpl similarity index 100% rename from internal/agent/templates/task.gotmpl rename to internal/agent/templates/task.md.tpl diff --git a/internal/agent/templates/title.md b/internal/agent/templates/title.md index 6da44069787ce6e5d69a6bb9f24b3dc5caa3782f..aaf2a49edaa965710269a22a70e23fbbb644934e 100644 --- a/internal/agent/templates/title.md +++ b/internal/agent/templates/title.md @@ -1,8 +1,10 @@ you will generate a short title based on the first message a user begins a conversation with + - ensure it is not more than 50 characters long - the title should be a summary of the user's message - it should be one line long - do not use quotes or colons - the entire text you return will be used as the title - never return anything that is more than one sentence (one line) long + diff --git a/internal/agent/tools/bash.go b/internal/agent/tools/bash.go index 72c8c2261ccab6bea19781f4db0164a5759a91ea..f125d5ff1ca8262db99a10e5e895f04dc8fceef9 100644 --- a/internal/agent/tools/bash.go +++ b/internal/agent/tools/bash.go @@ -44,7 +44,7 @@ const ( BashNoOutput = "no output" ) -//go:embed bash.gotmpl +//go:embed bash.tpl var bashDescriptionTmpl []byte var bashDescriptionTpl = template.Must( diff --git a/internal/agent/tools/bash.gotmpl b/internal/agent/tools/bash.tpl similarity index 99% rename from internal/agent/tools/bash.gotmpl rename to internal/agent/tools/bash.tpl index 5c97f24696cf2f358b54f52c6c9fbf24b5904aea..05b34517a0743c6c185734ac04b99677539c7307 100644 --- a/internal/agent/tools/bash.gotmpl +++ b/internal/agent/tools/bash.tpl @@ -108,7 +108,7 @@ Important: - Return empty response - user sees gh output - Never update git config - + Good: pytest /foo/bar/tests diff --git a/internal/agent/tools/edit.md b/internal/agent/tools/edit.md index 9c8dc946beda59d7bb74bc12b318f12ff5d1eaad..33caca553b5014e21d7cb146bc20037e7955470c 100644 --- a/internal/agent/tools/edit.md +++ b/internal/agent/tools/edit.md @@ -13,10 +13,9 @@ Edits files by replacing text, creating new files, or deleting content. For movi - - Create file: provide file_path + new_string, leave old_string empty - Delete content: provide file_path + old_string, leave new_string empty - + UNIQUENESS (when replace_all=false): old_string MUST uniquely identify target instance @@ -34,7 +33,7 @@ VERIFICATION: Before using - Check how many instances of target text exist - Gather sufficient context for unique identification - Plan separate calls or use replace_all - + Tool fails if: @@ -44,17 +43,15 @@ Tool fails if: - - Ensure edits result in correct, idiomatic code - Don't leave code in broken state - Use absolute file paths (starting with /) - Use forward slashes (/) for cross-platform compatibility - Multiple edits to same file: send all in single message with multiple tool calls - + - - Forward slashes work throughout (C:/path/file) - File permissions handled automatically - Line endings converted automatically (\n ↔ \r\n) - + diff --git a/internal/agent/tools/glob.md b/internal/agent/tools/glob.md index cee5e7ba242cdf6f87cfb0047b5ea99520d6e64f..bce7223cdda9b99495f65e9adf07c0199e6323ac 100644 --- a/internal/agent/tools/glob.md +++ b/internal/agent/tools/glob.md @@ -7,13 +7,12 @@ Fast file pattern matching tool that finds files by name/pattern, returning path - - '\*' matches any sequence of non-separator characters - '\*\*' matches any sequence including separators - '?' matches any single non-separator character - '[...]' matches any character in brackets - '[!...]' matches any character not in brackets - + - '*.js' - JavaScript files in current directory @@ -29,11 +28,10 @@ Fast file pattern matching tool that finds files by name/pattern, returning path - - Path separators handled automatically (/ and \ work) - Uses ripgrep (rg) if available, otherwise Go implementation - Patterns should use forward slashes (/) for compatibility - + - Combine with Grep: find files with Glob, search contents with Grep diff --git a/internal/agent/tools/grep.md b/internal/agent/tools/grep.md index b3af96c6b6419e3365bea13629a89f13690dd5cc..2fe104ba4a7cf006d21c7b531f5ff9af85874979 100644 --- a/internal/agent/tools/grep.md +++ b/internal/agent/tools/grep.md @@ -14,14 +14,13 @@ When literal_text=false (supports standard regex): - 'function' searches for literal text "function" - 'log\..\*Error' finds text starting with "log." and ending with "Error" - 'import\s+.\*\s+from' finds import statements in JavaScript/TypeScript - + - - '\*.js' - Only search JavaScript files - '\*.{ts,tsx}' - Only search TypeScript files - '\*.go' - Only search Go files - + - Results limited to 100 files (newest first) @@ -31,18 +30,16 @@ When literal_text=false (supports standard regex): - - Respects .gitignore patterns to skip ignored files/directories - Respects .crushignore patterns for additional ignore rules - Both ignore files auto-detected in search root directory - + - - Uses ripgrep (rg) if available for better performance - Falls back to Go implementation if ripgrep unavailable - File paths normalized automatically for compatibility - + - For faster searches: use Glob to find relevant files first, then Grep diff --git a/internal/agent/tools/ls.md b/internal/agent/tools/ls.md index b29525df9c408b00c16b68c867884eaab2113cd8..ba4fd7462dfefd540fc9e33b672a73f6e412ca7a 100644 --- a/internal/agent/tools/ls.md +++ b/internal/agent/tools/ls.md @@ -21,12 +21,11 @@ Shows files and subdirectories in tree structure for exploring project organizat - - Hidden file detection uses Unix convention (files starting with '.') - Windows hidden files (with hidden attribute) not auto-skipped - Common Windows directories (System32, Program Files) not in default ignore - Path separators handled automatically (/ and \ work) - + - Use Glob for finding files by name patterns instead of browsing diff --git a/internal/agent/tools/multiedit.md b/internal/agent/tools/multiedit.md index aba52dbe8f0d67279f212b8994a29bede8607530..8efb96d994ddd4537f5858630cc23c3b22bcd4bd 100644 --- a/internal/agent/tools/multiedit.md +++ b/internal/agent/tools/multiedit.md @@ -21,11 +21,10 @@ Makes multiple edits to a single file in one operation. Built on Edit tool for e - 1. All edits follow same requirements as single Edit tool 2. Edits are atomic - either all succeed or none applied 3. Plan edits carefully to avoid conflicts between sequential operations - + - 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 - - Ensure all edits result in correct, idiomatic code - Don't leave code in broken state - Use absolute file paths (starting with /) - Use replace_all for renaming variables across file - Avoid adding emojis unless user explicitly requests - + - - Provide new file path (including directory if needed) - First edit: empty old_string, new file contents as new_string - Subsequent edits: normal edit operations on created content - + diff --git a/internal/agent/tools/sourcegraph.md b/internal/agent/tools/sourcegraph.md index 8ed51564c95f74e381a7c6cb62034ed3e5c3c47e..6e0e3fb7f55256491a716e0cbcb02ad567c73635 100644 --- a/internal/agent/tools/sourcegraph.md +++ b/internal/agent/tools/sourcegraph.md @@ -7,7 +7,6 @@ Search code across public repositories using Sourcegraph's GraphQL API. - - "fmt.Println" - exact matches - "file:.go fmt.Println" - limit to Go files - "repo:^github\.com/golang/go$ fmt.Println" - specific repos @@ -16,7 +15,7 @@ Search code across public repositories using Sourcegraph's GraphQL API. - "fmt\.(Print|Printf|Println)" - regex patterns - "\"exact phrase\"" - exact phrase matching - "-file:test" or "-repo:forks" - exclude matches - + Repository: 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 - - "term1 AND term2" - both terms - "term1 OR term2" - either term - "term1 NOT term2" - term1 but not term2 - "term1 and (term2 or term3)" - grouping with parentheses - + - Only searches public repositories diff --git a/internal/agent/tools/tools.go b/internal/agent/tools/tools.go index 4c15db3f689d6fc570cf5b1d9c613ef2fa4ecb23..365d7a33e689189096611adb9c7af7bfc76bce75 100644 --- a/internal/agent/tools/tools.go +++ b/internal/agent/tools/tools.go @@ -11,6 +11,7 @@ type ( const ( SessionIDContextKey sessionIDContextKey = "session_id" + MessageIDContextKey messageIDContextKey = "message_id" ) func GetSessionFromContext(ctx context.Context) string { @@ -24,3 +25,15 @@ func GetSessionFromContext(ctx context.Context) string { } return s } + +func GetMessageFromContext(ctx context.Context) string { + messageID := ctx.Value(MessageIDContextKey) + if messageID == nil { + return "" + } + s, ok := messageID.(string) + if !ok { + return "" + } + return s +} diff --git a/internal/agent/tools/view.md b/internal/agent/tools/view.md index c7c430ea682caad813d7e1e3386d574454317447..8a71559c2c876f0225dae3c00463ff879cb1b48c 100644 --- a/internal/agent/tools/view.md +++ b/internal/agent/tools/view.md @@ -23,11 +23,10 @@ Reads and displays file contents with line numbers for examining code, logs, or - - Handles Windows (CRLF) and Unix (LF) line endings - Works with forward slashes (/) and backslashes (\) - Auto-detects text encoding for common formats - + - Use with Glob to find files first diff --git a/internal/agent/tools/write.md b/internal/agent/tools/write.md index 2fddf1451d130fadf49b1d39b9da6d004cec585b..6cdf0351d12563a688984f1047f2052a504cf3bd 100644 --- a/internal/agent/tools/write.md +++ b/internal/agent/tools/write.md @@ -19,9 +19,8 @@ Creates or updates files in filesystem for saving/modifying text content. - - Use forward slashes (/) for compatibility - + - Use View tool first to examine existing files before modifying diff --git a/internal/session/session.go b/internal/session/session.go index f83f66ffa4d1cfb75c6a0d41f09caebcb1c64cf3..a1818970e71fcedfd755327a97c64a8baeab43e6 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -3,6 +3,8 @@ package session import ( "context" "database/sql" + "fmt" + "strings" "github.com/charmbracelet/crush/internal/db" "github.com/charmbracelet/crush/internal/event" @@ -32,6 +34,11 @@ type Service interface { List(ctx context.Context) ([]Session, error) Save(ctx context.Context, session Session) (Session, error) Delete(ctx context.Context, id string) error + + // Agent tool session management + CreateAgentToolSessionID(messageID, toolCallID string) string + ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool) + IsAgentToolSession(sessionID string) bool } type service struct { @@ -157,3 +164,23 @@ func NewService(q db.Querier) Service { q, } } + +// CreateAgentToolSessionID creates a session ID for agent tool sessions using the format "messageID$$toolCallID" +func (s *service) CreateAgentToolSessionID(messageID, toolCallID string) string { + return fmt.Sprintf("%s$$%s", messageID, toolCallID) +} + +// ParseAgentToolSessionID parses an agent tool session ID into its components +func (s *service) ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool) { + parts := strings.Split(sessionID, "$$") + if len(parts) != 2 { + return "", "", false + } + return parts[0], parts[1], true +} + +// IsAgentToolSession checks if a session ID follows the agent tool session format +func (s *service) IsAgentToolSession(sessionID string) bool { + _, _, ok := s.ParseAgentToolSessionID(sessionID) + return ok +} diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 1d97915e11f8f98e1545f5b4b186fb032904c3fb..1643af9549a6ff41c0940dd067033d4e776a9eae 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -261,12 +261,19 @@ func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 { return nil } + + // Check if this is an agent tool session and parse it + childSessionID := event.Payload.SessionID + parentMessageID, toolCallID, ok := m.app.Sessions.ParseAgentToolSessionID(childSessionID) + if !ok { + return nil + } items := m.listCmp.Items() toolCallInx := NotFound var toolCall messages.ToolCallCmp for i := len(items) - 1; i >= 0; i-- { if msg, ok := items[i].(messages.ToolCallCmp); ok { - if msg.GetToolCall().ID == event.Payload.SessionID { + if msg.ParentMessageID() == parentMessageID && msg.GetToolCall().ID == toolCallID { toolCallInx = i toolCall = msg } @@ -613,7 +620,8 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions, options...)) // If this tool call is the agent tool, fetch nested tool calls if tc.Name == agent.AgentToolName { - nestedMessages, _ := m.app.Messages.List(context.Background(), tc.ID) + agentToolSessionID := m.app.Sessions.CreateAgentToolSessionID(msg.ID, tc.ID) + nestedMessages, _ := m.app.Messages.List(context.Background(), agentToolSessionID) nestedToolResultMap := m.buildToolResultMap(nestedMessages) nestedUIMessages := m.convertMessagesToUI(nestedMessages, nestedToolResultMap) nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages)) diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index 7b32cda78bde99ea92ec4472a0e817521b748c34..f813862aa927bb3e44c2eb878b302facd01ab400 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -563,13 +563,6 @@ func (s *sidebarCmp) currentModelBlock() string { if model.CanReason { reasoningInfoStyle := t.S().Subtle.PaddingLeft(2) switch modelProvider.Type { - case catwalk.TypeOpenAI: - reasoningEffort := model.DefaultReasoningEffort - if selectedModel.ReasoningEffort != "" { - reasoningEffort = selectedModel.ReasoningEffort - } - formatter := cases.Title(language.English, cases.NoLower) - parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort)))) case catwalk.TypeAnthropic: formatter := cases.Title(language.English, cases.NoLower) if selectedModel.Think { @@ -577,6 +570,13 @@ func (s *sidebarCmp) currentModelBlock() string { } else { parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking off"))) } + default: + reasoningEffort := model.DefaultReasoningEffort + if selectedModel.ReasoningEffort != "" { + reasoningEffort = selectedModel.ReasoningEffort + } + formatter := cases.Title(language.English, cases.NoLower) + parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort)))) } } if s.session.ID != "" { diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 55f25c0d3f2da7c590abf76f7f533f837e4d52f2..0c3269336b5d382c8a5e4ddcca316696850a1052 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -326,7 +326,7 @@ func (c *commandDialogCmp) defaultCommands() []Command { } // OpenAI models: reasoning effort dialog - if providerCfg.Type == catwalk.TypeOpenAI && model.HasReasoningEffort { + if model.HasReasoningEffort { commands = append(commands, Command{ ID: "select_reasoning_effort", Title: "Select Reasoning Effort", diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index a8b4e30c00e21ff7f8d020d48ce9e848665654ef..19f2a3ea20694e1a11f9ff586cd51562f82ba064 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -9,7 +9,6 @@ import ( "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/spinner" tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/app" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/history" @@ -563,8 +562,7 @@ func (p *chatPage) openReasoningDialog() tea.Cmd { model := cfg.GetModelByType(agentCfg.Model) providerCfg := cfg.GetProviderForModel(agentCfg.Model) - if providerCfg != nil && model != nil && - providerCfg.Type == catwalk.TypeOpenAI && model.HasReasoningEffort { + if providerCfg != nil && model != nil && model.HasReasoningEffort { // Return the OpenDialogMsg directly so it bubbles up to the main TUI return dialogs.OpenDialogMsg{ Model: reasoning.NewReasoningDialog(),