Skip to content

Commit a1fb7b4

Browse files
committed
perf: stop deferring core tools
1 parent ba31e14 commit a1fb7b4

12 files changed

Lines changed: 192 additions & 11 deletions

File tree

internal/bootstrap/assemble_session.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,31 @@ func buildHookSupport(input *resolvedInput, services *bootServices, settings con
173173
}
174174

175175
// coreToolNames are tools that remain always visible to the LLM.
176-
// When tool search is enabled, all tools except tool_search itself are deferred.
177-
// tool_search is added separately and never appears in the deferred set.
178-
var coreToolNames = map[string]bool{}
176+
// Tools NOT in this set are deferred behind tool_search when the model
177+
// supports it. The default is opt-in: frequently used core tools stay
178+
// in the main prompt so the model can call them turn 1 without a
179+
// tool_search round-trip; rarely used or schema-heavy tools defer to
180+
// keep the base prompt compact.
181+
var coreToolNames = map[string]bool{
182+
// Filesystem + shell — used in virtually every turn.
183+
"read": true,
184+
"write": true,
185+
"edit": true,
186+
"bash": true,
187+
"grep": true,
188+
"glob": true,
189+
"ls": true,
190+
// Task management — if present, should be immediately callable
191+
// (the system prompt tells the model to use them proactively).
192+
"task_create": true,
193+
"task_update": true,
194+
"task_list": true,
195+
"task_get": true,
196+
// Interaction / plan mode — turn-1 UX primitives.
197+
"ask_user": true,
198+
"enter_plan_mode": true,
199+
"exit_plan_mode": true,
200+
}
179201

180202
// supportsToolSearch reports whether the given provider/model combination
181203
// supports deferred tool search. Currently only Claude models and GPT-5.4+

internal/config/prompt.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,47 @@ Break down and manage your work with task_create, task_update, and task_list.
7474
- Check task_list before creating more tasks if a relevant task may already exist
7575
- After completing a task, call task_list to find the next pending or unblocked task`
7676
}
77+
doingTasksInstructions := `## Doing tasks
78+
- The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.
79+
- You are highly capable and often allow users to complete ambitious tasks that would otherwise be too complex or take too long. You should defer to user judgement about whether a task is too large to attempt.
80+
- In general, do not propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications.
81+
- Do not create files unless they're absolutely necessary for achieving your goal. Generally prefer editing an existing file to creating a new one, as this prevents file bloat and builds on existing work more effectively.
82+
- Avoid giving time estimates or predictions for how long tasks will take, whether for your own work or for users planning projects. Focus on what needs to be done, not how long it might take.
83+
- If an approach fails, diagnose why before switching tactics—read the error, check your assumptions, try a focused fix. Don't retry the identical action blindly, but don't abandon a viable approach after a single failure either. Escalate to the user with ask_user only when you're genuinely stuck after investigation, not as a first response to friction.
84+
- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it. Prioritize writing safe, secure, and correct code.
85+
- Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.
86+
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.
87+
- Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is what the task actually requires—no speculative abstractions, but no half-finished implementations either. Three similar lines of code is better than a premature abstraction.
88+
- Avoid backwards-compatibility hacks like renaming unused _vars, re-exporting types, adding // removed comments for removed code, etc. If you are certain that something is unused, you can delete it completely.`
89+
90+
usingYourToolsInstructions := `## Using your tools
91+
- Do NOT use bash to run commands when a relevant dedicated tool is provided. Using dedicated tools allows the user to better understand and review your work. This is CRITICAL to assisting the user:
92+
- To read files use read instead of cat, head, tail, or sed
93+
- To edit files use edit instead of sed or awk
94+
- To create files use write instead of cat with heredoc or echo redirection
95+
- To search for files use glob instead of find or ls
96+
- To search the content of files, use grep instead of grep or rg
97+
- Reserve using bash exclusively for system commands and terminal operations that require shell execution. If you are unsure and there is a relevant dedicated tool, default to using the dedicated tool and only fallback on using bash for these if it is absolutely necessary.
98+
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead.`
99+
100+
outputEfficiencyInstructions := `## Output efficiency
101+
102+
IMPORTANT: Go straight to the point. Try the simplest approach first without going in circles. Do not overdo it. Be extra concise.
103+
104+
Keep your text output brief and direct. Lead with the answer or action, not the reasoning. Skip filler words, preamble, and unnecessary transitions. Do not restate what the user said — just do it. When explaining, include only what is necessary for the user to understand.
105+
106+
Focus text output on:
107+
- Decisions that need the user's input
108+
- High-level status updates at natural milestones
109+
- Errors or blockers that change the plan
110+
111+
If you can say it in one sentence, don't use three. Prefer short, direct sentences over long explanations. This does not apply to code or tool calls.`
77112
autoMemoryInstructions := BuildAutoMemoryInstructions(ctx.MemoryDir)
78113
var instructionParts []string
79114
if toolsBody.Len() > 0 {
80115
instructionParts = append(instructionParts, toolsBody.String())
81116
}
117+
instructionParts = append(instructionParts, doingTasksInstructions, usingYourToolsInstructions, outputEfficiencyInstructions)
82118
if taskManagementInstructions != "" {
83119
instructionParts = append(instructionParts, taskManagementInstructions)
84120
}

internal/config/prompt_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ func TestBuildSystemBlockTextsNoTools(t *testing.T) {
4343
}
4444
}
4545

46+
func TestBuildSystemBlockTextsIncludesDoingTasksGuardrails(t *testing.T) {
47+
t.Parallel()
48+
49+
_, instructions := BuildSystemBlockTexts("/tmp/ws", ContextFiles{}, []ToolInfo{{Name: "read"}})
50+
51+
for _, marker := range []string{
52+
"## Doing tasks",
53+
`"improvements" beyond what was asked`,
54+
"scenarios that can't happen",
55+
"premature abstraction",
56+
"diagnose why before switching tactics",
57+
"OWASP top 10",
58+
"backwards-compatibility hacks",
59+
"## Using your tools",
60+
"Maximize use of parallel tool calls",
61+
"## Output efficiency",
62+
"Go straight to the point",
63+
} {
64+
if !strings.Contains(instructions, marker) {
65+
t.Errorf("instructions missing guardrail %q", marker)
66+
}
67+
}
68+
}
69+
4670
func TestBuildSystemBlockTextsAddsTaskManagementSection(t *testing.T) {
4771
t.Parallel()
4872

internal/ui/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,6 @@ func (a *App) execShell(cmd string) tea.Cmd {
433433
} else if err != nil {
434434
result = err.Error()
435435
}
436-
return tui.CommandResultMsg{Text: tui.CommandStyle.Render(result)}
436+
return tui.CommandResultMsg{Text: tui.CommandStyle.Render(result), Inline: true}
437437
}
438438
}

internal/ui/tui/events.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,31 @@ import (
1010
"github.com/voocel/agentcore"
1111
)
1212

13+
// formatScrollbackBlock applies the project's standard spacing rules to a
14+
// block of scrollback output: trailing newlines stripped, and optionally a
15+
// leading blank line so the block is visually separated from what came
16+
// before. Kept pure so the two print helpers and the tests can share it.
17+
func formatScrollbackBlock(content string, inline bool) string {
18+
content = strings.TrimRight(content, "\n")
19+
if inline {
20+
return content
21+
}
22+
return "\n" + content
23+
}
24+
1325
// printBlock prints content to terminal scrollback with a leading blank line.
1426
// Every top-level output block (assistant reply, tool result, error) should
1527
// use this instead of raw tea.Println so blocks are visually separated by
16-
// exactly one blank line. Trailing newlines are stripped so that spacing
17-
// between blocks is always consistent regardless of content construction.
28+
// exactly one blank line.
1829
func printBlock(content string) tea.Cmd {
19-
return tea.Println("\n" + strings.TrimRight(content, "\n"))
30+
return tea.Println(formatScrollbackBlock(content, false))
31+
}
32+
33+
// printInline prints content flush against the previous block (no leading
34+
// blank line). Use for output that should feel like a direct continuation
35+
// of what came before — e.g. shell command output under its echoed prompt.
36+
func printInline(content string) tea.Cmd {
37+
return tea.Println(formatScrollbackBlock(content, true))
2038
}
2139

2240
// HandleAgentEvent processes agent events.

internal/ui/tui/messages.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ type AgentEventMsg struct {
1313

1414
// CommandResultMsg carries the result of a slash command back to the model.
1515
type CommandResultMsg struct {
16-
Text string
17-
Quit bool // true for /exit
18-
Clear bool // true for /clear
16+
Text string
17+
// Inline prints the result flush against the previous scrollback block
18+
// (no leading blank line). Use for output that should feel like a direct
19+
// continuation — e.g. shell command output under its echoed prompt.
20+
Inline bool
21+
Quit bool // true for /exit
22+
Clear bool // true for /clear
1923
NewProvider string // non-empty if provider was switched
2024
NewModel string // non-empty if model was switched
2125
NewContextWindow int // non-zero if context window changed

internal/ui/tui/model_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,30 @@ func TestHandleCommandResultUpdatesProviderAndModel(t *testing.T) {
174174
}
175175
}
176176

177+
func TestFormatScrollbackBlock(t *testing.T) {
178+
t.Parallel()
179+
180+
cases := []struct {
181+
name string
182+
content string
183+
inline bool
184+
want string
185+
}{
186+
{name: "block adds leading blank line", content: " ok", inline: false, want: "\n ok"},
187+
{name: "inline stays flush", content: " ok", inline: true, want: " ok"},
188+
{name: "block strips trailing newlines", content: "hello\n\n", inline: false, want: "\nhello"},
189+
{name: "inline strips trailing newlines", content: "hello\n\n", inline: true, want: "hello"},
190+
}
191+
for _, tc := range cases {
192+
t.Run(tc.name, func(t *testing.T) {
193+
t.Parallel()
194+
if got := formatScrollbackBlock(tc.content, tc.inline); got != tc.want {
195+
t.Fatalf("formatScrollbackBlock(%q, %v) = %q, want %q", tc.content, tc.inline, got, tc.want)
196+
}
197+
})
198+
}
199+
}
200+
177201
func TestOverlayAppearsBelowInput(t *testing.T) {
178202
m := New(nil, "anthropic/claude-sonnet-4.6", Config{
179203
Overlay: func(*Model) *OverlayState {

internal/ui/tui/render.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,11 @@ func (m *Model) renderInputPanel() string {
203203
sections = append(sections, inputView)
204204

205205
content := strings.Join(sections, "\n\n")
206-
return InputPanelStyle.Width(max(width-2, 20)).Render(content)
206+
panelStyle := InputPanelStyle
207+
if m.shellInputActive() {
208+
panelStyle = ShellInputPanelStyle
209+
}
210+
return panelStyle.Width(max(width-2, 20)).Render(content)
207211
}
208212

209213
// RenderStatusBar renders the status line above the input.

internal/ui/tui/render_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ func TestRenderContextBarShowsModeIndicator(t *testing.T) {
6363
}
6464
}
6565

66+
func TestRenderInputPanelHighlightsShellMode(t *testing.T) {
67+
m := New(nil, "anthropic/claude-sonnet-4.6")
68+
m.Ready = true
69+
m.Width = 80
70+
m.Input.SetValue("!git status")
71+
72+
if !m.shellInputActive() {
73+
t.Fatal("expected shell input mode to activate for !-prefixed input")
74+
}
75+
}
76+
77+
func TestRenderInputPanelUsesDefaultStyleWithoutShellPrefix(t *testing.T) {
78+
m := New(nil, "anthropic/claude-sonnet-4.6")
79+
m.Ready = true
80+
m.Width = 80
81+
m.Input.SetValue("git status")
82+
83+
if m.shellInputActive() {
84+
t.Fatal("did not expect shell input mode without ! prefix")
85+
}
86+
}
87+
6688
func TestIndentBlock(t *testing.T) {
6789
cases := []struct {
6890
name string

internal/ui/tui/styles.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,21 @@ var (
207207
BorderForeground(ColorInputChrome).
208208
Padding(0, 1)
209209

210+
ShellInputPanelStyle = lipgloss.NewStyle().
211+
Border(lipgloss.Border{Top: "─", Bottom: "─"}).
212+
BorderTop(true).
213+
BorderBottom(true).
214+
BorderLeft(false).
215+
BorderRight(false).
216+
BorderForeground(ColorShell).
217+
Padding(0, 1)
218+
219+
// ShellAccentStyle is used for both the prompt caret ("❯") and the "!" prefix
220+
// when the input is in shell mode — they share the same foreground/weight by design.
221+
ShellAccentStyle = lipgloss.NewStyle().
222+
Foreground(ColorShell).
223+
Bold(true)
224+
210225
InputHintStyle = lipgloss.NewStyle().
211226
Foreground(ColorMuted)
212227

0 commit comments

Comments
 (0)