Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ from this file's releases; see CONTRACT.md for the stability policy.
read-only `mcp_list_resources`, `mcp_read_resource`, `mcp_list_prompts` and
`mcp_get_prompt` tools.
- TUI: `ctrl+k` opens the command palette (the slash menu without typing `/`).
- Incremental indexing: successful `write`/`edit`/`multiedit` calls notify the
engine, which updates the symbol index and filename index immediately
(`ToolRegistry.SetFileChangeHandler`).
- Cache diagnostics: `llm.PrefixFingerprint` hashes the stable system+tools
prefix; `RAKIT_DEBUG=1` logs it per request so cache busts are visible.
- Permission engine (`permission`): modes `plan|build|edit|yolo|auto`,
per-tool allow/deny/ask rules from `~/.rakit/settings.json` and
`<workspace>/.rakit/settings.json`, per-call decisions, project/session
Expand Down
17 changes: 16 additions & 1 deletion engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type Engine struct {
mcpMgr *mcp.Manager
mgr *agent.Manager
idxStore *index.Store
searchMgr *search.SearchManager
home string
workspace string
yolo bool
Expand Down Expand Up @@ -122,8 +123,9 @@ func New(opts Options) (*Engine, error) {
}

toolReg := opts.Tools
var searchMgr *search.SearchManager
if toolReg == nil {
searchMgr := search.NewSearchManager()
searchMgr = search.NewSearchManager()
searchMgr.SetDirectories([]string{workspace})
toolReg = tools.NewRegistry(searchMgr)
}
Expand Down Expand Up @@ -163,6 +165,18 @@ func New(opts Options) (*Engine, error) {
}
})
}
if toolReg != nil {
toolReg.SetFileChangeHandler(func(path string) {
go func() {
if err := idxStore.Update(path); err != nil {
log.Printf("[engine] index update %s: %v", path, err)
}
if searchMgr != nil {
searchMgr.IndexFile(path)
}
}()
})
}
if toolReg != nil {
if _, exists := toolReg.Lookup("symbol_search"); !exists {
toolReg.Register(tools.ToolSpec{
Expand Down Expand Up @@ -249,6 +263,7 @@ func New(opts Options) (*Engine, error) {
mcpMgr: mcpMgr,
mgr: mgr,
idxStore: idxStore,
searchMgr: searchMgr,
home: home,
workspace: workspace,
yolo: opts.Yolo,
Expand Down
29 changes: 29 additions & 0 deletions llm/fingerprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package llm

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
)

// PrefixFingerprint hashes the cacheable prefix: system messages plus tool
// definitions. Log it (RAKIT_DEBUG=1) to detect accidental cache busting.
func PrefixFingerprint(messages []LLMMessage, tools []ToolDefinition) string {
h := sha256.New()
for _, m := range messages {
if m.Role != RoleSystem {
continue
}
switch c := m.Content.(type) {
case string:
h.Write([]byte(c))
default:
b, _ := json.Marshal(c)
h.Write(b)
}
h.Write([]byte{'\n'})
}
b, _ := json.Marshal(tools)
h.Write(b)
return hex.EncodeToString(h.Sum(nil))[:16]
}
19 changes: 19 additions & 0 deletions llm/fingerprint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package llm

import "testing"

func TestPrefixFingerprintStability(t *testing.T) {
tools := []ToolDefinition{{Type: "function", Function: FunctionSpec{Name: "read"}}}
a := []LLMMessage{{Role: RoleSystem, Content: "system"}, {Role: RoleUser, Content: "hello"}}
b := []LLMMessage{{Role: RoleSystem, Content: "system"}, {Role: RoleUser, Content: "different"}}
if PrefixFingerprint(a, tools) != PrefixFingerprint(b, tools) {
t.Fatal("user content must not affect the prefix fingerprint")
}
c := []LLMMessage{{Role: RoleSystem, Content: "changed"}, {Role: RoleUser, Content: "hello"}}
if PrefixFingerprint(a, tools) == PrefixFingerprint(c, tools) {
t.Fatal("system change must change the fingerprint")
}
if PrefixFingerprint(a, tools) == PrefixFingerprint(a, nil) {
t.Fatal("tools change must change the fingerprint")
}
}
4 changes: 4 additions & 0 deletions llm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,10 @@ func (c *LLMClient) ChatWithProviderStream(
if len(tools) > 0 {
reqBody["tools"] = reqTools
}
if os.Getenv("RAKIT_DEBUG") != "" {
log.Printf("[cache] provider=%s model=%s tools=%d msgs=%d prefix=%s",
targetProviderID, model, len(tools), len(messages), PrefixFingerprint(messages, tools))
}

// Normalize reasoning effort ("off", "low", "medium", "high") so gateways
// like OpenAI and OpenRouter never receive unsupported values (e.g. "max").
Expand Down
50 changes: 44 additions & 6 deletions tools/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -46,11 +47,12 @@ type ToolSpec struct {
}

type Registry struct {
mu sync.RWMutex
tools map[string]ToolSpec
nextOrder int
mcpCaller MCPCaller
bg bgState
mu sync.RWMutex
tools map[string]ToolSpec
nextOrder int
mcpCaller MCPCaller
fileChange func(path string)
bg bgState
}

func NewRegistry(searchMgr *search.SearchManager) *Registry {
Expand Down Expand Up @@ -316,6 +318,38 @@ func (r *Registry) Definitions() []llm.ToolDefinition {
return defs
}

// SetFileChangeHandler installs a callback invoked after a successful
// file-mutating tool (write/edit/multiedit) with the absolute path. Hosts use
// it to update search/symbol indexes incrementally.
func (r *Registry) SetFileChangeHandler(fn func(path string)) {
r.mu.Lock()
r.fileChange = fn
r.mu.Unlock()
}

func (r *Registry) notifyFileChange(ctx context.Context, spec ToolSpec, args map[string]any) {
if spec.ReadOnly || spec.Category != "file" {
return
}
r.mu.RLock()
fn := r.fileChange
r.mu.RUnlock()
if fn == nil {
return
}
for _, key := range []string{"path", "file_path", "target_file", "file"} {
p, ok := args[key].(string)
if !ok || p == "" {
continue
}
if !filepath.IsAbs(p) {
p = filepath.Join(defaultCwd(ctx), p)
}
fn(filepath.Clean(p))
return
}
}

func (r *Registry) Lookup(name string) (ToolSpec, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
Expand Down Expand Up @@ -360,5 +394,9 @@ func (r *Registry) Execute(ctx context.Context, name string, rawArgs string) (in
args = make(map[string]interface{})
}

return spec.Handler(ctx, args)
res, err := spec.Handler(ctx, args)
if err == nil {
r.notifyFileChange(ctx, spec, args)
}
return res, err
}
Loading