mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
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.
99 lines
2.9 KiB
Go
99 lines
2.9 KiB
Go
package skills
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/patriceckhart/zot/internal/core"
|
|
"github.com/patriceckhart/zot/internal/provider"
|
|
)
|
|
|
|
// Tool implements core.Tool, exposing a `skill` tool the LLM can call
|
|
// to load the body of a discovered skill on demand. The system-prompt
|
|
// addendum lists the available names; this tool returns the full
|
|
// markdown for the requested one.
|
|
//
|
|
// The list of skills is held behind a mutex so tests / future
|
|
// /reload-skills wiring can swap in a fresh set without races.
|
|
type Tool struct {
|
|
mu sync.RWMutex
|
|
skills []*Skill
|
|
}
|
|
|
|
// NewTool returns a skill loader tool seeded with the given skills.
|
|
// Pass the slice from Discover().
|
|
func NewTool(skills []*Skill) *Tool { return &Tool{skills: skills} }
|
|
|
|
// SetSkills atomically replaces the underlying skill set. Used when
|
|
// the user re-runs discovery (e.g. after editing a SKILL.md).
|
|
func (t *Tool) SetSkills(s []*Skill) {
|
|
t.mu.Lock()
|
|
t.skills = s
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
// Skills returns a snapshot for callers that need to render the
|
|
// current set (e.g. the /skills picker).
|
|
func (t *Tool) Skills() []*Skill {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
out := make([]*Skill, len(t.skills))
|
|
copy(out, t.skills)
|
|
return out
|
|
}
|
|
|
|
// ---- core.Tool implementation ----
|
|
|
|
// Name is the LLM-facing tool name.
|
|
func (*Tool) Name() string { return "skill" }
|
|
|
|
// Description tells the model what this tool does. Kept blunt so the
|
|
// model reliably uses it instead of guessing what a "skill" is.
|
|
func (*Tool) Description() string {
|
|
return "Load a named skill's instructions. Use when the user's request matches a skill listed above."
|
|
}
|
|
|
|
// Schema is one required string parameter: the skill name.
|
|
func (*Tool) Schema() json.RawMessage {
|
|
return json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}`)
|
|
}
|
|
|
|
// Execute returns the markdown body of the requested skill.
|
|
func (t *Tool) Execute(ctx context.Context, args json.RawMessage, progress func(string)) (core.ToolResult, error) {
|
|
var in struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.Unmarshal(args, &in); err != nil {
|
|
return core.ToolResult{
|
|
IsError: true,
|
|
Content: []provider.Content{provider.TextBlock{Text: "skill: invalid args: " + err.Error()}},
|
|
}, nil
|
|
}
|
|
if in.Name == "" {
|
|
return core.ToolResult{
|
|
IsError: true,
|
|
Content: []provider.Content{provider.TextBlock{Text: "skill: name is required"}},
|
|
}, nil
|
|
}
|
|
|
|
t.mu.RLock()
|
|
s := FindByName(t.skills, in.Name)
|
|
t.mu.RUnlock()
|
|
if s == nil {
|
|
return core.ToolResult{
|
|
IsError: true,
|
|
Content: []provider.Content{provider.TextBlock{Text: fmt.Sprintf("skill: no skill named %q (run /skills in zot to see what's available)", in.Name)}},
|
|
}, nil
|
|
}
|
|
|
|
header := fmt.Sprintf("# Skill: %s\n\n%s\n\n---\n\n", s.Name, s.Description)
|
|
return core.ToolResult{
|
|
Content: []provider.Content{provider.TextBlock{Text: header + s.Body}},
|
|
Details: map[string]any{
|
|
"skill": s.Name,
|
|
"path": s.Path,
|
|
},
|
|
}, nil
|
|
}
|