refactor toolsrv into client protocol and metadata packages
This commit is contained in:
parent
e972b5666a
commit
81933e5281
|
|
@ -27,8 +27,9 @@ import (
|
|||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"9fans.net/go/plan9"
|
||||
p9client "ollie/lib9p/client"
|
||||
|
||||
"9fans.net/go/plan9"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import (
|
|||
"fmt"
|
||||
"sync"
|
||||
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
"ollie/toolsrv/protocol"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
// bypassNotifier handles desktop notifications for bypass requests.
|
||||
// When a notification action is clicked, it writes the resolution directly to toolsrv.
|
||||
// When a notification action is clicked, it writes the resolution directly to toolsrvclient.
|
||||
type bypassNotifier struct {
|
||||
conn *dbus.Conn
|
||||
|
||||
|
|
@ -23,7 +25,7 @@ type pendingRequest struct {
|
|||
}
|
||||
|
||||
type sessionRef struct {
|
||||
dialFn func() *toolsrv.Conn
|
||||
dialFn func() *toolsrvclient.Conn
|
||||
}
|
||||
|
||||
var notifier *bypassNotifier
|
||||
|
|
@ -50,15 +52,15 @@ func initBypassNotifier(conn *dbus.Conn) {
|
|||
}
|
||||
|
||||
// notifyBypass sends a desktop notification for a bypass request.
|
||||
// When the user responds, it writes the resolution directly to toolsrv.
|
||||
func notifyBypass(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) {
|
||||
// When the user responds, it writes the resolution directly to toolsrvclient.
|
||||
func notifyBypass(req *protocol.BypassRequest, sessionID string, dialFn func() *toolsrvclient.Conn) {
|
||||
if notifier == nil {
|
||||
return
|
||||
}
|
||||
notifier.sendNotification(req, sessionID, dialFn)
|
||||
}
|
||||
|
||||
func (n *bypassNotifier) sendNotification(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) {
|
||||
func (n *bypassNotifier) sendNotification(req *protocol.BypassRequest, sessionID string, dialFn func() *toolsrvclient.Conn) {
|
||||
obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
||||
|
||||
summary := "Elevation Request"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
lib9p "ollie/lib9p/client"
|
||||
olog "ollie/log"
|
||||
"ollie/paths"
|
||||
"ollie/toolsrv"
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
)
|
||||
|
||||
// Agent holds the state of the current Agent entity (the "agent"
|
||||
|
|
@ -28,7 +28,7 @@ type Agent struct {
|
|||
agentsDir string
|
||||
systemPrompt string // system prompt for /agent reloads
|
||||
envBlock string // environment block for /agent reloads
|
||||
newToolServer func() *toolsrv.Conn
|
||||
newToolServer func() *toolsrvclient.Conn
|
||||
newBackend func(string) (backend.Backend, error)
|
||||
currentAction atomic.Pointer[actionHandle]
|
||||
warnedContext bool
|
||||
|
|
@ -331,7 +331,7 @@ func (ag *Agent) emit(ev Event) {
|
|||
|
||||
// SetToolServer updates the tool server connection and factory.
|
||||
// Used when resuming a paused session that was restored without infra.
|
||||
func (ag *Agent) SetToolServer(newToolServer func() *toolsrv.Conn, conn *toolsrv.Conn) {
|
||||
func (ag *Agent) SetToolServer(newToolServer func() *toolsrvclient.Conn, conn *toolsrvclient.Conn) {
|
||||
ag.newToolServer = newToolServer
|
||||
if ag.runtime != nil {
|
||||
ag.runtime.ToolServer = conn
|
||||
|
|
@ -623,7 +623,7 @@ func (ag *Agent) ListModels() []string {
|
|||
|
||||
// ToolServer returns the tool execution server, or nil if unavailable.
|
||||
// Exported for use by the 9P filesystem layer to sync tool registries.
|
||||
func (ag *Agent) ToolServer() *toolsrv.Conn {
|
||||
func (ag *Agent) ToolServer() *toolsrvclient.Conn {
|
||||
return ag.runtime.ToolServer
|
||||
}
|
||||
|
||||
|
|
@ -688,7 +688,7 @@ type AgentParams struct {
|
|||
Cwd string // working directory for tool execution
|
||||
SystemPrompt string
|
||||
EnvBlock string
|
||||
NewToolServer func() *toolsrv.Conn
|
||||
NewToolServer func() *toolsrvclient.Conn
|
||||
NewBackend func(string) (backend.Backend, error)
|
||||
Log *olog.Logger
|
||||
StartupMsgs []string
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
func TestRefreshToolListing(t *testing.T) {
|
||||
infos := []toolsrv.ToolInfo{
|
||||
infos := []protocol.ToolInfo{
|
||||
{
|
||||
Name: "new_tool",
|
||||
Description: "New description",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
"ollie/cmd/olliesrv/internal/backend"
|
||||
"ollie/paths"
|
||||
"ollie/toolsrv"
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
)
|
||||
|
||||
const maxTransientRetries = 3
|
||||
|
|
@ -599,7 +599,7 @@ func (ag *Agent) execOne(ctx context.Context, call backend.ToolCall) (toolResult
|
|||
ag.emit(Event{Role: "tool", Name: call.Name, Content: result, OutputFormat: toolOutputFormat(rt, call.Name)})
|
||||
return toolResult{ToolCallID: call.ID, Name: call.Name, Content: result, IsError: true}, true
|
||||
}
|
||||
var rlErr *toolsrv.RateLimitedError
|
||||
var rlErr *toolsrvclient.RateLimitedError
|
||||
if errors.As(err, &rlErr) {
|
||||
result = fmt.Sprintf("error: shell is rate-limited — blocked for %v. Do not call shell() again until the block expires. Use other tools or wait.", rlErr.Remaining)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import (
|
|||
"testing"
|
||||
|
||||
"ollie/cmd/olliesrv/internal/backend"
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
func TestToolConflictKeys(t *testing.T) {
|
||||
rt := &Runtime{
|
||||
ToolMeta: map[string]toolsrv.ToolInfo{
|
||||
ToolMeta: map[string]protocol.ToolInfo{
|
||||
"file_read": {Name: "file_read", Scope: "read"},
|
||||
"file_grep": {Name: "file_grep", Scope: "read"},
|
||||
"file_edit": {Name: "file_edit", Scope: "write"},
|
||||
|
|
@ -80,7 +80,7 @@ func TestExecToolCallsBatching(t *testing.T) {
|
|||
// Verify the batching logic groups correctly without actually executing.
|
||||
// We test by inspecting which calls would be grouped together.
|
||||
rt := &Runtime{
|
||||
ToolMeta: map[string]toolsrv.ToolInfo{
|
||||
ToolMeta: map[string]protocol.ToolInfo{
|
||||
"file_read": {Name: "file_read", Scope: "read"},
|
||||
"file_edit": {Name: "file_edit", Scope: "write"},
|
||||
"file_write": {Name: "file_write", Scope: "write"},
|
||||
|
|
@ -144,7 +144,7 @@ func TestExecToolCallsBatching(t *testing.T) {
|
|||
|
||||
func TestExecToolCallsSamePathSerializes(t *testing.T) {
|
||||
rt := &Runtime{
|
||||
ToolMeta: map[string]toolsrv.ToolInfo{
|
||||
ToolMeta: map[string]protocol.ToolInfo{
|
||||
"file_edit": {Name: "file_edit", Scope: "write"},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import (
|
|||
|
||||
"ollie/cmd/olliesrv/internal/backend"
|
||||
"ollie/cmd/olliesrv/internal/prompts"
|
||||
"ollie/toolsrv"
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// --- Preamble sections ---
|
||||
|
|
@ -92,8 +93,8 @@ func EnvironmentBlock(cwd, platform string, isGitRepo bool, sandbox string) stri
|
|||
// renderTools produces a single combined section: a one-line summary per tool
|
||||
// followed immediately by its documentation (if any). This keeps the listing
|
||||
// and usage instructions adjacent so models don't lose context between them.
|
||||
func RenderTools(infos []toolsrv.ToolInfo) string {
|
||||
infos = append([]toolsrv.ToolInfo(nil), infos...)
|
||||
func RenderTools(infos []protocol.ToolInfo) string {
|
||||
infos = append([]protocol.ToolInfo(nil), infos...)
|
||||
sort.SliceStable(infos, func(i, j int) bool {
|
||||
if infos[i].Name == infos[j].Name {
|
||||
return infos[i].Server < infos[j].Server
|
||||
|
|
@ -138,10 +139,10 @@ func RenderTools(infos []toolsrv.ToolInfo) string {
|
|||
// agents replaces it atomically.
|
||||
type Runtime struct {
|
||||
Backend backend.Backend
|
||||
ToolServer *toolsrv.Conn
|
||||
ToolServer *toolsrvclient.Conn
|
||||
Preamble *Preamble
|
||||
Tools []backend.Tool
|
||||
ToolMeta map[string]toolsrv.ToolInfo
|
||||
ToolMeta map[string]protocol.ToolInfo
|
||||
Exec toolExecutor
|
||||
GenParams backend.GenerationParams
|
||||
MaxSteps int
|
||||
|
|
@ -160,10 +161,10 @@ func (rt *Runtime) PreambleString() string {
|
|||
|
||||
// BuildRuntime constructs a Runtime from a pre-configured tool server and
|
||||
// optional agent config.
|
||||
func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string, systemPrompt, envBlock string) *Runtime {
|
||||
func BuildRuntime(cfg *AgentConfig, srv *toolsrvclient.Conn, cwd string, env []string, systemPrompt, envBlock string) *Runtime {
|
||||
var messages []string
|
||||
|
||||
var allToolInfos []toolsrv.ToolInfo
|
||||
var allToolInfos []protocol.ToolInfo
|
||||
var allTools []backend.Tool
|
||||
|
||||
if srv != nil && (cfg == nil || cfg.ToolsEnabled()) {
|
||||
|
|
@ -210,7 +211,7 @@ func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string,
|
|||
return text, blocks, nil
|
||||
}
|
||||
|
||||
toolMeta := make(map[string]toolsrv.ToolInfo, len(allToolInfos))
|
||||
toolMeta := make(map[string]protocol.ToolInfo, len(allToolInfos))
|
||||
for _, ti := range allToolInfos {
|
||||
toolMeta[ti.Name] = ti
|
||||
}
|
||||
|
|
@ -250,7 +251,7 @@ func ResolveSystemPrompt(cfg *AgentConfig) string {
|
|||
return prompts.DefaultSystemPrompt
|
||||
}
|
||||
|
||||
func toolInfosToBackend(infos []toolsrv.ToolInfo) []backend.Tool {
|
||||
func toolInfosToBackend(infos []protocol.ToolInfo) []backend.Tool {
|
||||
out := make([]backend.Tool, len(infos))
|
||||
for i, t := range infos {
|
||||
out[i] = backend.Tool{
|
||||
|
|
@ -308,9 +309,9 @@ func injectDispatchFlags(schema json.RawMessage) json.RawMessage {
|
|||
// extractToolResult parses the tool server's JSON-RPC response into text
|
||||
// and optional content blocks.
|
||||
//
|
||||
// See toolsrv.ToolResult for the wire format.
|
||||
// See protocol.ToolResult for the wire format.
|
||||
func extractToolResult(raw json.RawMessage) (text string, contentBlocks []backend.ContentBlock, isError bool) {
|
||||
var result toolsrv.ToolResult
|
||||
var result protocol.ToolResult
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return string(raw), nil, false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
func TestEnvironmentBlockStable(t *testing.T) {
|
||||
|
|
@ -19,7 +19,7 @@ func TestEnvironmentBlockStable(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPreambleSections(t *testing.T) {
|
||||
toolInfos := []toolsrv.ToolInfo{
|
||||
toolInfos := []protocol.ToolInfo{
|
||||
{Name: "file_read", Description: "Read a file with line numbers", Prompt: "**file_read**\n\nRead a file with line numbers...", InputSchema: []byte(`{"type":"object"}`)},
|
||||
{Name: "file_write", Description: "Create or overwrite a file", Prompt: "**file_write**\n\nCreate or overwrite a file...", InputSchema: []byte(`{"type":"object"}`), Server: "remote"},
|
||||
{Name: "shell", Description: "Execute shell command", InputSchema: []byte(`{}`)},
|
||||
|
|
@ -41,9 +41,9 @@ func TestPreambleSections(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRenderToolsStableOrder(t *testing.T) {
|
||||
infos := []toolsrv.ToolInfo{{Name: "z_tool", Description: "z"}, {Name: "a_tool", Description: "a"}}
|
||||
infos := []protocol.ToolInfo{{Name: "z_tool", Description: "z"}, {Name: "a_tool", Description: "a"}}
|
||||
first := RenderTools(infos)
|
||||
second := RenderTools([]toolsrv.ToolInfo{infos[1], infos[0]})
|
||||
second := RenderTools([]protocol.ToolInfo{infos[1], infos[0]})
|
||||
if first != second || strings.Index(first, "a_tool") > strings.Index(first, "z_tool") {
|
||||
t.Fatalf("tool rendering is not deterministic: %q", first)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
"ollie/cmd/olliesrv/internal/backend"
|
||||
"ollie/cmd/olliesrv/internal/metrics"
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// Submit processes one line of user input: it starts an agent turn that streams
|
||||
|
|
@ -131,7 +131,7 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
|||
return ""
|
||||
}
|
||||
ag.runtime.Tools = toolInfosToBackend(ti)
|
||||
meta := make(map[string]toolsrv.ToolInfo, len(ti))
|
||||
meta := make(map[string]protocol.ToolInfo, len(ti))
|
||||
for _, info := range ti {
|
||||
meta[info.Name] = info
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,3 @@ func NewGemini(apiKey string) (*OpenAIBackend, error) {
|
|||
b.model = geminiDefaultModel
|
||||
return b, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1078,7 +1078,6 @@ func kiroRandomJitter() time.Duration {
|
|||
return time.Duration(n%uint32(kiroTokenExpiryJitter/time.Millisecond)) * time.Millisecond
|
||||
}
|
||||
|
||||
|
||||
// ── SQLite helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// Kiro supports two auth flows whose tokens live under different SQLite keys:
|
||||
|
|
@ -1474,5 +1473,3 @@ func newKiroAuthSource(apiKey string) (kiroAuthSource, error) {
|
|||
func kiroDefaultSQLiteKey() string {
|
||||
return "sqlite://" + filepath.Join(paths.UserDataHome(), "kiro-cli", "data.sqlite3")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -544,4 +544,3 @@ func (b *OpenAIBackend) ChatStream(ctx context.Context, messages []Message, tool
|
|||
|
||||
return streamRequest(b.client, httpReq, "openai", streamOpenAISSE)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ func TestOpenRouterClaude_LastToolCacheControl(t *testing.T) {
|
|||
}
|
||||
|
||||
// TestOpenRouterNonClaude_NoCacheControl verifies that a non-Claude model on
|
||||
// openrouter does NOT get cache_control injected into system messages or toolsrv.
|
||||
// openrouter does NOT get cache_control injected into system messages or toolsrvclient.
|
||||
func TestOpenRouterNonClaude_NoCacheControl(t *testing.T) {
|
||||
top := captureRawRequest(t, "openrouter", "mistral-7b",
|
||||
[]Message{{Role: "system", Content: "be helpful"}, {Role: "user", Content: "hi"}},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const (
|
|||
)
|
||||
|
||||
// Broker manages bypass request evaluation and policy.
|
||||
// It does NOT execute commands - that happens in toolsrv.
|
||||
// It does NOT execute commands - that happens in toolsrvclient.
|
||||
type Broker struct {
|
||||
policy *PolicyStore
|
||||
notify NotifyFunc
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package fs
|
|||
import (
|
||||
"context"
|
||||
|
||||
"ollie/virtfs"
|
||||
olog "ollie/log"
|
||||
"ollie/cmd/olliesrv/internal/session"
|
||||
olog "ollie/log"
|
||||
"ollie/virtfs"
|
||||
)
|
||||
|
||||
// Config carries all runtime dependencies for the filesystem tree.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
"ollie/cmd/olliesrv/internal/agent"
|
||||
"ollie/cmd/olliesrv/internal/session"
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/protocol"
|
||||
"ollie/virtfs"
|
||||
)
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ func parseAgentNewRequest(data []byte) (session.AgentParams, string, string) {
|
|||
continue
|
||||
}
|
||||
if k, v, ok := strings.Cut(tok, "="); ok {
|
||||
v = toolsrv.UnescapeValue(v)
|
||||
v = protocol.UnescapeValue(v)
|
||||
switch k {
|
||||
case "cwd":
|
||||
p.CWD = v
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import (
|
|||
"ollie/cmd/olliesrv/internal/toolclient"
|
||||
olog "ollie/log"
|
||||
"ollie/paths"
|
||||
"ollie/toolsrv"
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// Session owns all session-level state: identity, agents, tool server lifecycle.
|
||||
|
|
@ -39,7 +40,7 @@ type Session struct {
|
|||
// Mutable state protected by mu
|
||||
mu sync.RWMutex
|
||||
name string // friendly name
|
||||
toolsConn *toolsrv.Conn // multiplexed RPC connection
|
||||
toolsConn *toolsrvclient.Conn // multiplexed RPC connection
|
||||
paused bool
|
||||
|
||||
// Autosave
|
||||
|
|
@ -84,7 +85,7 @@ func (s *Session) SetName(name string) {
|
|||
}
|
||||
|
||||
// ToolsConn returns the tool server connection.
|
||||
func (s *Session) ToolsConn() *toolsrv.Conn {
|
||||
func (s *Session) ToolsConn() *toolsrvclient.Conn {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.toolsConn
|
||||
|
|
@ -92,7 +93,7 @@ func (s *Session) ToolsConn() *toolsrv.Conn {
|
|||
|
||||
// DialToolServer dials a fresh toolsrv connection via the keeper.
|
||||
// Caller is responsible for closing the returned conn.
|
||||
func (s *Session) DialToolServer() *toolsrv.Conn {
|
||||
func (s *Session) DialToolServer() *toolsrvclient.Conn {
|
||||
s.mu.RLock()
|
||||
k := s.Keeper
|
||||
s.mu.RUnlock()
|
||||
|
|
@ -107,7 +108,7 @@ func (s *Session) DialToolServer() *toolsrv.Conn {
|
|||
}
|
||||
|
||||
// SetToolsConn sets the tool server connection.
|
||||
func (s *Session) SetToolsConn(conn *toolsrv.Conn) {
|
||||
func (s *Session) SetToolsConn(conn *toolsrvclient.Conn) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.toolsConn = conn
|
||||
|
|
@ -115,7 +116,7 @@ func (s *Session) SetToolsConn(conn *toolsrv.Conn) {
|
|||
|
||||
// BypassNotifyFunc is called when a bypass request arrives.
|
||||
// It receives the request, session ID, and a dial function to get a connection for resolution.
|
||||
type BypassNotifyFunc func(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn)
|
||||
type BypassNotifyFunc func(req *protocol.BypassRequest, sessionID string, dialFn func() *toolsrvclient.Conn)
|
||||
|
||||
// StartBypassLoop starts a goroutine that reads bypass requests from toolsrv
|
||||
// and calls notifyFn for each one. This should be called after SetToolsConn.
|
||||
|
|
@ -151,7 +152,7 @@ func (s *Session) runBypassLoop(notifyFn BypassNotifyFunc) {
|
|||
|
||||
// Notify (non-blocking) - the notifier will write resolution when user responds
|
||||
if notifyFn != nil {
|
||||
notifyFn(req, s.ID, s.DialToolServer)
|
||||
notifyFn((*protocol.BypassRequest)(req), s.ID, s.DialToolServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -326,7 +327,7 @@ func (s *Session) Resume() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("resume agent dial: %w", err)
|
||||
}
|
||||
ag.SetToolServer(func() *toolsrv.Conn {
|
||||
ag.SetToolServer(func() *toolsrvclient.Conn {
|
||||
c, _ := s.Keeper.Dial()
|
||||
return c
|
||||
}, agConn)
|
||||
|
|
@ -392,7 +393,7 @@ func (s *Session) LoadTool(name string, ag *agent.Agent) error {
|
|||
}
|
||||
|
||||
// LoadToolOnConn loads a tool on the given connection.
|
||||
func LoadToolOnConn(conn *toolsrv.Conn, name string) error {
|
||||
func LoadToolOnConn(conn *toolsrvclient.Conn, name string) error {
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import (
|
|||
"fmt"
|
||||
|
||||
"ollie/cmd/olliesrv/internal/agent"
|
||||
"ollie/cmd/olliesrv/internal/prompts"
|
||||
"ollie/cmd/olliesrv/internal/toolclient"
|
||||
"ollie/paths"
|
||||
"ollie/cmd/olliesrv/internal/prompts"
|
||||
"ollie/toolsrv"
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
)
|
||||
|
||||
// SessionInfra holds the tool server infrastructure for a session.
|
||||
|
|
@ -16,8 +16,8 @@ import (
|
|||
type SessionInfra struct {
|
||||
Proc *toolclient.Process
|
||||
Keeper *toolclient.ProcessKeeper
|
||||
ToolsConn *toolsrv.Conn
|
||||
NewToolServer func() *toolsrv.Conn
|
||||
ToolsConn *toolsrvclient.Conn
|
||||
NewToolServer func() *toolsrvclient.Conn
|
||||
Platform string
|
||||
IsGitRepo bool
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ type ToolServerConfig struct {
|
|||
type InfraConfig struct {
|
||||
Proc *toolclient.Process
|
||||
Keeper *toolclient.ProcessKeeper
|
||||
ToolsConn *toolsrv.Conn
|
||||
ToolsConn *toolsrvclient.Conn
|
||||
}
|
||||
|
||||
// SetupToolServer spawns the tool server process and creates the infrastructure.
|
||||
|
|
@ -50,9 +50,9 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
|||
// Reuse existing infrastructure if provided.
|
||||
if cfg.ReuseFrom != nil {
|
||||
r := cfg.ReuseFrom
|
||||
var newToolServer func() *toolsrv.Conn
|
||||
var newToolServer func() *toolsrvclient.Conn
|
||||
if r.Keeper != nil {
|
||||
newToolServer = func() *toolsrv.Conn {
|
||||
newToolServer = func() *toolsrvclient.Conn {
|
||||
conn, err := r.Keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
|
|
@ -60,7 +60,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
|||
return conn
|
||||
}
|
||||
} else {
|
||||
newToolServer = func() *toolsrv.Conn {
|
||||
newToolServer = func() *toolsrvclient.Conn {
|
||||
return r.ToolsConn
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
|||
}
|
||||
var proc *toolclient.Process
|
||||
var keeper *toolclient.ProcessKeeper
|
||||
var newToolServer func() *toolsrv.Conn
|
||||
var newToolServer func() *toolsrvclient.Conn
|
||||
var err error
|
||||
var platform string
|
||||
var isGitRepo bool
|
||||
|
|
@ -102,7 +102,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
|||
Yolo: cfg.Yolo,
|
||||
})
|
||||
})
|
||||
newToolServer = func() *toolsrv.Conn {
|
||||
newToolServer = func() *toolsrvclient.Conn {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
|
|
@ -126,7 +126,7 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
|||
keeper = toolclient.NewProcessKeeper(cfg.Ctx, proc, func(ctx context.Context) (*toolclient.Process, error) {
|
||||
return toolclient.Spawn(ctx, cfg.CWD, dialOpts...)
|
||||
})
|
||||
newToolServer = func() *toolsrv.Conn {
|
||||
newToolServer = func() *toolsrvclient.Conn {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
|
|
@ -173,7 +173,7 @@ func BuildPromptLayers(cfg *agent.AgentConfig, cwd, sessID, uname, platform stri
|
|||
// 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
|
||||
// ollie-remote can attach its tool registry before processing tool_load RPCs.
|
||||
func LoadAutoLoadTools(cfg *agent.AgentConfig, conn *toolsrv.Conn, sessID, uname string, logError func(string, ...any)) {
|
||||
func LoadAutoLoadTools(cfg *agent.AgentConfig, conn *toolsrvclient.Conn, sessID, uname string, logError func(string, ...any)) {
|
||||
if cfg == nil || conn == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// spawn.go - Process spawning and lifecycle management for toolsrv.
|
||||
// spawn.go - Process spawning and lifecycle management for toolsrvclient.
|
||||
package toolclient
|
||||
|
||||
import (
|
||||
|
|
@ -18,7 +18,7 @@ import (
|
|||
"time"
|
||||
|
||||
"ollie/paths"
|
||||
toolsrvclient "ollie/toolsrv"
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
)
|
||||
|
||||
// Process represents a running toolsrv process.
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ import (
|
|||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"9fans.net/go/plan9/client"
|
||||
"ollie/cmd/olliesrv/internal/agent"
|
||||
"ollie/env"
|
||||
"ollie/cmd/olliesrv/internal/fs"
|
||||
"ollie/env"
|
||||
olog "ollie/log"
|
||||
"ollie/paths"
|
||||
|
||||
"9fans.net/go/plan9/client"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import (
|
|||
"time"
|
||||
|
||||
"ollie/cmd/olliesrv/internal/fs"
|
||||
olog "ollie/log"
|
||||
"ollie/cmd/olliesrv/internal/session"
|
||||
olog "ollie/log"
|
||||
|
||||
"9fans.net/go/plan9"
|
||||
)
|
||||
|
|
@ -113,18 +113,36 @@ func New(cfg Config) *Server {
|
|||
}
|
||||
return &plan9.Fcall{Type: plan9.Rversion, Tag: fc.Tag, Msize: msize, Version: "9P2000"}
|
||||
},
|
||||
plan9.Tauth: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return errFcall(fc, "no auth required") },
|
||||
plan9.Tauth: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return errFcall(fc, "no auth required")
|
||||
},
|
||||
plan9.Tflush: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return flush(cs, fc) },
|
||||
plan9.Tattach: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return attach(s.groups, cs, fc) },
|
||||
plan9.Twalk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return walk(cfg.RootTree, cs, fc) },
|
||||
plan9.Topen: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return open(cfg.RootTree, s.groups, cs, fc) },
|
||||
plan9.Tcreate: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return create(cfg.RootTree, cs, fc) },
|
||||
plan9.Tread: func(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall { return read(cfg.RootTree, cs, fc, ctx) },
|
||||
plan9.Twalk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return walk(cfg.RootTree, cs, fc)
|
||||
},
|
||||
plan9.Topen: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return open(cfg.RootTree, s.groups, cs, fc)
|
||||
},
|
||||
plan9.Tcreate: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return create(cfg.RootTree, cs, fc)
|
||||
},
|
||||
plan9.Tread: func(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall {
|
||||
return read(cfg.RootTree, cs, fc, ctx)
|
||||
},
|
||||
plan9.Twrite: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return write(cs, fc) },
|
||||
plan9.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return stat(cfg.RootTree, cs, fc) },
|
||||
plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return wstat(cfg.RootTree, cs, fc) },
|
||||
plan9.Tclunk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return clunk(cfg.RootTree, cs, fc) },
|
||||
plan9.Tremove: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return remove(cfg.RootTree, cs, fc) },
|
||||
plan9.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return stat(cfg.RootTree, cs, fc)
|
||||
},
|
||||
plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return wstat(cfg.RootTree, cs, fc)
|
||||
},
|
||||
plan9.Tclunk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return clunk(cfg.RootTree, cs, fc)
|
||||
},
|
||||
plan9.Tremove: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall {
|
||||
return remove(cfg.RootTree, cs, fc)
|
||||
},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"ollie/toolsrv"
|
||||
toolsrvclient "ollie/toolsrv/client"
|
||||
"ollie/toolsrv/metadata"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
var testToolsrvBinary string
|
||||
|
|
@ -85,7 +87,7 @@ network:
|
|||
}
|
||||
|
||||
// Symlink the real tools directory so tests can find them
|
||||
realToolsPath := toolsrv.ToolsPath()
|
||||
realToolsPath := metadata.ToolsPath()
|
||||
if _, err := os.Stat(realToolsPath); err == nil {
|
||||
if err := os.Symlink(realToolsPath, filepath.Join(cfgDir, "tools")); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
|
|
@ -93,7 +95,7 @@ network:
|
|||
}
|
||||
}
|
||||
|
||||
socketPath = filepath.Join(tmpDir, "toolsrv.sock")
|
||||
socketPath = filepath.Join(tmpDir, "toolsrvclient.sock")
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
|
@ -140,7 +142,7 @@ func TestIntegration_FirstConnectionSetsSecret(t *testing.T) {
|
|||
|
||||
// First connection with a secret should succeed
|
||||
secret := "test-secret-first-connection"
|
||||
conn, err := toolsrv.Dial(socketPath, secret)
|
||||
conn, err := toolsrvclient.Dial(socketPath, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("first Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -164,7 +166,7 @@ func TestIntegration_SameSecretReconnects(t *testing.T) {
|
|||
secret := "test-secret-reconnect"
|
||||
|
||||
// First connection establishes the secret
|
||||
conn1, err := toolsrv.Dial(socketPath, secret)
|
||||
conn1, err := toolsrvclient.Dial(socketPath, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("first Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -172,7 +174,7 @@ func TestIntegration_SameSecretReconnects(t *testing.T) {
|
|||
conn1.Close()
|
||||
|
||||
// Second connection with same secret should succeed
|
||||
conn2, err := toolsrv.Dial(socketPath, secret)
|
||||
conn2, err := toolsrvclient.Dial(socketPath, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("second Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -189,14 +191,14 @@ func TestIntegration_WrongSecretFails(t *testing.T) {
|
|||
defer cleanup()
|
||||
|
||||
// First connection establishes the secret
|
||||
conn1, err := toolsrv.Dial(socketPath, "correct-secret")
|
||||
conn1, err := toolsrvclient.Dial(socketPath, "correct-secret")
|
||||
if err != nil {
|
||||
t.Fatalf("first Dial failed: %v", err)
|
||||
}
|
||||
conn1.Close()
|
||||
|
||||
// Second connection with wrong secret should fail
|
||||
conn2, err := toolsrv.Dial(socketPath, "wrong-secret")
|
||||
conn2, err := toolsrvclient.Dial(socketPath, "wrong-secret")
|
||||
if err == nil {
|
||||
conn2.Close()
|
||||
t.Fatal("Dial with wrong secret should fail")
|
||||
|
|
@ -208,7 +210,7 @@ func TestIntegration_EmptySecretGeneratesOne(t *testing.T) {
|
|||
defer cleanup()
|
||||
|
||||
// Empty secret should generate a random one
|
||||
conn, err := toolsrv.Dial(socketPath, "")
|
||||
conn, err := toolsrvclient.Dial(socketPath, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial with empty secret failed: %v", err)
|
||||
}
|
||||
|
|
@ -229,7 +231,7 @@ func TestIntegration_BasicOperations(t *testing.T) {
|
|||
socketPath, cleanup := startTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
conn, err := toolsrv.Dial(socketPath, "test-secret-ops")
|
||||
conn, err := toolsrvclient.Dial(socketPath, "test-secret-ops")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -260,14 +262,14 @@ func TestIntegration_BasicOperations(t *testing.T) {
|
|||
|
||||
func TestIntegration_ToolExecution(t *testing.T) {
|
||||
// Skip if tools directory doesn't exist (CI environment)
|
||||
if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil {
|
||||
if _, err := os.ReadDir(metadata.ToolsPath()); err != nil {
|
||||
t.Skipf("skipping: tools directory not available: %v", err)
|
||||
}
|
||||
|
||||
socketPath, cleanup := startTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
conn, err := toolsrv.Dial(socketPath, "test-secret-exec")
|
||||
conn, err := toolsrvclient.Dial(socketPath, "test-secret-exec")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -304,14 +306,14 @@ func TestIntegration_ToolExecution(t *testing.T) {
|
|||
}
|
||||
|
||||
// Parse the result the same way agent/runtime.go does
|
||||
var toolResult toolsrv.ToolResult
|
||||
var toolResult protocol.ToolResult
|
||||
if err := json.Unmarshal(result, &toolResult); err != nil {
|
||||
t.Fatalf("failed to parse toolsrv.ToolResult: %v (raw: %s)", err, string(result))
|
||||
t.Fatalf("failed to parse protocol.ToolResult: %v (raw: %s)", err, string(result))
|
||||
}
|
||||
|
||||
// Verify structure
|
||||
if len(toolResult.Content) == 0 {
|
||||
t.Fatal("toolsrv.ToolResult.Content is empty")
|
||||
t.Fatal("protocol.ToolResult.Content is empty")
|
||||
}
|
||||
if toolResult.Content[0].Type != "text" {
|
||||
t.Errorf("expected type 'text', got %q", toolResult.Content[0].Type)
|
||||
|
|
@ -320,7 +322,7 @@ func TestIntegration_ToolExecution(t *testing.T) {
|
|||
t.Errorf("output doesn't contain expected text: %q", toolResult.Content[0].Text)
|
||||
}
|
||||
if toolResult.IsError {
|
||||
t.Error("toolsrv.ToolResult.IsError should be false")
|
||||
t.Error("protocol.ToolResult.IsError should be false")
|
||||
}
|
||||
|
||||
t.Logf("Tool result: %+v", toolResult)
|
||||
|
|
@ -328,14 +330,14 @@ func TestIntegration_ToolExecution(t *testing.T) {
|
|||
|
||||
func TestIntegration_MultilineContent(t *testing.T) {
|
||||
// Skip if tools directory doesn't exist (CI environment)
|
||||
if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil {
|
||||
if _, err := os.ReadDir(metadata.ToolsPath()); err != nil {
|
||||
t.Skipf("skipping: tools directory not available: %v", err)
|
||||
}
|
||||
|
||||
socketPath, cleanup := startTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
conn, err := toolsrv.Dial(socketPath, "test-secret-multiline")
|
||||
conn, err := toolsrvclient.Dial(socketPath, "test-secret-multiline")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -360,9 +362,9 @@ func TestIntegration_MultilineContent(t *testing.T) {
|
|||
t.Fatalf("CallTool failed: %v", err)
|
||||
}
|
||||
|
||||
var toolResult toolsrv.ToolResult
|
||||
var toolResult protocol.ToolResult
|
||||
if err := json.Unmarshal(result, &toolResult); err != nil {
|
||||
t.Fatalf("failed to parse toolsrv.ToolResult: %v (raw: %s)", err, string(result))
|
||||
t.Fatalf("failed to parse protocol.ToolResult: %v (raw: %s)", err, string(result))
|
||||
}
|
||||
|
||||
if toolResult.IsError {
|
||||
|
|
@ -382,14 +384,14 @@ func TestIntegration_MultilineContent(t *testing.T) {
|
|||
|
||||
func TestIntegration_ToolExecutionError(t *testing.T) {
|
||||
// Skip if tools directory doesn't exist (CI environment)
|
||||
if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil {
|
||||
if _, err := os.ReadDir(metadata.ToolsPath()); err != nil {
|
||||
t.Skipf("skipping: tools directory not available: %v", err)
|
||||
}
|
||||
|
||||
socketPath, cleanup := startTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
conn, err := toolsrv.Dial(socketPath, "test-secret-exec-err")
|
||||
conn, err := toolsrvclient.Dial(socketPath, "test-secret-exec-err")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -410,14 +412,14 @@ func TestIntegration_ToolExecutionError(t *testing.T) {
|
|||
}
|
||||
|
||||
// Parse the result
|
||||
var toolResult toolsrv.ToolResult
|
||||
var toolResult protocol.ToolResult
|
||||
if err := json.Unmarshal(result, &toolResult); err != nil {
|
||||
t.Fatalf("failed to parse toolsrv.ToolResult: %v (raw: %s)", err, string(result))
|
||||
t.Fatalf("failed to parse protocol.ToolResult: %v (raw: %s)", err, string(result))
|
||||
}
|
||||
|
||||
// Should be an error result with exit code info
|
||||
if len(toolResult.Content) == 0 {
|
||||
t.Fatal("toolsrv.ToolResult.Content is empty")
|
||||
t.Fatal("protocol.ToolResult.Content is empty")
|
||||
}
|
||||
if !toolResult.IsError {
|
||||
t.Error("expected IsError=true for non-zero exit")
|
||||
|
|
@ -436,7 +438,7 @@ func TestIntegration_ConcurrentConnections(t *testing.T) {
|
|||
secret := "test-secret-concurrent"
|
||||
|
||||
// Establish the secret
|
||||
conn0, err := toolsrv.Dial(socketPath, secret)
|
||||
conn0, err := toolsrvclient.Dial(socketPath, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("initial Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -445,11 +447,11 @@ func TestIntegration_ConcurrentConnections(t *testing.T) {
|
|||
|
||||
// Open multiple concurrent connections
|
||||
const numConns = 5
|
||||
conns := make([]*toolsrv.Conn, numConns)
|
||||
conns := make([]*toolsrvclient.Conn, numConns)
|
||||
errors := make([]error, numConns)
|
||||
|
||||
for i := 0; i < numConns; i++ {
|
||||
conns[i], errors[i] = toolsrv.Dial(socketPath, secret)
|
||||
conns[i], errors[i] = toolsrvclient.Dial(socketPath, secret)
|
||||
}
|
||||
|
||||
// Check all succeeded with same token
|
||||
|
|
@ -478,7 +480,7 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) {
|
|||
socketPath1, cleanup1 := startTestServer(t)
|
||||
|
||||
secret1 := "secret-for-first-server"
|
||||
conn1, err := toolsrv.Dial(socketPath1, secret1)
|
||||
conn1, err := toolsrvclient.Dial(socketPath1, secret1)
|
||||
if err != nil {
|
||||
cleanup1()
|
||||
t.Fatalf("first Dial failed: %v", err)
|
||||
|
|
@ -495,7 +497,7 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) {
|
|||
|
||||
// New server should accept a new secret (any secret, since it's fresh)
|
||||
secret2 := "secret-for-second-server"
|
||||
conn2, err := toolsrv.Dial(socketPath2, secret2)
|
||||
conn2, err := toolsrvclient.Dial(socketPath2, secret2)
|
||||
if err != nil {
|
||||
t.Fatalf("second Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -508,7 +510,7 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) {
|
|||
}
|
||||
|
||||
// Old secret should NOT work on new server
|
||||
conn3, err := toolsrv.Dial(socketPath2, secret1)
|
||||
conn3, err := toolsrvclient.Dial(socketPath2, secret1)
|
||||
if err == nil {
|
||||
conn3.Close()
|
||||
t.Error("old secret should not work on new server")
|
||||
|
|
@ -522,14 +524,14 @@ func TestIntegration_ProcessKeeperRespawn(t *testing.T) {
|
|||
// call causes the call to return quickly (not wait for the full tool execution).
|
||||
func TestIntegration_ToolCancellation(t *testing.T) {
|
||||
// Skip if tools directory doesn't exist (CI environment)
|
||||
if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil {
|
||||
if _, err := os.ReadDir(metadata.ToolsPath()); err != nil {
|
||||
t.Skipf("skipping: tools directory not available: %v", err)
|
||||
}
|
||||
|
||||
socketPath, cleanup := startTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
conn, err := toolsrv.Dial(socketPath, "test-secret-cancel")
|
||||
conn, err := toolsrvclient.Dial(socketPath, "test-secret-cancel")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial failed: %v", err)
|
||||
}
|
||||
|
|
@ -588,14 +590,14 @@ func TestIntegration_ToolCancellation(t *testing.T) {
|
|||
// take a long time to complete.
|
||||
func TestIntegration_ToolCancellation_ContextPropagation(t *testing.T) {
|
||||
// Skip if tools directory doesn't exist (CI environment)
|
||||
if _, err := os.ReadDir(toolsrv.ToolsPath()); err != nil {
|
||||
if _, err := os.ReadDir(metadata.ToolsPath()); err != nil {
|
||||
t.Skipf("skipping: tools directory not available: %v", err)
|
||||
}
|
||||
|
||||
socketPath, cleanup := startTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
conn, err := toolsrv.Dial(socketPath, "test-secret-cancel-prop")
|
||||
conn, err := toolsrvclient.Dial(socketPath, "test-secret-cancel-prop")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial failed: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Package bypass handles bypass request submission and resolution for toolsrv.
|
||||
// Package bypass handles bypass request submission and resolution for toolsrvclient.
|
||||
// Bypass requests are submitted by sandboxed execution, exposed via 9P for
|
||||
// external approval (olliesrv), and resolved when the approver responds.
|
||||
package bypass
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import (
|
|||
"ollie/cmd/toolsrv/internal/bypass"
|
||||
"ollie/cmd/toolsrv/internal/sandbox"
|
||||
"ollie/paths"
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/metadata"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// Config contains all context needed for tool execution.
|
||||
|
|
@ -46,9 +47,9 @@ func WithStreamFunc(ctx context.Context, fn func(string)) context.Context {
|
|||
}
|
||||
|
||||
// ExecuteTool runs a tool script with the given args and returns the result.
|
||||
func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessage, cfg Config) (json.RawMessage, error) {
|
||||
func ExecuteTool(ctx context.Context, info protocol.ToolInfo, args json.RawMessage, cfg Config) (json.RawMessage, error) {
|
||||
// Resolve script path
|
||||
toolPath, err := toolsrv.ResolveTool(info.Name)
|
||||
toolPath, err := metadata.ResolveTool(info.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve tool %s: %w", info.Name, err)
|
||||
}
|
||||
|
|
@ -99,13 +100,13 @@ func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessag
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
return json.Marshal(toolsrv.ToolResult{
|
||||
return json.Marshal(protocol.ToolResult{
|
||||
IsError: true,
|
||||
Content: []toolsrv.ToolResultContent{{Type: "text", Text: result + ": " + err.Error()}},
|
||||
Content: []protocol.ToolResultContent{{Type: "text", Text: result + ": " + err.Error()}},
|
||||
})
|
||||
}
|
||||
return json.Marshal(toolsrv.ToolResult{
|
||||
Content: []toolsrv.ToolResultContent{{Type: "text", Text: result}},
|
||||
return json.Marshal(protocol.ToolResult{
|
||||
Content: []protocol.ToolResultContent{{Type: "text", Text: result}},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +145,7 @@ func executeSandboxed(ctx context.Context, toolPath, stdinData, cwd string, envE
|
|||
envMap[k] = v
|
||||
}
|
||||
// Prepend tools directory to PATH so meta-only cmd fields can reference other tools
|
||||
if toolsDir := toolsrv.ToolsPath(); toolsDir != "" {
|
||||
if toolsDir := metadata.ToolsPath(); toolsDir != "" {
|
||||
if existing := envMap["PATH"]; existing != "" {
|
||||
envMap["PATH"] = toolsDir + ":" + existing
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Package registry provides the tool registry for toolsrv.
|
||||
// Package registry provides the tool registry for toolsrvclient.
|
||||
// Tools are scoped per-agent within a session (one toolsrv per session).
|
||||
package registry
|
||||
|
||||
|
|
@ -7,28 +7,29 @@ import (
|
|||
"sort"
|
||||
"sync"
|
||||
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/metadata"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// Registry tracks loaded tools per agent.
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
agents map[string]map[string]toolsrv.ToolInfo
|
||||
agents map[string]map[string]protocol.ToolInfo
|
||||
revisions map[string]uint64
|
||||
}
|
||||
|
||||
// New creates a new tool registry.
|
||||
func New() (*Registry, error) {
|
||||
return &Registry{
|
||||
agents: make(map[string]map[string]toolsrv.ToolInfo),
|
||||
agents: make(map[string]map[string]protocol.ToolInfo),
|
||||
revisions: make(map[string]uint64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// discover scans the tools directory and returns all valid tool infos.
|
||||
func (r *Registry) discover() map[string]toolsrv.ToolInfo {
|
||||
infos := toolsrv.DiscoverTools()
|
||||
tools := make(map[string]toolsrv.ToolInfo, len(infos))
|
||||
func (r *Registry) discover() map[string]protocol.ToolInfo {
|
||||
infos := metadata.DiscoverTools()
|
||||
tools := make(map[string]protocol.ToolInfo, len(infos))
|
||||
for _, info := range infos {
|
||||
tools[info.Name] = info
|
||||
}
|
||||
|
|
@ -47,7 +48,7 @@ func (r *Registry) Load(agentID, name string) error {
|
|||
defer r.mu.Unlock()
|
||||
|
||||
if r.agents[agentID] == nil {
|
||||
r.agents[agentID] = make(map[string]toolsrv.ToolInfo)
|
||||
r.agents[agentID] = make(map[string]protocol.ToolInfo)
|
||||
}
|
||||
|
||||
r.agents[agentID][name] = tool
|
||||
|
|
@ -75,7 +76,7 @@ func (r *Registry) Unload(agentID, name string) error {
|
|||
}
|
||||
|
||||
// Loaded returns all loaded tools for an agent.
|
||||
func (r *Registry) Loaded(agentID string) []toolsrv.ToolInfo {
|
||||
func (r *Registry) Loaded(agentID string) []protocol.ToolInfo {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ func (r *Registry) Loaded(agentID string) []toolsrv.ToolInfo {
|
|||
return nil
|
||||
}
|
||||
|
||||
var loaded []toolsrv.ToolInfo
|
||||
var loaded []protocol.ToolInfo
|
||||
for _, info := range agentTools {
|
||||
loaded = append(loaded, info)
|
||||
}
|
||||
|
|
@ -93,13 +94,13 @@ func (r *Registry) Loaded(agentID string) []toolsrv.ToolInfo {
|
|||
}
|
||||
|
||||
// Lookup returns a tool by name for an agent.
|
||||
func (r *Registry) Lookup(agentID, name string) (toolsrv.ToolInfo, bool) {
|
||||
func (r *Registry) Lookup(agentID, name string) (protocol.ToolInfo, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
agentTools, ok := r.agents[agentID]
|
||||
if !ok {
|
||||
return toolsrv.ToolInfo{}, false
|
||||
return protocol.ToolInfo{}, false
|
||||
}
|
||||
|
||||
tool, exists := agentTools[name]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// proc.go - Process management and tool operations for toolsrv.
|
||||
// proc.go - Process management and tool operations for toolsrvclient.
|
||||
package server
|
||||
|
||||
import (
|
||||
|
|
@ -19,10 +19,10 @@ import (
|
|||
"ollie/cmd/toolsrv/internal/exec"
|
||||
"ollie/cmd/toolsrv/internal/registry"
|
||||
"ollie/paths"
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// State implements the filesystem state for toolsrv.
|
||||
// State implements the filesystem state for toolsrvclient.
|
||||
type State struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ func (st *State) GetEnv(key string) string {
|
|||
// --- Tool Registry ---
|
||||
|
||||
// ListTools returns loaded tools for the given agent.
|
||||
func (st *State) ListTools(agentID string) []toolsrv.ToolInfo {
|
||||
func (st *State) ListTools(agentID string) []protocol.ToolInfo {
|
||||
if agentID == "" {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -217,7 +217,7 @@ func (st *State) allocID() int {
|
|||
// If background is true, returns immediately with pid.
|
||||
func (st *State) NewProc(ctx context.Context, payload string, background bool) (result string, pid int, err error) {
|
||||
// Parse payload
|
||||
args := toolsrv.ParsePayload(payload)
|
||||
args := protocol.ParsePayload(payload)
|
||||
toolName := args["tool"]
|
||||
if toolName == "" {
|
||||
return "", 0, fmt.Errorf("missing 'tool' in payload")
|
||||
|
|
@ -378,7 +378,7 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) (
|
|||
|
||||
// executeTool runs a tool and returns output + exit code.
|
||||
// timeout: 0 = no timeout, >0 = seconds.
|
||||
func (st *State) executeTool(ctx context.Context, info toolsrv.ToolInfo, args map[string]string, cwd string, envExtra map[string]string, yolo bool, timeout int, output io.Writer, started chan *os.Process) (string, int) {
|
||||
func (st *State) executeTool(ctx context.Context, info protocol.ToolInfo, args map[string]string, cwd string, envExtra map[string]string, yolo bool, timeout int, output io.Writer, started chan *os.Process) (string, int) {
|
||||
// Convert args map to JSON for the execution path
|
||||
jsonArgs := argsToJSON(args)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
func TestState_Basic(t *testing.T) {
|
||||
|
|
@ -191,10 +191,10 @@ func TestParsePayload(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := toolsrv.ParsePayload(tt.input)
|
||||
got := protocol.ParsePayload(tt.input)
|
||||
for k, v := range tt.want {
|
||||
if got[k] != v {
|
||||
t.Errorf("toolsrv.ParsePayload(%q)[%q] = %q, want %q", tt.input, k, got[k], v)
|
||||
t.Errorf("protocol.ParsePayload(%q)[%q] = %q, want %q", tt.input, k, got[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// spec.go - virtfs-based namespace specification for toolsrv.
|
||||
// spec.go - virtfs-based namespace specification for toolsrvclient.
|
||||
package server
|
||||
|
||||
import (
|
||||
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
"ollie/cmd/toolsrv/internal/bypass"
|
||||
"ollie/cmd/toolsrv/internal/registry"
|
||||
"ollie/toolsrv"
|
||||
"ollie/toolsrv/metadata"
|
||||
"ollie/virtfs"
|
||||
)
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
|
|||
virtfs.FileNode("all", 0444,
|
||||
virtfs.Doc("All available tools on disk: name<tab>description per line"),
|
||||
virtfs.Read(func() ([]byte, error) {
|
||||
tools := toolsrv.DiscoverTools()
|
||||
tools := metadata.DiscoverTools()
|
||||
var sb strings.Builder
|
||||
for _, ti := range tools {
|
||||
fmt.Fprintf(&sb, "%s\t%s\n", ti.Name, ti.Description)
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"ollie/cmd/toolsrv/internal/server"
|
||||
"ollie/cmd/toolsrv/internal/registry"
|
||||
"ollie/cmd/toolsrv/internal/server"
|
||||
"ollie/env"
|
||||
lib9p "ollie/lib9p/client"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// p9.go - 9P filesystem server for toolsrv.
|
||||
// p9.go - 9P filesystem server for toolsrvclient.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// client9p.go - 9P client for toolsrv.
|
||||
package toolsrv
|
||||
// client9p.go - 9P client for toolsrvclient.
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -12,9 +12,11 @@ import (
|
|||
|
||||
"9fans.net/go/plan9"
|
||||
p9client "9fans.net/go/plan9/client"
|
||||
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// Conn is a 9P connection to a toolsrv.
|
||||
// Conn is a 9P connection to a toolsrvclient.
|
||||
type Conn struct {
|
||||
fsys *p9client.Fsys
|
||||
conn *p9client.Conn
|
||||
|
|
@ -102,7 +104,7 @@ func (c *Conn) Token() string {
|
|||
}
|
||||
|
||||
// ListTools returns the list of loaded tools for this agent.
|
||||
func (c *Conn) ListTools() ([]ToolInfo, error) {
|
||||
func (c *Conn) ListTools() ([]protocol.ToolInfo, error) {
|
||||
fid, err := c.fsys.Open("tools", plan9.ORDWR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -129,7 +131,7 @@ func (c *Conn) ListTools() ([]ToolInfo, error) {
|
|||
}
|
||||
}
|
||||
|
||||
var tools []ToolInfo
|
||||
var tools []protocol.ToolInfo
|
||||
if err := json.Unmarshal(result, &tools); err != nil {
|
||||
return nil, fmt.Errorf("parse tools: %w", err)
|
||||
}
|
||||
|
|
@ -245,7 +247,7 @@ func (c *Conn) LoadTool(name string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// ListAllTools reads the full catalog of available tools from toolsrv.
|
||||
// ListAllTools reads the full catalog of available tools from toolsrvclient.
|
||||
func (c *Conn) ListAllTools() ([]byte, error) {
|
||||
fid, err := c.fsys.Open("all", plan9.OREAD)
|
||||
if err != nil {
|
||||
|
|
@ -475,7 +477,7 @@ func (c *Conn) GetDetachedOutput(pid int) (string, error) {
|
|||
|
||||
// --- Helper types ---
|
||||
|
||||
// HostInfo contains information about the host running toolsrv.
|
||||
// HostInfo contains information about the host running toolsrvclient.
|
||||
type HostInfo struct {
|
||||
Platform string
|
||||
IsGitRepo bool
|
||||
|
|
@ -539,18 +541,10 @@ func escapeValue(s string) string {
|
|||
return string(result)
|
||||
}
|
||||
|
||||
// BypassRequest represents a bypass request from toolsrv.
|
||||
type BypassRequest struct {
|
||||
ID string `json:"id"`
|
||||
Cmd string `json:"cmd"`
|
||||
Cwd string `json:"cwd"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
// ReadBypassPending blocks until a bypass request is available.
|
||||
// This should be called in a loop by the approval handler.
|
||||
// Returns nil, ctx.Err() if context is cancelled.
|
||||
func (c *Conn) ReadBypassPending(ctx context.Context) (*BypassRequest, error) {
|
||||
func (c *Conn) ReadBypassPending(ctx context.Context) (*protocol.BypassRequest, error) {
|
||||
fid, err := c.fsys.Open("bypass/pending", plan9.OREAD)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open bypass/pending: %w", err)
|
||||
|
|
@ -588,7 +582,7 @@ func (c *Conn) ReadBypassPending(ctx context.Context) (*BypassRequest, error) {
|
|||
}
|
||||
fid.Close()
|
||||
|
||||
var req BypassRequest
|
||||
var req protocol.BypassRequest
|
||||
if err := json.Unmarshal(result, &req); err != nil {
|
||||
return nil, fmt.Errorf("parse bypass request: %w", err)
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// registry.go - Tool discovery and shared types.
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"ollie/paths"
|
||||
"ollie/toolsrv/protocol"
|
||||
)
|
||||
|
||||
// ToolsPath returns ~/.config/ollie/tools.
|
||||
func ToolsPath() string {
|
||||
return paths.CfgDir() + "/tools"
|
||||
}
|
||||
|
||||
// DiscoverTools scans the tools directory for .meta files and returns
|
||||
// metadata for all tools.
|
||||
func DiscoverTools() []protocol.ToolInfo {
|
||||
dir := ToolsPath()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var infos []protocol.ToolInfo
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".meta") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(e.Name(), ".meta")
|
||||
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var m MetaFile
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
continue
|
||||
}
|
||||
info := ToolInfoFromMeta(name, &m)
|
||||
if info.Name == "" {
|
||||
continue // no variant matched — tool unavailable on this host
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package toolsrv
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ollie/toolsrv/protocol"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -162,10 +163,10 @@ func checkCondition(key, val string) bool {
|
|||
|
||||
// ToolInfoFromMeta builds a ToolInfo from a MetaFile.
|
||||
// Applies variant resolution first. Returns zero ToolInfo if no variant matches.
|
||||
func ToolInfoFromMeta(name string, m *MetaFile) ToolInfo {
|
||||
func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo {
|
||||
resolved := m.Resolve()
|
||||
if resolved == nil {
|
||||
return ToolInfo{}
|
||||
return protocol.ToolInfo{}
|
||||
}
|
||||
args := resolved.Args
|
||||
if args == nil {
|
||||
|
|
@ -175,7 +176,7 @@ func ToolInfoFromMeta(name string, m *MetaFile) ToolInfo {
|
|||
if tier == "" {
|
||||
tier = "hot"
|
||||
}
|
||||
return ToolInfo{
|
||||
return protocol.ToolInfo{
|
||||
Name: name,
|
||||
Description: resolved.Description,
|
||||
InputSchema: args,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// parse.go — Shared key=value payload parsing.
|
||||
package toolsrv
|
||||
package protocol
|
||||
|
||||
import "strings"
|
||||
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// types.go - Shared types for toolsrv
|
||||
package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// ToolInfo describes a tool provided by a server.
|
||||
type ToolInfo struct {
|
||||
Server string
|
||||
Name string
|
||||
Description string
|
||||
InputSchema json.RawMessage
|
||||
Prompt string
|
||||
Tier string
|
||||
Scope string
|
||||
OutputFormat string
|
||||
ResetsCounter bool
|
||||
}
|
||||
|
||||
// ToolResult is the structured result from a tool execution.
|
||||
type ToolResult struct {
|
||||
Content []ToolResultContent `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
// ToolResultContent is a single content item in a tool result.
|
||||
type ToolResultContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// BypassRequest represents a bypass request from toolsrv.
|
||||
type BypassRequest struct {
|
||||
ID string `json:"id"`
|
||||
Cmd string `json:"cmd"`
|
||||
Cwd string `json:"cwd"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
// registry.go - Tool discovery and shared types.
|
||||
package toolsrv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"ollie/paths"
|
||||
)
|
||||
|
||||
// ToolInfo describes a tool provided by a server.
|
||||
type ToolInfo struct {
|
||||
Server string
|
||||
Name string
|
||||
Description string
|
||||
InputSchema json.RawMessage
|
||||
// Prompt is the usage documentation for this tool, extracted from
|
||||
// the script's ollie:prompt block. Included in the system prompt.
|
||||
Prompt string
|
||||
// Tier is the retention tier: "hot", "warm", or "cold". Parsed from
|
||||
// the script's ollie:tier annotation. Empty defaults to "hot".
|
||||
Tier string
|
||||
// Scope classifies the tool's concurrency behavior for parallel dispatch:
|
||||
// "read" — path-scoped read; never conflicts (always parallelizable)
|
||||
// "write" — path-scoped write; conflicts only on same file path
|
||||
// "global" — full serialization barrier; runs alone
|
||||
// Empty defaults to inference: "write" if tool has a path arg, else "global".
|
||||
Scope string
|
||||
// OutputFormat is the source-fence language for tool output. Empty means plaintext.
|
||||
OutputFormat string
|
||||
// ResetsCounter is true for action-oriented tools (file_write, lsp_rename, etc.)
|
||||
// that indicate active progress rather than passive research. When true, the
|
||||
// agent loop resets its step counter, allowing continued work without hitting
|
||||
// the soft step-budget guardrail.
|
||||
ResetsCounter bool
|
||||
}
|
||||
|
||||
// ToolsPath returns ~/.config/ollie/tools.
|
||||
func ToolsPath() string {
|
||||
return paths.CfgDir() + "/tools"
|
||||
}
|
||||
|
||||
// DiscoverTools scans the tools directory for .meta files and returns
|
||||
// metadata for all tools.
|
||||
func DiscoverTools() []ToolInfo {
|
||||
dir := ToolsPath()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var infos []ToolInfo
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".meta") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(e.Name(), ".meta")
|
||||
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var m MetaFile
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
continue
|
||||
}
|
||||
info := ToolInfoFromMeta(name, &m)
|
||||
if info.Name == "" {
|
||||
continue // no variant matched — tool unavailable on this host
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// types.go - Shared types for toolsrv
|
||||
package toolsrv
|
||||
|
||||
// ToolResult is the structured result from a tool execution.
|
||||
type ToolResult struct {
|
||||
Content []ToolResultContent `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
// ToolResultContent is a single content item in a tool result.
|
||||
type ToolResultContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
Loading…
Reference in New Issue