diff --git a/fs/handlers.go b/fs/handlers.go index cf234d8..1c60f29 100644 --- a/fs/handlers.go +++ b/fs/handlers.go @@ -18,6 +18,14 @@ import ( "ollie/toolsrv" ) +// mergeCtx returns a context that cancels when either parent or child cancels. +// parent is typically the session context; child is the per-read timeout from the server. +func mergeCtx(parent, child context.Context) (context.Context, context.CancelFunc) { + merged, cancel := context.WithCancel(parent) + stop := context.AfterFunc(child, func() { cancel() }) + return merged, func() { stop(); cancel() } +} + // wireAgentStateEvents sets up the agent to publish state change events to the event bus. func wireAgentStateEvents(sessID string, ag *agent.Agent) { ag.SetOnStateChange(func(agentID, state string) { @@ -113,8 +121,8 @@ func readEventwait(_ HandlerCtx) ([]byte, error) { return nil, fmt.Errorf("eventwait: use blocking read") } -func blockEventwait(_ HandlerCtx, cctx context.Context, base string) ([]byte, string, error) { - return session.WaitEvent(cctx, base) +func blockEventwait(_ HandlerCtx, cctx context.Context, _ string) ([]byte, string, error) { + return session.WaitEvent(cctx) } func requestGenerate(ctx HandlerCtx, data []byte) ([]byte, error) { @@ -253,6 +261,7 @@ func sessionBindings(ctx HandlerCtx) ([]Binding, error) { UID: sess.Uname(), GID: "agent", Applier: func(c HandlerCtx) HandlerCtx { + c.Context = node.Ctx() c.Session = node c.Remove = func() { session.Kill(n) @@ -485,7 +494,9 @@ func readAgentChat(ctx HandlerCtx) ([]byte, error) { } func streamAgentChat(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) { - return streamChat(ctx.AgentLog, cctx, base) + merged, cancel := mergeCtx(ctx, cctx) + defer cancel() + return streamChat(ctx.AgentLog, merged, base) } func readAgentConnection(ctx HandlerCtx) ([]byte, error) { @@ -499,13 +510,12 @@ func readAgentConnection(ctx HandlerCtx) ([]byte, error) { } func blockAgentStateWait(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) { - waitCtx, cancel := context.WithCancel(cctx) + merged, cancel := mergeCtx(ctx, cctx) defer cancel() - context.AfterFunc(ctx.Session.Ctx(), cancel) if base == "" { base = ctx.Agent.State() } - v, ok := ctx.Agent.WaitChange(waitCtx, agent.WatchState, base) + v, ok := ctx.Agent.WaitChange(merged, agent.WatchState, base) if !ok { st := ctx.Agent.State() return []byte(st + "\n"), st, nil diff --git a/session/event.go b/session/event.go index 3af9d24..31348f6 100644 --- a/session/event.go +++ b/session/event.go @@ -56,7 +56,7 @@ func SubscribeEvents(ctx context.Context, topic string) <-chan Event { } // WaitEvent blocks until an event is available. Returns the event as "topic payload\n". -func WaitEvent(ctx context.Context, _ string) ([]byte, string, error) { +func WaitEvent(ctx context.Context) ([]byte, string, error) { ch := SubscribeEvents(ctx, "*") select { case ev, ok := <-ch: diff --git a/toolsrv/accessors.go b/toolsrv/accessors.go deleted file mode 100644 index 3d00d81..0000000 --- a/toolsrv/accessors.go +++ /dev/null @@ -1,33 +0,0 @@ -package toolsrv - -import ( - "fmt" - "strings" -) - -// SetOnToolsChanged sets the callback for tool directory changes. -func (s *Server) SetOnToolsChanged(fn func(string)) { - s.OnToolsChanged = fn -} - -// ToolRegistryRevision returns the current registry revision for this server's session. -// Returns 0 if no registry is configured. -func (s *Server) ToolRegistryRevision() uint64 { - if s.toolRegistry == nil || s.sessionID == "" { - return 0 - } - return s.toolRegistry.Revision(s.sessionID) -} - -// buildToolListing formats a slice of ToolInfo into the preamble listing -// format consumed by agent.refreshToolListing: "- **name** — description\n" per entry. -// Only local tools (Server == "") with a non-empty description are included. -func buildToolListing(infos []ToolInfo) string { - var sb strings.Builder - for _, ti := range infos { - if ti.Description != "" && ti.Server == "" { - fmt.Fprintf(&sb, "- **%s** — %s\n", ti.Name, ti.Description) - } - } - return sb.String() -} diff --git a/toolsrv/conn.go b/toolsrv/conn.go index f1e0dc1..a739ee6 100644 --- a/toolsrv/conn.go +++ b/toolsrv/conn.go @@ -281,3 +281,29 @@ func (c *Conn) callStreaming(ctx context.Context, method string, params json.Raw return nil, fmt.Errorf("rpc %s: %w", method, err) } } + +// JSON-RPC 2.0 types shared between Conn (client) and transport layer. + +type rpcRequest struct { + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type rpcResponse struct { + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *rpcError `json:"error,omitempty"` + Stream bool `json:"stream,omitempty"` +} + +type rpcError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type outputNotification struct { + Data string `json:"data"` +} diff --git a/toolsrv/discover.go b/toolsrv/discover.go deleted file mode 100644 index abe23a1..0000000 --- a/toolsrv/discover.go +++ /dev/null @@ -1,46 +0,0 @@ -package toolsrv - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - - "ollie/paths" -) - -// 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 -} \ No newline at end of file diff --git a/toolsrv/registry.go b/toolsrv/registry.go index 9eb4b14..f77766c 100644 --- a/toolsrv/registry.go +++ b/toolsrv/registry.go @@ -1,10 +1,15 @@ package toolsrv import ( + "encoding/json" "fmt" "os" + "path/filepath" "sort" + "strings" "sync" + + "ollie/paths" ) type Registry struct { @@ -110,3 +115,62 @@ func (r *Registry) Revision(sessionID string) uint64 { } + +// 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 + // ReadOnly is true when the tool carries an "ollie:parallel read" annotation. + ReadOnly bool + // 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 +} diff --git a/toolsrv/rpc.go b/toolsrv/rpc.go deleted file mode 100644 index 23d04c1..0000000 --- a/toolsrv/rpc.go +++ /dev/null @@ -1,29 +0,0 @@ -package toolsrv - -import "encoding/json" - -// JSON-RPC 2.0 types shared between Conn (client) and transport layer. - -type rpcRequest struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params,omitempty"` -} - -type rpcResponse struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Result json.RawMessage `json:"result,omitempty"` - Error *rpcError `json:"error,omitempty"` - Stream bool `json:"stream,omitempty"` -} - -type rpcError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -type outputNotification struct { - Data string `json:"data"` -} diff --git a/toolsrv/server.go b/toolsrv/server.go index 25e2321..3d42066 100644 --- a/toolsrv/server.go +++ b/toolsrv/server.go @@ -392,3 +392,30 @@ func (s *Server) cleanupDetached() { p.Mu.Unlock() } } + +// SetOnToolsChanged sets the callback for tool directory changes. +func (s *Server) SetOnToolsChanged(fn func(string)) { + s.OnToolsChanged = fn +} + +// ToolRegistryRevision returns the current registry revision for this server's session. +// Returns 0 if no registry is configured. +func (s *Server) ToolRegistryRevision() uint64 { + if s.toolRegistry == nil || s.sessionID == "" { + return 0 + } + return s.toolRegistry.Revision(s.sessionID) +} + +// buildToolListing formats a slice of ToolInfo into the preamble listing +// format consumed by agent.refreshToolListing: "- **name** — description\n" per entry. +// Only local tools (Server == "") with a non-empty description are included. +func buildToolListing(infos []ToolInfo) string { + var sb strings.Builder + for _, ti := range infos { + if ti.Description != "" && ti.Server == "" { + fmt.Fprintf(&sb, "- **%s** — %s\n", ti.Name, ti.Description) + } + } + return sb.String() +} diff --git a/toolsrv/tools.go b/toolsrv/tools.go deleted file mode 100644 index d25d656..0000000 --- a/toolsrv/tools.go +++ /dev/null @@ -1,30 +0,0 @@ -// Package tools implements the tool server (sandboxed execution, tool registry, -// skill management) and supporting types. -package toolsrv - -import ( - "encoding/json" -) - -// 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 - // ReadOnly is true when the tool carries an "ollie:parallel read" annotation. - ReadOnly bool - // 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 -}