diff --git a/agent/agent.go b/agent/agent.go index 78a76b8..c734557 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -319,6 +319,35 @@ func (ag *Agent) RenamePreamble(old, new string) { } } +// refreshToolListing replaces the "# Available Tools" section in the preamble +// with an updated listing. Called when tool_list detects directory changes. +func (ag *Agent) refreshToolListing(listing string) { + if ag.runtime == nil { + return + } + const marker = "\n# Available Tools\n\n" + if idx := strings.Index(ag.runtime.Preamble, marker); idx >= 0 { + ag.runtime.Preamble = ag.runtime.Preamble[:idx] + } + if listing != "" { + ag.runtime.Preamble += marker + listing + } +} + +// wireToolsChanged sets the OnToolsChanged callback on the tool server +// so the preamble stays in sync when tools are added/removed from disk. +func (ag *Agent) wireToolsChanged() { + if ag.runtime == nil || ag.runtime.ToolServer == nil { + return + } + type hasHook interface { + SetOnToolsChanged(func(string)) + } + if srv, ok := ag.runtime.ToolServer.(hasHook); ok { + srv.SetOnToolsChanged(ag.refreshToolListing) + } +} + // SaveFull persists the full session state (history + metadata) to the given path. func (ag *Agent) SaveFull(path, sessionID, cwd, remote string) error { if ag.history == nil { diff --git a/agent/commands.go b/agent/commands.go index e0af3db..cf820bd 100644 --- a/agent/commands.go +++ b/agent/commands.go @@ -203,6 +203,7 @@ func (ag *Agent) cmdAgent(_ context.Context, args []string) { ag.runtime = rt ag.agentName = name ag.history = nil + ag.wireToolsChanged() ag.notifyChange() for _, msg := range rt.Messages { ag.emit(infoEvent(msg)) diff --git a/agent/new.go b/agent/new.go index 2674d41..a638285 100644 --- a/agent/new.go +++ b/agent/new.go @@ -56,5 +56,6 @@ func NewAgent(cfg AgentCfg) *Agent { } ag.changeCond = sync.NewCond(&ag.changeMu) ag.turnError = ag.defaultTurnError + ag.wireToolsChanged() return ag } diff --git a/tools/tool.go b/tools/tool.go index 7bf1e27..d4ed254 100644 --- a/tools/tool.go +++ b/tools/tool.go @@ -28,6 +28,19 @@ func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (s } out.WriteString("\n") } + + // Notify the agent that the tool listing may have changed so the + // preamble stays in sync with the directory contents. + if srv.OnToolsChanged != nil { + var listing strings.Builder + for _, s := range summaries { + if s.Description != "" { + fmt.Fprintf(&listing, "- **%s** — %s\n", s.Name, s.Description) + } + } + srv.OnToolsChanged(listing.String()) + } + return strings.TrimRight(out.String(), "\n"), nil } diff --git a/toolsrv/accessors.go b/toolsrv/accessors.go index 1debffe..b2e0521 100644 --- a/toolsrv/accessors.go +++ b/toolsrv/accessors.go @@ -30,6 +30,11 @@ func (e *Server) InjectContent(content string) { } } +// SetOnToolsChanged sets the callback for tool directory changes. +func (e *Server) SetOnToolsChanged(fn func(string)) { + e.OnToolsChanged = fn +} + // ExecuteInSandbox runs a command in the sandbox. Exported for builtin tools. func (e *Server) ExecuteInSandbox(ctx context.Context, cmd, language string, timeout int, sandbox string, trusted bool, stdin string, detach bool) (string, error) { return e.executeWithStdin(ctx, cmd, language, timeout, sandbox, trusted, stdin, detach) diff --git a/toolsrv/registry.go b/toolsrv/registry.go index dbb725a..a444f8b 100644 --- a/toolsrv/registry.go +++ b/toolsrv/registry.go @@ -2,41 +2,40 @@ package toolsrv import ( "fmt" - "strings" - "sort" "os" "path/filepath" + "sort" + "strings" "sync" - - ) type Registry struct { mu sync.RWMutex - global map[string]ToolInfo sessions map[string]map[string]ToolInfo revisions map[string]uint64 } func NewRegistry() (*Registry, error) { r := &Registry{ - global: make(map[string]ToolInfo), sessions: make(map[string]map[string]ToolInfo), revisions: make(map[string]uint64), } - if err := r.Discover(); err != nil { - return nil, err + // Validate that the tools directory is readable. + dir := ToolsPath() + if _, err := os.ReadDir(dir); err != nil { + return nil, fmt.Errorf("read tools dir %s: %w", dir, err) } return r, nil } -func (r *Registry) Discover() error { +// discover scans the tools directory and returns all valid tool infos. +func (r *Registry) discover() map[string]ToolInfo { dir := ToolsPath() entries, err := os.ReadDir(dir) if err != nil { - return fmt.Errorf("read tools dir %s: %w", dir, err) + return nil } - global := make(map[string]ToolInfo) + tools := make(map[string]ToolInfo) for _, e := range entries { if e.IsDir() || e.Name() == "idx" || e.Name()[0] == '.' { continue @@ -49,19 +48,15 @@ func (r *Registry) Discover() error { if !strings.Contains(script, "ollie:prompt") { continue } - global[e.Name()] = ParseToolInfo(e.Name(), script) + tools[e.Name()] = ParseToolInfo(e.Name(), script) } - r.mu.Lock() - r.global = global - r.mu.Unlock() - return nil + return tools } func (r *Registry) Summaries() []ToolInfo { - r.mu.RLock() - defer r.mu.RUnlock() + tools := r.discover() var summaries []ToolInfo - for _, info := range r.global { + for _, info := range tools { summaries = append(summaries, ToolInfo{ Name: info.Name, Description: info.Description, @@ -72,9 +67,8 @@ func (r *Registry) Summaries() []ToolInfo { } func (r *Registry) Load(sessionID, name string) error { - r.mu.RLock() - tool, exists := r.global[name] - r.mu.RUnlock() + tools := r.discover() + tool, exists := tools[name] if !exists { return fmt.Errorf("tool not found: %s", name) } diff --git a/toolsrv/server.go b/toolsrv/server.go index 6caf1e2..6b5b84a 100644 --- a/toolsrv/server.go +++ b/toolsrv/server.go @@ -58,6 +58,10 @@ type Server struct { // should be injected into the agent's context. OnInjection func(content string) + // OnToolsChanged is called when tool_list detects the available tools + // have changed, providing the updated preamble-format listing. + OnToolsChanged func(listing string) + // rate limiting state (per-Server) rateLimitMu sync.Mutex validationFailures int