on main help sign in
feat(prompts): template prompts and add github and ripgrep info
5ad8beeKieran Klukas
internal/agent/tools/bash.go+3−1
@@ -53,7 +53,7 @@ const (
53 BashNoOutput = "no output"
53 BashNoOutput = "no output"
54)
54)
55
55
56−//go:embed bash.tpl
56+//go:embed bash.md.tpl
57var bashDescriptionTmpl []byte
57var bashDescriptionTmpl []byte
58
58
59var bashDescriptionTpl = template.Must(
59var bashDescriptionTpl = template.Must(
@@ -66,6 +66,7 @@ type bashDescriptionData struct {
66 MaxOutputLength int
66 MaxOutputLength int
67 Attribution config.Attribution
67 Attribution config.Attribution
68 ModelID string
68 ModelID string
69+ RgAvailable bool
69}
70}
70
71
71var bannedCommands = []string{
72var bannedCommands = []string{
@@ -149,6 +150,7 @@ func bashDescription(attribution *config.Attribution, modelID string) string {
149 MaxOutputLength: MaxOutputLength,
150 MaxOutputLength: MaxOutputLength,
150 Attribution: *attribution,
151 Attribution: *attribution,
151 ModelID: modelID,
152 ModelID: modelID,
153+ RgAvailable: getRg() != "",
152 }); err != nil {
154 }); err != nil {
153 // this should never happen.
155 // this should never happen.
154 panic("failed to execute bash description template: " + err.Error())
156 panic("failed to execute bash description template: " + err.Error())
internal/agent/tools/bash.tpl → internal/agent/tools/bash.md.tplrenamed+4−1
@@ -21,6 +21,9 @@ Common shell builtins and core utils available on Windows.
21- Chain with ';' or '&&', avoid newlines except in quoted strings
21- Chain with ';' or '&&', avoid newlines except in quoted strings
22- Each command runs in independent shell (no state persistence between calls)
22- Each command runs in independent shell (no state persistence between calls)
23- Prefer absolute paths over 'cd' (use 'cd' only if user explicitly requests)
23- Prefer absolute paths over 'cd' (use 'cd' only if user explicitly requests)
24+{{- if .RgAvailable }}
25+- Ripgrep (`rg`) is available; prefer it over `grep` for faster, more intuitive searching
26+{{- end }}
24</usage_notes>
27</usage_notes>
25
28
26<background_execution>
29<background_execution>
@@ -82,7 +85,7 @@ When user asks to create git commit:
82
85
836. Run git status to verify.
866. Run git status to verify.
84
87
85−Notes: Use "git commit -am" when possible, don't stage unrelated files, NEVER update config, don't push, no -i flags, no empty commits, return empty response.
88+Notes: Use "git commit -am" when possible, don't stage unrelated files, NEVER update config, don't push, no -i flags, no empty commits, return empty response, when rebasing always use -m or GIT_EDITOR=true.
86</git_commits>
89</git_commits>
87
90
88<pull_requests>
91<pull_requests>
internal/agent/tools/fetch.go+9−3
@@ -4,6 +4,7 @@ import (
4 "context"
4 "context"
5 _ "embed"
5 _ "embed"
6 "fmt"
6 "fmt"
7+ "html/template"
7 "io"
8 "io"
8 "net/http"
9 "net/http"
9 "strings"
10 "strings"
@@ -21,8 +22,13 @@ const (
21 MaxFetchSize = 100 * 1024 // 100KB
22 MaxFetchSize = 100 * 1024 // 100KB
22)
23)
23
24
24−//go:embed fetch.md
25+//go:embed fetch.md.tpl
26+var fetchDescriptionTmpl []byte
27+
28+var fetchDescriptionTpl = template.Must(
25−var fetchDescription string
29+ template.New("fetchDescription").
30+ Parse(string(fetchDescriptionTmpl)),
31+)
26
32
27func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
33func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) fantasy.AgentTool {
28 if client == nil {
34 if client == nil {
@@ -39,7 +45,7 @@ func NewFetchTool(permissions permission.Service, workingDir string, client *htt
39
45
40 return fantasy.NewParallelAgentTool(
46 return fantasy.NewParallelAgentTool(
41 FetchToolName,
47 FetchToolName,
42− fetchDescription,
48+ renderToolDescription(fetchDescriptionTpl),
43 func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
49 func(ctx context.Context, params FetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
44 if params.URL == "" {
50 if params.URL == "" {
45 return fantasy.NewTextErrorResponse("URL parameter is required"), nil
51 return fantasy.NewTextErrorResponse("URL parameter is required"), nil
internal/agent/tools/fetch.mddeleted−1
@@ -1 +0,0 @@
1−Fetch raw content from a URL as text, markdown, or html (max 100KB); no AI processing. For analysis or extraction use agentic_fetch.
internal/agent/tools/fetch.md.tplnew file+2
@@ -0,0 +1,2 @@
1+Fetch raw content from a URL as text, markdown, or html (max 100KB); no AI processing. For analysis or extraction use agentic_fetch.
2+{{- if .GhAvailable }} For GitHub content when an exact repo, issue, or PR link is provided, use `gh` CLI in bash instead.{{- end }}
internal/agent/tools/tools.go+26
@@ -1,7 +1,10 @@
1package tools
1package tools
2
2
3import (
3import (
4+ "bytes"
4 "context"
5 "context"
6+ "html/template"
7+ "os/exec"
5
8
6 "charm.land/fantasy"
9 "charm.land/fantasy"
7)
10)
@@ -64,3 +67,26 @@ func NewPermissionDeniedResponse() fantasy.ToolResponse {
64 resp.StopTurn = true
67 resp.StopTurn = true
65 return resp
68 return resp
66}
69}
70+
71+// ghAvailable indicates whether the `gh` CLI is available on PATH.
72+var ghAvailable = func() bool {
73+ _, err := exec.LookPath("gh")
74+ return err == nil
75+}()
76+
77+// toolDescriptionData is the common data structure for tool description templates.
78+type toolDescriptionData struct {
79+ GhAvailable bool
80+}
81+
82+// renderToolDescription renders a tool description template with the given data.
83+func renderToolDescription(tmpl *template.Template) string {
84+ data := toolDescriptionData{
85+ GhAvailable: ghAvailable,
86+ }
87+ var out bytes.Buffer
88+ if err := tmpl.Execute(&out, data); err != nil {
89+ panic("failed to execute tool description template: " + err.Error())
90+ }
91+ return out.String()
92+}
internal/agent/tools/web_fetch.go+9−3
@@ -4,6 +4,7 @@ import (
4 "context"
4 "context"
5 _ "embed"
5 _ "embed"
6 "fmt"
6 "fmt"
7+ "html/template"
7 "net/http"
8 "net/http"
8 "os"
9 "os"
9 "strings"
10 "strings"
@@ -12,8 +13,13 @@ import (
12 "charm.land/fantasy"
13 "charm.land/fantasy"
13)
14)
14
15
15−//go:embed web_fetch.md
16+//go:embed web_fetch.md.tpl
16−var webFetchToolDescription string
17+var webFetchDescriptionTmpl []byte
18+
19+var webFetchDescriptionTpl = template.Must(
20+ template.New("webFetchDescription").
21+ Parse(string(webFetchDescriptionTmpl)),
22+)
17
23
18// NewWebFetchTool creates a simple web fetch tool for sub-agents (no permissions needed).
24// NewWebFetchTool creates a simple web fetch tool for sub-agents (no permissions needed).
19func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
25func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
@@ -31,7 +37,7 @@ func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
31
37
32 return fantasy.NewParallelAgentTool(
38 return fantasy.NewParallelAgentTool(
33 WebFetchToolName,
39 WebFetchToolName,
34− webFetchToolDescription,
40+ renderToolDescription(webFetchDescriptionTpl),
35 func(ctx context.Context, params WebFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
41 func(ctx context.Context, params WebFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
36 if params.URL == "" {
42 if params.URL == "" {
37 return fantasy.NewTextErrorResponse("url is required"), nil
43 return fantasy.NewTextErrorResponse("url is required"), nil
internal/agent/tools/web_fetch.mddeleted−1
@@ -1 +0,0 @@
1−Fetch a web URL and return content as markdown; for use inside sub-agents. Large pages (>50KB) are saved to a temp file for grep/view.
internal/agent/tools/web_fetch.md.tplnew file+2
@@ -0,0 +1,2 @@
1+Fetch a web URL and return content as markdown; for use inside sub-agents. Large pages (>50KB) are saved to a temp file for grep/view.
2+{{- if .GhAvailable }} For GitHub content when an exact repo, issue, or PR link is provided, use `gh` CLI in bash instead.{{- end }}
internal/agent/tools/web_search.go+9−3
@@ -3,6 +3,7 @@ package tools
3import (
3import (
4 "context"
4 "context"
5 _ "embed"
5 _ "embed"
6+ "html/template"
6 "log/slog"
7 "log/slog"
7 "net/http"
8 "net/http"
8 "time"
9 "time"
@@ -10,8 +11,13 @@ import (
10 "charm.land/fantasy"
11 "charm.land/fantasy"
11)
12)
12
13
13−//go:embed web_search.md
14+//go:embed web_search.md.tpl
14−var webSearchToolDescription string
15+var webSearchDescriptionTmpl []byte
16+
17+var webSearchDescriptionTpl = template.Must(
18+ template.New("webSearchDescription").
19+ Parse(string(webSearchDescriptionTmpl)),
20+)
15
21
16// NewWebSearchTool creates a web search tool for sub-agents (no permissions needed).
22// NewWebSearchTool creates a web search tool for sub-agents (no permissions needed).
17func NewWebSearchTool(client *http.Client) fantasy.AgentTool {
23func NewWebSearchTool(client *http.Client) fantasy.AgentTool {
@@ -29,7 +35,7 @@ func NewWebSearchTool(client *http.Client) fantasy.AgentTool {
29
35
30 return fantasy.NewParallelAgentTool(
36 return fantasy.NewParallelAgentTool(
31 WebSearchToolName,
37 WebSearchToolName,
32− webSearchToolDescription,
38+ renderToolDescription(webSearchDescriptionTpl),
33 func(ctx context.Context, params WebSearchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
39 func(ctx context.Context, params WebSearchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
34 if params.Query == "" {
40 if params.Query == "" {
35 return fantasy.NewTextErrorResponse("query is required"), nil
41 return fantasy.NewTextErrorResponse("query is required"), nil
internal/agent/tools/web_search.mddeleted−1
@@ -1 +0,0 @@
1−Search the web via DuckDuckGo; returns titles, URLs, and snippets. Follow up with web_fetch to get full page content.
internal/agent/tools/web_search.md.tplnew file+2
@@ -0,0 +1,2 @@
1+Search the web via DuckDuckGo; returns titles, URLs, and snippets. Follow up with web_fetch to get full page content.
2+{{- if .GhAvailable }} For GitHub searches when an exact repo name, issue, or link is provided, use `gh search` in bash instead.{{- end }}