detached 05d0df9

perf(prompt): cut system prompt to the bone (410 -> 54 tokens)

Three levers, all dead-simple, compounded savings.

1) System prompt rewritten to a one-line identity.
   Was 410 tokens (identity + tool listing duplicating the tool
   schemas + operating guidelines that frontier models already
   internalise). Now 54 tokens:

       You are zot, a lightweight terminal coding agent. Be
       concise, act on the user's request directly, and reply
       with a short summary when done.

   The old 'You have the following tools available:' block listed
   every tool by name and description, which the provider sends
   alongside the actual tool schemas. Pure duplication. Dropped.
   Operating guidelines (prefer edit over write, read before
   editing, don't apologize, etc.) are ~150 tokens of advice the
   model already follows by default. Dropped.

2) Tool descriptions trimmed.
   read:  long paragraph   -> 'Read a file. Images (png/jpg/gif/webp) return inline.'
   write: long paragraph   -> 'Write a file. Creates parent dirs. Overwrites.'
   edit:  long paragraph   -> 'Edit a file via exact-match replacements. Each oldText must be unique in the file.'
   bash:  long paragraph   -> 'Run a shell command. stdout+stderr merged.'
   skill: 2-sentence para  -> 'Load a named skill's instructions. Use when the user's request matches a skill listed above.'

3) Tool schemas minified.
   Every schema was pretty-printed JSON with per-field descriptions
   that reiterated the tool's own description ('Path to the file
   to read (relative or absolute)'). The model infers the obvious
   from property names. Schemas now single-line, type+required
   only. Saves ~20-40 bytes per schema, 5 tools = ~150 bytes per
   request.

Net effect on a fresh OAuth turn, measured end-to-end:

   request body:   3205 bytes -> ~1600 bytes
   system prompt:  410 tokens -> 54 tokens
   tools payload:  ~400 tokens -> ~100 tokens

Escape hatches preserved: --system-prompt (per-run), --append-
system-prompt (per-run, repeatable), and $ZOT_HOME/SYSTEM.md
(persistent) all still work and take precedence over the built-
in identity when set.
commit
05d0df91b8030a4b2f4ef03067d4afb7324f17c7
author
patriceckhart · · 2026-04-19 15:39 UTC
parent
f5719c6
signature
unsigned
refs
tag: v0.0.40
6 files · +31 -93
M internal/agent/systemprompt.go +21 -29
@@ -6,8 +6,10 @@ import (
66 "time"
77 )
88
9-// ToolSummary is a name+one-line description, used when rendering the
10-// "available tools" section of the system prompt.
9+// ToolSummary is a name+one-line description. Kept for backwards
10+// compatibility with callers that still pass tool summaries in; the
11+// built-in system prompt no longer lists tools by name, since the
12+// tool schemas themselves already reach the model.
1113 type ToolSummary struct {
1214 Name string
1315 Description string
@@ -23,6 +25,22 @@ type SystemPromptOpts struct {
2325 }
2426
2527 // BuildSystemPrompt constructs the system prompt.
28+//
29+// Design note: the prompt is intentionally tiny. Every byte here
30+// is re-sent on every request (cached after the first, but still
31+// counts toward cache-write on turn 1 and live context throughout).
32+// We avoid:
33+//
34+// - Listing the tool names and descriptions (the provider sends
35+// the tool schemas separately; duplicating them costs tokens
36+// for zero benefit, the model already sees the tools).
37+// - Repeating generic coding-assistant advice the frontier models
38+// already internalise ("always read before editing", "prefer
39+// minimal diffs", "don't apologize"). These were free tokens
40+// on older models; they are pure overhead now.
41+//
42+// Anything the user explicitly needs can still be added via
43+// --system-prompt, --append-system-prompt, or $ZOT_HOME/SYSTEM.md.
2644 func BuildSystemPrompt(o SystemPromptOpts) string {
2745 if o.Now.IsZero() {
2846 o.Now = time.Now()
@@ -39,10 +57,6 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
3957 sb.WriteString(o.Custom)
4058 } else {
4159 sb.WriteString(defaultIdentity)
42- sb.WriteString("\n\n")
43- sb.WriteString(renderToolsSection(o.Tools))
44- sb.WriteString("\n")
45- sb.WriteString(defaultGuidelines)
4660 }
4761
4862 for _, a := range o.Append {
@@ -57,26 +71,4 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
5771 return sb.String()
5872 }
5973
60-func renderToolsSection(tools []ToolSummary) string {
61- if len(tools) == 0 {
62- return "No tools are available in this session."
63- }
64- var sb strings.Builder
65- sb.WriteString("You have the following tools available:\n")
66- for _, t := range tools {
67- fmt.Fprintf(&sb, "- %s: %s\n", t.Name, t.Description)
68- }
69- return sb.String()
70-}
71-
72-const defaultIdentity = `You are zot, a lightweight terminal coding assistant.
73-You help a developer by reading files, writing files, editing files, and running shell commands.
74-You are concise. You explain your plan briefly, then act. You do not apologize or hedge.`
75-
76-const defaultGuidelines = `Operating guidelines:
77-- Prefer "edit" over "write" for existing files. Always read a file before editing it.
78-- Before running "bash", explain what the command will do in one short sentence.
79-- Avoid destructive commands (rm -rf, dropping tables, force-pushing, etc.) unless the user explicitly asks.
80-- Keep shell commands non-interactive (pass -y / --yes where needed; pipe "yes" if required).
81-- When unsure about a file's contents or structure, read it first rather than guess.
82-- When you are done, reply with a short summary of what you changed and any commands the user should run.`
74+const defaultIdentity = `You are zot, a lightweight terminal coding agent. Be concise, act on the user's request directly, and reply with a short summary when done.`
@@ -6,8 +6,10 @@ import (
6 "time"6 "time"
7 )7 )
8 8
9-// ToolSummary is a name+one-line description, used when rendering the9+// ToolSummary is a name+one-line description. Kept for backwards
10-// "available tools" section of the system prompt.10+// compatibility with callers that still pass tool summaries in; the
11+// built-in system prompt no longer lists tools by name, since the
12+// tool schemas themselves already reach the model.
11 type ToolSummary struct {13 type ToolSummary struct {
12 Name string14 Name string
13 Description string15 Description string
@@ -23,6 +25,22 @@ type SystemPromptOpts struct {
23 }25 }
24 26
25 // BuildSystemPrompt constructs the system prompt.27 // BuildSystemPrompt constructs the system prompt.
28+//
29+// Design note: the prompt is intentionally tiny. Every byte here
30+// is re-sent on every request (cached after the first, but still
31+// counts toward cache-write on turn 1 and live context throughout).
32+// We avoid:
33+//
34+// - Listing the tool names and descriptions (the provider sends
35+// the tool schemas separately; duplicating them costs tokens
36+// for zero benefit, the model already sees the tools).
37+// - Repeating generic coding-assistant advice the frontier models
38+// already internalise ("always read before editing", "prefer
39+// minimal diffs", "don't apologize"). These were free tokens
40+// on older models; they are pure overhead now.
41+//
42+// Anything the user explicitly needs can still be added via
43+// --system-prompt, --append-system-prompt, or $ZOT_HOME/SYSTEM.md.
26 func BuildSystemPrompt(o SystemPromptOpts) string {44 func BuildSystemPrompt(o SystemPromptOpts) string {
27 if o.Now.IsZero() {45 if o.Now.IsZero() {
28 o.Now = time.Now()46 o.Now = time.Now()
@@ -39,10 +57,6 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
39 sb.WriteString(o.Custom)57 sb.WriteString(o.Custom)
40 } else {58 } else {
41 sb.WriteString(defaultIdentity)59 sb.WriteString(defaultIdentity)
42- sb.WriteString("\n\n")
43- sb.WriteString(renderToolsSection(o.Tools))
44- sb.WriteString("\n")
45- sb.WriteString(defaultGuidelines)
46 }60 }
47 61
48 for _, a := range o.Append {62 for _, a := range o.Append {
@@ -57,26 +71,4 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
57 return sb.String()71 return sb.String()
58 }72 }
59 73
60-func renderToolsSection(tools []ToolSummary) string {74+const defaultIdentity = `You are zot, a lightweight terminal coding agent. Be concise, act on the user's request directly, and reply with a short summary when done.`
61- if len(tools) == 0 {
62- return "No tools are available in this session."
63- }
64- var sb strings.Builder
65- sb.WriteString("You have the following tools available:\n")
66- for _, t := range tools {
67- fmt.Fprintf(&sb, "- %s: %s\n", t.Name, t.Description)
68- }
69- return sb.String()
70-}
71-
72-const defaultIdentity = `You are zot, a lightweight terminal coding assistant.
73-You help a developer by reading files, writing files, editing files, and running shell commands.
74-You are concise. You explain your plan briefly, then act. You do not apologize or hedge.`
75-
76-const defaultGuidelines = `Operating guidelines:
77-- Prefer "edit" over "write" for existing files. Always read a file before editing it.
78-- Before running "bash", explain what the command will do in one short sentence.
79-- Avoid destructive commands (rm -rf, dropping tables, force-pushing, etc.) unless the user explicitly asks.
80-- Keep shell commands non-interactive (pass -y / --yes where needed; pipe "yes" if required).
81-- When unsure about a file's contents or structure, read it first rather than guess.
82-- When you are done, reply with a short summary of what you changed and any commands the user should run.`
M internal/agent/tools/bash.go +2 -10
@@ -36,18 +36,10 @@ type bashArgs struct {
3636 Timeout int `json:"timeout,omitempty"`
3737 }
3838
39-const bashSchema = `{
40- "type":"object",
41- "properties":{
42- "command":{"type":"string","description":"Shell command to execute."},
43- "timeout":{"type":"integer","description":"Timeout in seconds. No default timeout."}
44- },
45- "required":["command"],
46- "additionalProperties":false
47-}`
39+const bashSchema = `{"type":"object","properties":{"command":{"type":"string"},"timeout":{"type":"integer"}},"required":["command"]}`
4840
4941 func (t *BashTool) Name() string { return "bash" }
50-func (t *BashTool) Description() string { return "Run a shell command. stdout and stderr are merged." }
42+func (t *BashTool) Description() string { return "Run a shell command. stdout+stderr merged." }
5143 func (t *BashTool) Schema() json.RawMessage { return json.RawMessage(bashSchema) }
5244
5345 func (t *BashTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
@@ -36,18 +36,10 @@ type bashArgs struct {
36 Timeout int `json:"timeout,omitempty"`36 Timeout int `json:"timeout,omitempty"`
37 }37 }
38 38
39-const bashSchema = `{39+const bashSchema = `{"type":"object","properties":{"command":{"type":"string"},"timeout":{"type":"integer"}},"required":["command"]}`
40- "type":"object",
41- "properties":{
42- "command":{"type":"string","description":"Shell command to execute."},
43- "timeout":{"type":"integer","description":"Timeout in seconds. No default timeout."}
44- },
45- "required":["command"],
46- "additionalProperties":false
47-}`
48 40
49 func (t *BashTool) Name() string { return "bash" }41 func (t *BashTool) Name() string { return "bash" }
50-func (t *BashTool) Description() string { return "Run a shell command. stdout and stderr are merged." }42+func (t *BashTool) Description() string { return "Run a shell command. stdout+stderr merged." }
51 func (t *BashTool) Schema() json.RawMessage { return json.RawMessage(bashSchema) }43 func (t *BashTool) Schema() json.RawMessage { return json.RawMessage(bashSchema) }
52 44
53 func (t *BashTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {45 func (t *BashTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
M internal/agent/tools/edit.go +2 -22
@@ -28,31 +28,11 @@ type editArgs struct {
2828 Edits []editOp `json:"edits"`
2929 }
3030
31-const editSchema = `{
32- "type":"object",
33- "properties":{
34- "path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},
35- "edits":{
36- "type":"array",
37- "description":"One or more targeted replacements. Each oldText must match exactly once in the original file.",
38- "items":{
39- "type":"object",
40- "properties":{
41- "oldText":{"type":"string","description":"Exact text to replace. Must be unique in the file."},
42- "newText":{"type":"string","description":"Replacement text."}
43- },
44- "required":["oldText","newText"],
45- "additionalProperties":false
46- }
47- }
48- },
49- "required":["path","edits"],
50- "additionalProperties":false
51-}`
31+const editSchema = `{"type":"object","properties":{"path":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"oldText":{"type":"string"},"newText":{"type":"string"}},"required":["oldText","newText"]}}},"required":["path","edits"]}`
5232
5333 func (t *EditTool) Name() string { return "edit" }
5434 func (t *EditTool) Description() string {
55- return "Edit an existing file via exact-match replacements. Preserves line endings and BOM."
35+ return "Edit a file via exact-match replacements. Each oldText must be unique in the file."
5636 }
5737 func (t *EditTool) Schema() json.RawMessage { return json.RawMessage(editSchema) }
5838
@@ -28,31 +28,11 @@ type editArgs struct {
28 Edits []editOp `json:"edits"`28 Edits []editOp `json:"edits"`
29 }29 }
30 30
31-const editSchema = `{31+const editSchema = `{"type":"object","properties":{"path":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"oldText":{"type":"string"},"newText":{"type":"string"}},"required":["oldText","newText"]}}},"required":["path","edits"]}`
32- "type":"object",
33- "properties":{
34- "path":{"type":"string","description":"Path to the file to edit (relative or absolute)"},
35- "edits":{
36- "type":"array",
37- "description":"One or more targeted replacements. Each oldText must match exactly once in the original file.",
38- "items":{
39- "type":"object",
40- "properties":{
41- "oldText":{"type":"string","description":"Exact text to replace. Must be unique in the file."},
42- "newText":{"type":"string","description":"Replacement text."}
43- },
44- "required":["oldText","newText"],
45- "additionalProperties":false
46- }
47- }
48- },
49- "required":["path","edits"],
50- "additionalProperties":false
51-}`
52 32
53 func (t *EditTool) Name() string { return "edit" }33 func (t *EditTool) Name() string { return "edit" }
54 func (t *EditTool) Description() string {34 func (t *EditTool) Description() string {
55- return "Edit an existing file via exact-match replacements. Preserves line endings and BOM."35+ return "Edit a file via exact-match replacements. Each oldText must be unique in the file."
56 }36 }
57 func (t *EditTool) Schema() json.RawMessage { return json.RawMessage(editSchema) }37 func (t *EditTool) Schema() json.RawMessage { return json.RawMessage(editSchema) }
58 38
M internal/agent/tools/read.go +2 -11
@@ -31,20 +31,11 @@ type readArgs struct {
3131 Limit int `json:"limit,omitempty"`
3232 }
3333
34-const readSchema = `{
35- "type":"object",
36- "properties":{
37- "path":{"type":"string","description":"Path to the file to read (relative or absolute)"},
38- "offset":{"type":"integer","description":"Line number to start reading from (1-indexed)"},
39- "limit":{"type":"integer","description":"Maximum number of lines to read"}
40- },
41- "required":["path"],
42- "additionalProperties":false
43-}`
34+const readSchema = `{"type":"object","properties":{"path":{"type":"string"},"offset":{"type":"integer"},"limit":{"type":"integer"}},"required":["path"]}`
4435
4536 func (t *ReadTool) Name() string { return "read" }
4637 func (t *ReadTool) Description() string {
47- return "Read the contents of a text file or an image (png/jpg/jpeg/gif/webp). Large files are truncated."
38+ return "Read a file. Images (png/jpg/gif/webp) return inline."
4839 }
4940 func (t *ReadTool) Schema() json.RawMessage { return json.RawMessage(readSchema) }
5041
@@ -31,20 +31,11 @@ type readArgs struct {
31 Limit int `json:"limit,omitempty"`31 Limit int `json:"limit,omitempty"`
32 }32 }
33 33
34-const readSchema = `{34+const readSchema = `{"type":"object","properties":{"path":{"type":"string"},"offset":{"type":"integer"},"limit":{"type":"integer"}},"required":["path"]}`
35- "type":"object",
36- "properties":{
37- "path":{"type":"string","description":"Path to the file to read (relative or absolute)"},
38- "offset":{"type":"integer","description":"Line number to start reading from (1-indexed)"},
39- "limit":{"type":"integer","description":"Maximum number of lines to read"}
40- },
41- "required":["path"],
42- "additionalProperties":false
43-}`
44 35
45 func (t *ReadTool) Name() string { return "read" }36 func (t *ReadTool) Name() string { return "read" }
46 func (t *ReadTool) Description() string {37 func (t *ReadTool) Description() string {
47- return "Read the contents of a text file or an image (png/jpg/jpeg/gif/webp). Large files are truncated."38+ return "Read a file. Images (png/jpg/gif/webp) return inline."
48 }39 }
49 func (t *ReadTool) Schema() json.RawMessage { return json.RawMessage(readSchema) }40 func (t *ReadTool) Schema() json.RawMessage { return json.RawMessage(readSchema) }
50 41
M internal/agent/tools/write.go +2 -10
@@ -22,19 +22,11 @@ type writeArgs struct {
2222 Content string `json:"content"`
2323 }
2424
25-const writeSchema = `{
26- "type":"object",
27- "properties":{
28- "path":{"type":"string","description":"Path to the file to write (relative or absolute)"},
29- "content":{"type":"string","description":"File contents. Overwrites any existing file."}
30- },
31- "required":["path","content"],
32- "additionalProperties":false
33-}`
25+const writeSchema = `{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}`
3426
3527 func (t *WriteTool) Name() string { return "write" }
3628 func (t *WriteTool) Description() string {
37- return "Write content to a file. Creates parent directories. Overwrites existing files."
29+ return "Write a file. Creates parent dirs. Overwrites."
3830 }
3931 func (t *WriteTool) Schema() json.RawMessage { return json.RawMessage(writeSchema) }
4032
@@ -22,19 +22,11 @@ type writeArgs struct {
22 Content string `json:"content"`22 Content string `json:"content"`
23 }23 }
24 24
25-const writeSchema = `{25+const writeSchema = `{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}`
26- "type":"object",
27- "properties":{
28- "path":{"type":"string","description":"Path to the file to write (relative or absolute)"},
29- "content":{"type":"string","description":"File contents. Overwrites any existing file."}
30- },
31- "required":["path","content"],
32- "additionalProperties":false
33-}`
34 26
35 func (t *WriteTool) Name() string { return "write" }27 func (t *WriteTool) Name() string { return "write" }
36 func (t *WriteTool) Description() string {28 func (t *WriteTool) Description() string {
37- return "Write content to a file. Creates parent directories. Overwrites existing files."29+ return "Write a file. Creates parent dirs. Overwrites."
38 }30 }
39 func (t *WriteTool) Schema() json.RawMessage { return json.RawMessage(writeSchema) }31 func (t *WriteTool) Schema() json.RawMessage { return json.RawMessage(writeSchema) }
40 32
M internal/skills/tool.go +2 -11
@@ -52,21 +52,12 @@ func (*Tool) Name() string { return "skill" }
5252 // Description tells the model what this tool does. Kept blunt so the
5353 // model reliably uses it instead of guessing what a "skill" is.
5454 func (*Tool) Description() string {
55- return "Load the full body of a named skill. Use this when the user's request matches one of the skills listed in the system prompt; the tool returns the skill's instructions, which you should then follow."
55+ return "Load a named skill's instructions. Use when the user's request matches a skill listed above."
5656 }
5757
5858 // Schema is one required string parameter: the skill name.
5959 func (*Tool) Schema() json.RawMessage {
60- return json.RawMessage(`{
61- "type": "object",
62- "properties": {
63- "name": {
64- "type": "string",
65- "description": "The skill name (must match one listed in the system prompt)."
66- }
67- },
68- "required": ["name"]
69- }`)
60+ return json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}`)
7061 }
7162
7263 // Execute returns the markdown body of the requested skill.
@@ -52,21 +52,12 @@ func (*Tool) Name() string { return "skill" }
52 // Description tells the model what this tool does. Kept blunt so the52 // Description tells the model what this tool does. Kept blunt so the
53 // model reliably uses it instead of guessing what a "skill" is.53 // model reliably uses it instead of guessing what a "skill" is.
54 func (*Tool) Description() string {54 func (*Tool) Description() string {
55- return "Load the full body of a named skill. Use this when the user's request matches one of the skills listed in the system prompt; the tool returns the skill's instructions, which you should then follow."55+ return "Load a named skill's instructions. Use when the user's request matches a skill listed above."
56 }56 }
57 57
58 // Schema is one required string parameter: the skill name.58 // Schema is one required string parameter: the skill name.
59 func (*Tool) Schema() json.RawMessage {59 func (*Tool) Schema() json.RawMessage {
60- return json.RawMessage(`{60+ return json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}`)
61- "type": "object",
62- "properties": {
63- "name": {
64- "type": "string",
65- "description": "The skill name (must match one listed in the system prompt)."
66- }
67- },
68- "required": ["name"]
69- }`)
70 }61 }
71 62
72 // Execute returns the markdown body of the requested skill.63 // Execute returns the markdown body of the requested skill.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close