14 "github.com/charmbracelet/crush/internal/agent/prompt"
15 "github.com/charmbracelet/crush/internal/agent/tools"
16 "github.com/charmbracelet/crush/internal/permission"
19//go:embed templates/agentic_fetch.md
20var agenticFetchToolDescription []byte
22// agenticFetchValidationResult holds the validated parameters from the tool call context.
23type agenticFetchValidationResult struct {
28// validateAgenticFetchParams validates the tool call parameters and extracts required context values.
29func validateAgenticFetchParams(ctx context.Context, params tools.AgenticFetchParams) (agenticFetchValidationResult, error) {
30 if params.Prompt == "" {
31 return agenticFetchValidationResult{}, errors.New("prompt is required")
34 sessionID := tools.GetSessionFromContext(ctx)
36 return agenticFetchValidationResult{}, errors.New("session id missing from context")
39 agentMessageID := tools.GetMessageFromContext(ctx)
40 if agentMessageID == "" {
41 return agenticFetchValidationResult{}, errors.New("agent message id missing from context")
44 return agenticFetchValidationResult{
46 AgentMessageID: agentMessageID,
50//go:embed templates/agentic_fetch_prompt.md.tpl
51var agenticFetchPromptTmpl []byte
53func (c *coordinator) agenticFetchTool(_ context.Context, client *http.Client) (fantasy.AgentTool, error) {
55 client = &http.Client{
56 Timeout: 30 * time.Second,
57 Transport: &http.Transport{
59 MaxIdleConnsPerHost: 10,
60 IdleConnTimeout: 90 * time.Second,
65 return fantasy.NewParallelAgentTool(
66 tools.AgenticFetchToolName,
67 string(agenticFetchToolDescription),
68 func(ctx context.Context, params tools.AgenticFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
69 validationResult, err := validateAgenticFetchParams(ctx, params)
71 return fantasy.NewTextErrorResponse(err.Error()), nil
74 // Determine description based on mode.
75 var description string
77 description = fmt.Sprintf("Fetch and analyze content from URL: %s", params.URL)
79 description = "Search the web and analyze results"
82 p := c.permissions.Request(
83 permission.CreatePermissionRequest{
84 SessionID: validationResult.SessionID,
85 Path: c.cfg.WorkingDir(),
87 ToolName: tools.AgenticFetchToolName,
89 Description: description,
90 Params: tools.AgenticFetchPermissionsParams(params),
95 return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
98 tmpDir, err := os.MkdirTemp(c.cfg.Options.DataDirectory, "crush-fetch-*")
100 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary directory: %s", err)), nil
102 defer os.RemoveAll(tmpDir)
104 var fullPrompt string
106 if params.URL != "" {
107 // URL mode: fetch the URL content first.
108 content, err := tools.FetchURLAndConvert(ctx, client, params.URL)
110 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to fetch URL: %s", err)), nil
113 hasLargeContent := len(content) > tools.LargeContentThreshold
116 tempFile, err := os.CreateTemp(tmpDir, "page-*.md")
118 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary file: %s", err)), nil
120 tempFilePath := tempFile.Name()
122 if _, err := tempFile.WriteString(content); err != nil {
124 return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to write content to file: %s", err)), nil
128 fullPrompt = fmt.Sprintf("%s\n\nThe web page from %s has been saved to: %s\n\nUse the view and grep tools to analyze this file and extract the requested information.", params.Prompt, params.URL, tempFilePath)
130 fullPrompt = fmt.Sprintf("%s\n\nWeb page URL: %s\n\n<webpage_content>\n%s\n</webpage_content>", params.Prompt, params.URL, content)
133 // Search mode: let the sub-agent search and fetch as needed.
134 fullPrompt = fmt.Sprintf("%s\n\nUse the web_search tool to find relevant information. Break down the question into smaller, focused searches if needed. After searching, use web_fetch to get detailed content from the most relevant results.", params.Prompt)
137 promptOpts := []prompt.Option{
138 prompt.WithWorkingDir(tmpDir),
141 promptTemplate, err := prompt.NewPrompt("agentic_fetch", string(agenticFetchPromptTmpl), promptOpts...)
143 return fantasy.ToolResponse{}, fmt.Errorf("error creating prompt: %s", err)
146 _, small, err := c.buildAgentModels(ctx)
148 return fantasy.ToolResponse{}, fmt.Errorf("error building models: %s", err)
151 systemPrompt, err := promptTemplate.Build(ctx, small.Model.Provider(), small.Model.Model(), *c.cfg)
153 return fantasy.ToolResponse{}, fmt.Errorf("error building system prompt: %s", err)
156 smallProviderCfg, ok := c.cfg.Providers.Get(small.ModelCfg.Provider)
158 return fantasy.ToolResponse{}, errors.New("small model provider not configured")
161 webFetchTool := tools.NewWebFetchTool(tmpDir, client)
162 webSearchTool := tools.NewWebSearchTool(client)
163 fetchTools := []fantasy.AgentTool{
166 tools.NewGlobTool(tmpDir),
167 tools.NewGrepTool(tmpDir),
168 tools.NewSourcegraphTool(client),
169 tools.NewViewTool(c.lspClients, c.permissions, tmpDir),
172 agent := NewSessionAgent(SessionAgentOptions{
173 LargeModel: small, // Use small model for both (fetch doesn't need large)
175 SystemPromptPrefix: smallProviderCfg.SystemPromptPrefix,
176 SystemPrompt: systemPrompt,
177 DisableAutoSummarize: c.cfg.Options.DisableAutoSummarize,
178 IsYolo: c.permissions.SkipRequests(),
179 Sessions: c.sessions,
180 Messages: c.messages,
184 agentToolSessionID := c.sessions.CreateAgentToolSessionID(validationResult.AgentMessageID, call.ID)
185 session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, validationResult.SessionID, "Fetch Analysis")
187 return fantasy.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
190 c.permissions.AutoApproveSession(session.ID)
192 // Use small model for web content analysis (faster and cheaper)
193 maxTokens := small.CatwalkCfg.DefaultMaxTokens
194 if small.ModelCfg.MaxTokens != 0 {
195 maxTokens = small.ModelCfg.MaxTokens
198 result, err := agent.Run(ctx, SessionAgentCall{
199 SessionID: session.ID,
201 MaxOutputTokens: maxTokens,
202 ProviderOptions: getProviderOptions(small, smallProviderCfg),
203 Temperature: small.ModelCfg.Temperature,
204 TopP: small.ModelCfg.TopP,
205 TopK: small.ModelCfg.TopK,
206 FrequencyPenalty: small.ModelCfg.FrequencyPenalty,
207 PresencePenalty: small.ModelCfg.PresencePenalty,
210 return fantasy.NewTextErrorResponse("error generating response"), nil
213 updatedSession, err := c.sessions.Get(ctx, session.ID)
215 return fantasy.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
217 parentSession, err := c.sessions.Get(ctx, validationResult.SessionID)
219 return fantasy.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
222 parentSession.Cost += updatedSession.Cost
224 _, err = c.sessions.Save(ctx, parentSession)
226 return fantasy.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
229 return fantasy.NewTextResponse(result.Response.Content.Text()), nil
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list