toolsrv: dynamic tool discovery, drop global cache
Registry.Summaries() and Load() now scan the tools directory on every call instead of reading from a startup cache. New tools dropped into the directory are immediately visible without restarting the server. When tool_list is called, the OnToolsChanged hook fires and updates the preamble's '# Available Tools' section in the live agent runtime. No agent reload or session restart required.
This commit is contained in:
parent
80d7e4436d
commit
10bd60980a
|
|
@ -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.
|
// SaveFull persists the full session state (history + metadata) to the given path.
|
||||||
func (ag *Agent) SaveFull(path, sessionID, cwd, remote string) error {
|
func (ag *Agent) SaveFull(path, sessionID, cwd, remote string) error {
|
||||||
if ag.history == nil {
|
if ag.history == nil {
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ func (ag *Agent) cmdAgent(_ context.Context, args []string) {
|
||||||
ag.runtime = rt
|
ag.runtime = rt
|
||||||
ag.agentName = name
|
ag.agentName = name
|
||||||
ag.history = nil
|
ag.history = nil
|
||||||
|
ag.wireToolsChanged()
|
||||||
ag.notifyChange()
|
ag.notifyChange()
|
||||||
for _, msg := range rt.Messages {
|
for _, msg := range rt.Messages {
|
||||||
ag.emit(infoEvent(msg))
|
ag.emit(infoEvent(msg))
|
||||||
|
|
|
||||||
|
|
@ -56,5 +56,6 @@ func NewAgent(cfg AgentCfg) *Agent {
|
||||||
}
|
}
|
||||||
ag.changeCond = sync.NewCond(&ag.changeMu)
|
ag.changeCond = sync.NewCond(&ag.changeMu)
|
||||||
ag.turnError = ag.defaultTurnError
|
ag.turnError = ag.defaultTurnError
|
||||||
|
ag.wireToolsChanged()
|
||||||
return ag
|
return ag
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,19 @@ func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (s
|
||||||
}
|
}
|
||||||
out.WriteString("\n")
|
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
|
return strings.TrimRight(out.String(), "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// 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) {
|
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)
|
return e.executeWithStdin(ctx, cmd, language, timeout, sandbox, trusted, stdin, detach)
|
||||||
|
|
|
||||||
|
|
@ -2,41 +2,40 @@ package toolsrv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"sort"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
global map[string]ToolInfo
|
|
||||||
sessions map[string]map[string]ToolInfo
|
sessions map[string]map[string]ToolInfo
|
||||||
revisions map[string]uint64
|
revisions map[string]uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistry() (*Registry, error) {
|
func NewRegistry() (*Registry, error) {
|
||||||
r := &Registry{
|
r := &Registry{
|
||||||
global: make(map[string]ToolInfo),
|
|
||||||
sessions: make(map[string]map[string]ToolInfo),
|
sessions: make(map[string]map[string]ToolInfo),
|
||||||
revisions: make(map[string]uint64),
|
revisions: make(map[string]uint64),
|
||||||
}
|
}
|
||||||
if err := r.Discover(); err != nil {
|
// Validate that the tools directory is readable.
|
||||||
return nil, err
|
dir := ToolsPath()
|
||||||
|
if _, err := os.ReadDir(dir); err != nil {
|
||||||
|
return nil, fmt.Errorf("read tools dir %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
return r, nil
|
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()
|
dir := ToolsPath()
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
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 {
|
for _, e := range entries {
|
||||||
if e.IsDir() || e.Name() == "idx" || e.Name()[0] == '.' {
|
if e.IsDir() || e.Name() == "idx" || e.Name()[0] == '.' {
|
||||||
continue
|
continue
|
||||||
|
|
@ -49,19 +48,15 @@ func (r *Registry) Discover() error {
|
||||||
if !strings.Contains(script, "ollie:prompt") {
|
if !strings.Contains(script, "ollie:prompt") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
global[e.Name()] = ParseToolInfo(e.Name(), script)
|
tools[e.Name()] = ParseToolInfo(e.Name(), script)
|
||||||
}
|
}
|
||||||
r.mu.Lock()
|
return tools
|
||||||
r.global = global
|
|
||||||
r.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) Summaries() []ToolInfo {
|
func (r *Registry) Summaries() []ToolInfo {
|
||||||
r.mu.RLock()
|
tools := r.discover()
|
||||||
defer r.mu.RUnlock()
|
|
||||||
var summaries []ToolInfo
|
var summaries []ToolInfo
|
||||||
for _, info := range r.global {
|
for _, info := range tools {
|
||||||
summaries = append(summaries, ToolInfo{
|
summaries = append(summaries, ToolInfo{
|
||||||
Name: info.Name,
|
Name: info.Name,
|
||||||
Description: info.Description,
|
Description: info.Description,
|
||||||
|
|
@ -72,9 +67,8 @@ func (r *Registry) Summaries() []ToolInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) Load(sessionID, name string) error {
|
func (r *Registry) Load(sessionID, name string) error {
|
||||||
r.mu.RLock()
|
tools := r.discover()
|
||||||
tool, exists := r.global[name]
|
tool, exists := tools[name]
|
||||||
r.mu.RUnlock()
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("tool not found: %s", name)
|
return fmt.Errorf("tool not found: %s", name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ type Server struct {
|
||||||
// should be injected into the agent's context.
|
// should be injected into the agent's context.
|
||||||
OnInjection func(content string)
|
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)
|
// rate limiting state (per-Server)
|
||||||
rateLimitMu sync.Mutex
|
rateLimitMu sync.Mutex
|
||||||
validationFailures int
|
validationFailures int
|
||||||
|
|
|
||||||
Reference in New Issue