all: replace toolsrv.Runner interface with *toolsrv.Conn

The agent always talks to a *Conn (never a *Server directly).
Replace the Runner interface with the concrete type throughout:

- Runtime.ToolServer is now *toolsrv.Conn
- BuildRuntime takes *toolsrv.Conn
- session.Config, SessionInfra, InfraConfig use *toolsrv.Conn
- All inline type assertions in agent.go deleted — methods called
  directly on *Conn (SetEnv, SetCWD, Close, Detach, ListDetachedRaw,
  SignalDetached, GetDetachedOutput, DismissDetached, SetAllowTools,
  SetOnToolsChanged, Ping, ToolRegistryRevision)
- Added Conn.ToolRegistryRevision() that returns 0 (remote connections
  don't support revision tracking; use push-based OnToolsChanged instead)
- Deleted the Runner interface and its test
- Renamed LoadToolOnRunner → LoadToolOnConn
This commit is contained in:
Ollie Agent 2026-08-08 15:10:01 +02:00
parent 3b83b9d373
commit 06a996e967
11 changed files with 78 additions and 188 deletions

View File

@ -28,7 +28,7 @@ type Agent struct {
systemPrompt string // system prompt for /agent reloads systemPrompt string // system prompt for /agent reloads
envBlock string // environment block for /agent reloads envBlock string // environment block for /agent reloads
promptEnvExtra []string // PRIME_* vars for prompt resolution promptEnvExtra []string // PRIME_* vars for prompt resolution
newToolServer func() toolsrv.Runner newToolServer func() *toolsrv.Conn
newBackend func(string) (backend.Backend, error) newBackend func(string) (backend.Backend, error)
currentAction atomic.Pointer[actionHandle] currentAction atomic.Pointer[actionHandle]
warnedContext bool warnedContext bool
@ -239,7 +239,7 @@ func (ag *Agent) SetOutput(h EventHandler) {
// SetToolServer updates the tool server connection and factory. // SetToolServer updates the tool server connection and factory.
// Used when resuming a paused session that was restored without infra. // Used when resuming a paused session that was restored without infra.
func (ag *Agent) SetToolServer(newToolServer func() toolsrv.Runner, conn toolsrv.Runner) { func (ag *Agent) SetToolServer(newToolServer func() *toolsrv.Conn, conn *toolsrv.Conn) {
ag.newToolServer = newToolServer ag.newToolServer = newToolServer
if ag.runtime != nil { if ag.runtime != nil {
ag.runtime.ToolServer = conn ag.runtime.ToolServer = conn
@ -316,16 +316,12 @@ func (ag *Agent) SetSessionEnv(sessionID string) {
if ag.runtime.ToolServer == nil { if ag.runtime.ToolServer == nil {
return return
} }
if srv := ag.runtime.ToolServer; srv != nil { ag.runtime.ToolServer.SetEnv("OLLIE_SESSION_ID", sessionID)
if es, ok := srv.(interface{ SetEnv(string, string) }); ok {
es.SetEnv("OLLIE_SESSION_ID", sessionID)
if ag.id != "" { if ag.id != "" {
es.SetEnv("OLLIE_UNAME", ag.id) ag.runtime.ToolServer.SetEnv("OLLIE_UNAME", ag.id)
} }
if u := os.Getenv("USER"); u != "" { if u := os.Getenv("USER"); u != "" {
es.SetEnv("USER", u) ag.runtime.ToolServer.SetEnv("USER", u)
}
}
} }
} }
@ -334,11 +330,7 @@ func (ag *Agent) SetEnv(key, value string) {
if ag.runtime.ToolServer == nil { if ag.runtime.ToolServer == nil {
return return
} }
if srv := ag.runtime.ToolServer; srv != nil { ag.runtime.ToolServer.SetEnv(key, value)
if es, ok := srv.(interface{ SetEnv(string, string) }); ok {
es.SetEnv(key, value)
}
}
} }
// Close releases agent resources (dispatcher, execute server). // Close releases agent resources (dispatcher, execute server).
@ -346,11 +338,7 @@ func (ag *Agent) Close() {
if ag.runtime.ToolServer == nil { if ag.runtime.ToolServer == nil {
return return
} }
if srv := ag.runtime.ToolServer; srv != nil { ag.runtime.ToolServer.Close()
if c, ok := srv.(interface{ Close() }); ok {
c.Close()
}
}
} }
// SetCWD updates the agent's working directory, preamble references, and dispatcher. // SetCWD updates the agent's working directory, preamble references, and dispatcher.
@ -363,11 +351,7 @@ func (ag *Agent) SetCWD(dir string) {
ag.runtime.Preamble = strings.ReplaceAll(ag.runtime.Preamble, oldCwd, dir) ag.runtime.Preamble = strings.ReplaceAll(ag.runtime.Preamble, oldCwd, dir)
} }
if ag.runtime.ToolServer != nil { if ag.runtime.ToolServer != nil {
if srv := ag.runtime.ToolServer; srv != nil { ag.runtime.ToolServer.SetCWD(dir)
if ws, ok := srv.(interface{ SetCWD(string) }); ok {
ws.SetCWD(dir)
}
}
} }
ag.notifyChange() ag.notifyChange()
} }
@ -466,27 +450,18 @@ func (ag *Agent) wireToolsChanged() {
if ag.runtime == nil || ag.runtime.ToolServer == nil { if ag.runtime == nil || ag.runtime.ToolServer == nil {
return return
} }
type hasHook interface { ag.runtime.ToolServer.SetOnToolsChanged(ag.refreshToolListing)
SetOnToolsChanged(func(string))
}
if srv, ok := ag.runtime.ToolServer.(hasHook); ok {
srv.SetOnToolsChanged(ag.refreshToolListing)
}
} }
// toolsNeedRefresh reports whether the tool registry has changed since the // toolsNeedRefresh reports whether the tool registry has changed since the
// last time Tools was populated. If the tool server doesn't support revision // last time Tools was populated. If the tool server doesn't support revision
// tracking (e.g. remote Conn), this always returns true (safe fallback). // tracking (e.g. remote Conn returns 0), this always returns true (safe fallback).
func (ag *Agent) toolsNeedRefresh() bool { func (ag *Agent) toolsNeedRefresh() bool {
type revisionProvider interface { if ag.runtime.ToolServer == nil {
ToolRegistryRevision() uint64 return true
} }
srv, ok := ag.runtime.ToolServer.(revisionProvider) rev := ag.runtime.ToolServer.ToolRegistryRevision()
if !ok { if rev == 0 || rev != ag.runtime.toolRegistryRevision {
return true // no revision support — always refetch
}
rev := srv.ToolRegistryRevision()
if rev != ag.runtime.toolRegistryRevision {
ag.runtime.toolRegistryRevision = rev ag.runtime.toolRegistryRevision = rev
return true return true
} }
@ -577,7 +552,7 @@ func (ag *Agent) ListModels() []string {
// ToolServer returns the tool execution server, or nil if unavailable. // ToolServer returns the tool execution server, or nil if unavailable.
// Exported for use by the 9P filesystem layer to sync tool registries. // Exported for use by the 9P filesystem layer to sync tool registries.
func (ag *Agent) ToolServer() toolsrv.Runner { func (ag *Agent) ToolServer() *toolsrv.Conn {
return ag.runtime.ToolServer return ag.runtime.ToolServer
} }
@ -600,13 +575,11 @@ func (ag *Agent) BroadcastChange() {
// Detach detaches the current running process to background. // Detach detaches the current running process to background.
func (ag *Agent) Detach() bool { func (ag *Agent) Detach() bool {
if srv := ag.runtime.ToolServer; srv != nil { if ag.runtime.ToolServer == nil {
if d, ok := srv.(interface{ Detach() bool }); ok {
return d.Detach()
}
}
return false return false
} }
return ag.runtime.ToolServer.Detach()
}
// DetachedInfo describes a detached process. // DetachedInfo describes a detached process.
type DetachedInfo struct { type DetachedInfo struct {
@ -619,16 +592,10 @@ type DetachedInfo struct {
// ListDetached returns info about all detached processes. // ListDetached returns info about all detached processes.
func (ag *Agent) ListDetached() []DetachedInfo { func (ag *Agent) ListDetached() []DetachedInfo {
srv := ag.runtime.ToolServer if ag.runtime.ToolServer == nil {
if srv == nil {
return nil return nil
} }
type listDetacher interface{ ListDetachedRaw() []any } raw := ag.runtime.ToolServer.ListDetachedRaw()
ld, ok := srv.(listDetacher)
if !ok {
return nil
}
raw := ld.ListDetachedRaw()
out := make([]DetachedInfo, 0, len(raw)) out := make([]DetachedInfo, 0, len(raw))
for _, r := range raw { for _, r := range raw {
if m, ok := r.(map[string]any); ok { if m, ok := r.(map[string]any); ok {
@ -656,35 +623,24 @@ func (ag *Agent) ListDetached() []DetachedInfo {
// SignalDetached sends a signal to a detached process. // SignalDetached sends a signal to a detached process.
func (ag *Agent) SignalDetached(pid int, signal int) error { func (ag *Agent) SignalDetached(pid int, signal int) error {
if srv := ag.runtime.ToolServer; srv != nil { if ag.runtime.ToolServer == nil {
type signaler interface {
SignalDetached(int, syscall.Signal) error
}
if sg, ok := srv.(signaler); ok {
return sg.SignalDetached(pid, syscall.Signal(signal))
}
}
return fmt.Errorf("no execute server available") return fmt.Errorf("no execute server available")
} }
return ag.runtime.ToolServer.SignalDetached(pid, syscall.Signal(signal))
}
// GetDetachedOutput reads output from a detached process. // GetDetachedOutput reads output from a detached process.
func (ag *Agent) GetDetachedOutput(pid int) (string, error) { func (ag *Agent) GetDetachedOutput(pid int) (string, error) {
if srv := ag.runtime.ToolServer; srv != nil { if ag.runtime.ToolServer == nil {
type outputGetter interface{ GetDetachedOutput(int) (string, error) }
if og, ok := srv.(outputGetter); ok {
return og.GetDetachedOutput(pid)
}
}
return "", fmt.Errorf("no execute server available") return "", fmt.Errorf("no execute server available")
} }
return ag.runtime.ToolServer.GetDetachedOutput(pid)
}
// DismissDetached removes a finished detached process. // DismissDetached removes a finished detached process.
func (ag *Agent) DismissDetached(pid int) bool { func (ag *Agent) DismissDetached(pid int) bool {
if srv := ag.runtime.ToolServer; srv != nil { if ag.runtime.ToolServer == nil {
type dismisser interface{ DismissDetached(int) bool }
if d, ok := srv.(dismisser); ok {
return d.DismissDetached(pid)
}
}
return false return false
} }
return ag.runtime.ToolServer.DismissDetached(pid)
}

View File

@ -18,7 +18,7 @@ import (
// env provides additional environment variables injected into prompt resolution // env provides additional environment variables injected into prompt resolution
// subprocesses (e.g. OLLIE_SESSION_ID=xxx). // subprocesses (e.g. OLLIE_SESSION_ID=xxx).
// The caller is responsible for registering all servers on d before calling this. // The caller is responsible for registering all servers on d before calling this.
func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string, systemPrompt, envBlock string) *Runtime { func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string, systemPrompt, envBlock string) *Runtime {
var messages []string var messages []string
var allToolInfos []toolsrv.ToolInfo var allToolInfos []toolsrv.ToolInfo
@ -45,10 +45,8 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
} }
genParams = cfg.GenParams() genParams = cfg.GenParams()
maxSteps = cfg.MaxSteps maxSteps = cfg.MaxSteps
if len(cfg.AllowTools) > 0 { if len(cfg.AllowTools) > 0 && srv != nil {
if rs, ok := srv.(interface{ SetAllowTools([]string) }); ok { srv.SetAllowTools(cfg.AllowTools)
rs.SetAllowTools(cfg.AllowTools)
}
} }
} }

View File

@ -20,7 +20,7 @@ type AgentCfg struct {
SystemPrompt string SystemPrompt string
EnvBlock string EnvBlock string
PromptEnvExtra []string PromptEnvExtra []string
NewToolServer func() toolsrv.Runner NewToolServer func() *toolsrv.Conn
NewBackend func(string) (backend.Backend, error) NewBackend func(string) (backend.Backend, error)
Output EventHandler Output EventHandler
Log *olog.Logger Log *olog.Logger

View File

@ -11,7 +11,7 @@ import (
// agents replaces it atomically. // agents replaces it atomically.
type Runtime struct { type Runtime struct {
Backend backend.Backend Backend backend.Backend
ToolServer toolsrv.Runner // the execute server (tool runtime, env, cwd) ToolServer *toolsrv.Conn // the tool server connection (tool runtime, env, cwd)
Preamble string // compiled system prompt Preamble string // compiled system prompt
Tools []backend.Tool Tools []backend.Tool
ToolMeta map[string]toolsrv.ToolInfo // keyed by tool name — source of truth for all per-tool metadata ToolMeta map[string]toolsrv.ToolInfo // keyed by tool name — source of truth for all per-tool metadata

View File

@ -65,7 +65,7 @@ func (s *SessionNode) Ctx() context.Context {
return s.Core.Ctx() return s.Core.Ctx()
} }
func (s *SessionNode) ToolsConn() toolsrv.Runner { func (s *SessionNode) ToolsConn() *toolsrv.Conn {
return s.Core.ToolsConn() return s.Core.ToolsConn()
} }

View File

@ -277,8 +277,8 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
env := []string{"OLLIE_SESSION_ID=" + ps.ID, "OLLIE_UNAME=" + pa.ID} env := []string{"OLLIE_SESSION_ID=" + ps.ID, "OLLIE_UNAME=" + pa.ID}
env = append(env, promptEnv...) env = append(env, promptEnv...)
var toolsConn toolsrv.Runner var toolsConn *toolsrv.Conn
var newToolServer func() toolsrv.Runner var newToolServer func() *toolsrv.Conn
if infra != nil { if infra != nil {
toolsConn = infra.ToolsConn toolsConn = infra.ToolsConn
newToolServer = infra.NewToolServer newToolServer = infra.NewToolServer

View File

@ -31,7 +31,7 @@ type Config struct {
CWD string CWD string
History *agent.History History *agent.History
Runtime *agent.Runtime Runtime *agent.Runtime
NewToolServer func() toolsrv.Runner NewToolServer func() *toolsrv.Conn
NewBackend func(string) (backend.Backend, error) NewBackend func(string) (backend.Backend, error)
Log *olog.Logger Log *olog.Logger
MaxSteps int MaxSteps int
@ -72,7 +72,7 @@ type Session struct {
// Tool server lifecycle // Tool server lifecycle
proc *toolsrv.Process // toolsrv subprocess proc *toolsrv.Process // toolsrv subprocess
keeper *toolsrv.ProcessKeeper // resilient process manager keeper *toolsrv.ProcessKeeper // resilient process manager
toolsConn toolsrv.Runner // multiplexed RPC connection toolsConn *toolsrv.Conn // multiplexed RPC connection
paused bool // true when paused paused bool // true when paused
// Tool restrictions // Tool restrictions
@ -426,14 +426,14 @@ func (s *Session) SetKeeper(keeper *toolsrv.ProcessKeeper) {
} }
// ToolsConn returns the tool server connection. // ToolsConn returns the tool server connection.
func (s *Session) ToolsConn() toolsrv.Runner { func (s *Session) ToolsConn() *toolsrv.Conn {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
return s.toolsConn return s.toolsConn
} }
// SetToolsConn sets the tool server connection. // SetToolsConn sets the tool server connection.
func (s *Session) SetToolsConn(conn toolsrv.Runner) { func (s *Session) SetToolsConn(conn *toolsrv.Conn) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.toolsConn = conn s.toolsConn = conn
@ -463,10 +463,7 @@ func (s *Session) IsConnected() bool {
if paused || conn == nil { if paused || conn == nil {
return false return false
} }
if pinger, ok := conn.(interface{ Ping() error }); ok { return conn.Ping() == nil
return pinger.Ping() == nil
}
return true
} }
// Pause stops the tool server to save resources. // Pause stops the tool server to save resources.
@ -490,9 +487,7 @@ func (s *Session) Pause() error {
} }
// Close any existing connection. // Close any existing connection.
if s.toolsConn != nil { if s.toolsConn != nil {
if c, ok := s.toolsConn.(interface{ Close() }); ok { s.toolsConn.Close()
c.Close()
}
s.toolsConn = nil s.toolsConn = nil
} }
s.paused = true s.paused = true
@ -591,24 +586,24 @@ func (s *Session) LoadTool(name string, ag *agent.Agent) error {
if blocked { if blocked {
return fmt.Errorf("tool %q is disallowed for this session", name) return fmt.Errorf("tool %q is disallowed for this session", name)
} }
var runner toolsrv.Runner var conn *toolsrv.Conn
if s.toolsConn != nil { if s.toolsConn != nil {
runner = s.toolsConn conn = s.toolsConn
} else if ag != nil { } else if ag != nil {
runner = ag.ToolServer() conn = ag.ToolServer()
} }
return LoadToolOnRunner(runner, name) return LoadToolOnConn(conn, name)
} }
// LoadToolOnRunner loads a tool on the given runner. // LoadToolOnConn loads a tool on the given connection.
func LoadToolOnRunner(runner toolsrv.Runner, name string) error { func LoadToolOnConn(conn *toolsrv.Conn, name string) error {
if runner == nil { if conn == nil {
return nil return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
args, _ := json.Marshal(map[string]string{"name": name}) args, _ := json.Marshal(map[string]string{"name": name})
if _, err := runner.CallTool(ctx, "tool_load", json.RawMessage(args)); err != nil { if _, err := conn.CallTool(ctx, "tool_load", json.RawMessage(args)); err != nil {
return fmt.Errorf("remote load: %w", err) return fmt.Errorf("remote load: %w", err)
} }
return nil return nil

View File

@ -15,8 +15,8 @@ import (
type SessionInfra struct { type SessionInfra struct {
Proc *toolsrv.Process Proc *toolsrv.Process
Keeper *toolsrv.ProcessKeeper Keeper *toolsrv.ProcessKeeper
ToolsConn toolsrv.Runner ToolsConn *toolsrv.Conn
NewToolServer func() toolsrv.Runner NewToolServer func() *toolsrv.Conn
PromptEnv []string PromptEnv []string
} }
@ -36,7 +36,7 @@ type ToolServerConfig struct {
type InfraConfig struct { type InfraConfig struct {
Proc *toolsrv.Process Proc *toolsrv.Process
Keeper *toolsrv.ProcessKeeper Keeper *toolsrv.ProcessKeeper
ToolsConn toolsrv.Runner ToolsConn *toolsrv.Conn
} }
// SetupToolServer spawns the tool server process and creates the infrastructure. // SetupToolServer spawns the tool server process and creates the infrastructure.
@ -47,9 +47,9 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
// Reuse existing infrastructure if provided. // Reuse existing infrastructure if provided.
if cfg.ReuseFrom != nil { if cfg.ReuseFrom != nil {
r := cfg.ReuseFrom r := cfg.ReuseFrom
var newToolServer func() toolsrv.Runner var newToolServer func() *toolsrv.Conn
if r.Keeper != nil { if r.Keeper != nil {
newToolServer = func() toolsrv.Runner { newToolServer = func() *toolsrv.Conn {
conn, err := r.Keeper.Dial() conn, err := r.Keeper.Dial()
if err != nil { if err != nil {
return nil return nil
@ -57,13 +57,13 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
return conn return conn
} }
} else { } else {
newToolServer = func() toolsrv.Runner { newToolServer = func() *toolsrv.Conn {
return r.ToolsConn return r.ToolsConn
} }
} }
// Update CWD if the connection supports it. // Update CWD on the reused connection.
if cwc, ok := r.ToolsConn.(interface{ SetCWD(string) }); ok { if r.ToolsConn != nil {
cwc.SetCWD(cfg.CWD) r.ToolsConn.SetCWD(cfg.CWD)
} }
return &SessionInfra{ return &SessionInfra{
Proc: r.Proc, Proc: r.Proc,
@ -75,7 +75,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
} }
var proc *toolsrv.Process var proc *toolsrv.Process
var keeper *toolsrv.ProcessKeeper var keeper *toolsrv.ProcessKeeper
var newToolServer func() toolsrv.Runner var newToolServer func() *toolsrv.Conn
var promptEnv []string var promptEnv []string
var err error var err error
@ -93,7 +93,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
CWD: cfg.CWD, CWD: cfg.CWD,
}) })
}) })
newToolServer = func() toolsrv.Runner { newToolServer = func() *toolsrv.Conn {
conn, err := keeper.Dial() conn, err := keeper.Dial()
if err != nil { if err != nil {
return nil return nil
@ -117,7 +117,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
keeper = toolsrv.NewProcessKeeper(cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) { keeper = toolsrv.NewProcessKeeper(cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) {
return toolsrv.Spawn(ctx, cfg.CWD, dialOpts...) return toolsrv.Spawn(ctx, cfg.CWD, dialOpts...)
}) })
newToolServer = func() toolsrv.Runner { newToolServer = func() *toolsrv.Conn {
conn, err := keeper.Dial() conn, err := keeper.Dial()
if err != nil { if err != nil {
return nil return nil
@ -178,24 +178,22 @@ func BuildPromptLayers(cfg *agent.AgentConfig, cwd, sessID, uname string, prompt
// LoadAutoLoadTools loads all tools from the agent config's autoLoad list. // LoadAutoLoadTools loads all tools from the agent config's autoLoad list.
// It first sends the session ID and uname to the tool server so that // It first sends the session ID and uname to the tool server so that
// ollie-remote can attach its tool registry before processing tool_load RPCs. // ollie-remote can attach its tool registry before processing tool_load RPCs.
func LoadAutoLoadTools(cfg *agent.AgentConfig, runner toolsrv.Runner, sessID, uname string, logError func(string, ...any)) { func LoadAutoLoadTools(cfg *agent.AgentConfig, conn *toolsrv.Conn, sessID, uname string, logError func(string, ...any)) {
if cfg == nil || runner == nil { if cfg == nil || conn == nil {
return return
} }
// Send session ID to the tool server first — ollie-remote uses this // Send session ID to the tool server first — ollie-remote uses this
// to attach its tool registry via the OnEnvSet callback. // to attach its tool registry via the OnEnvSet callback.
if es, ok := runner.(interface{ SetEnv(string, string) }); ok {
if sessID != "" { if sessID != "" {
es.SetEnv("OLLIE_SESSION_ID", sessID) conn.SetEnv("OLLIE_SESSION_ID", sessID)
} }
if uname != "" { if uname != "" {
es.SetEnv("OLLIE_UNAME", uname) conn.SetEnv("OLLIE_UNAME", uname)
}
} }
for _, tl := range cfg.AutoLoad { for _, tl := range cfg.AutoLoad {
if err := LoadToolOnRunner(runner, tl); err != nil { if err := LoadToolOnConn(conn, tl); err != nil {
if logError != nil { if logError != nil {
logError("autoLoad tool %q: %v", tl, err) logError("autoLoad tool %q: %v", tl, err)
} }

View File

@ -103,6 +103,10 @@ func (c *Conn) SetOnToolsChanged(fn func(string)) {
c.pendingMu.Unlock() c.pendingMu.Unlock()
} }
// ToolRegistryRevision returns 0 for remote connections (revision tracking
// is not supported over RPC; use SetOnToolsChanged for push-based refresh).
func (c *Conn) ToolRegistryRevision() uint64 { return 0 }
func (c *Conn) Detach() bool { func (c *Conn) Detach() bool {
resp, err := c.call("detach", nil) resp, err := c.call("detach", nil)
if err != nil { if err != nil {

View File

@ -3,7 +3,6 @@
package toolsrv package toolsrv
import ( import (
"context"
"encoding/json" "encoding/json"
) )
@ -29,10 +28,3 @@ type ToolInfo struct {
// the soft step-budget guardrail. // the soft step-budget guardrail.
ResetsCounter bool ResetsCounter bool
} }
// Runner is the minimal interface satisfied by any tool server (local or remote).
// Consumers that need polymorphism over Server and RemoteServer use this.
type Runner interface {
ListTools() ([]ToolInfo, error)
CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error)
}

View File

@ -1,53 +0,0 @@
package toolsrv_test
import (
"context"
"encoding/json"
"testing"
"ollie/toolsrv"
)
// stubRunner is a minimal Runner used to verify the contract.
type stubRunner struct {
name string
tools []toolsrv.ToolInfo
}
func (s *stubRunner) ListTools() ([]toolsrv.ToolInfo, error) { return s.tools, nil }
func (s *stubRunner) CallTool(_ context.Context, tool string, _ json.RawMessage) (json.RawMessage, error) {
return json.RawMessage(`{"tool":"` + tool + `"}`), nil
}
func newStub(name string, toolNames ...string) *stubRunner {
var ti []toolsrv.ToolInfo
for _, n := range toolNames {
ti = append(ti, toolsrv.ToolInfo{Name: n, Description: n + " desc"})
}
return &stubRunner{name: name, tools: ti}
}
// checkRunnerContract verifies Runner invariants.
func checkRunnerContract(t *testing.T, r toolsrv.Runner) {
t.Helper()
tl, err := r.ListTools()
if err != nil {
t.Fatalf("ListTools: %v", err)
}
if tl == nil {
t.Fatal("ListTools returned nil")
}
for _, ti := range tl {
res, err := r.CallTool(context.Background(), ti.Name, json.RawMessage(`{}`))
if err != nil {
t.Errorf("CallTool(%q): %v", ti.Name, err)
}
if len(res) == 0 {
t.Errorf("CallTool(%q) returned empty result", ti.Name)
}
}
}
func TestStubRunnerContract(t *testing.T) {
checkRunnerContract(t, newStub("s", "a", "b"))
}