multiplex toolsrv, wip zero-tool server
This commit is contained in:
parent
7b5573ee89
commit
e25157ffcd
|
|
@ -454,6 +454,12 @@ func (ag *Agent) execServer() toolsrv.Runner {
|
||||||
return ag.runtime.ToolServer
|
return ag.runtime.ToolServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.Runner {
|
||||||
|
return ag.execServer()
|
||||||
|
}
|
||||||
|
|
||||||
// Queue pushes a prompt onto the agent's FIFO.
|
// Queue pushes a prompt onto the agent's FIFO.
|
||||||
func (ag *Agent) Queue(prompt string) {
|
func (ag *Agent) Queue(prompt string) {
|
||||||
ag.fifo.Push(prompt)
|
ag.fifo.Push(prompt)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ type AgentConfig struct {
|
||||||
Model string `json:"model,omitempty"`
|
Model string `json:"model,omitempty"`
|
||||||
Tools *bool `json:"tools,omitempty"`
|
Tools *bool `json:"tools,omitempty"`
|
||||||
AllowTools []string `json:"allowTools,omitempty"`
|
AllowTools []string `json:"allowTools,omitempty"`
|
||||||
|
AutoLoad []string `json:"autoLoad,omitempty"`
|
||||||
|
DisallowTools []string `json:"disallowTools,omitempty"`
|
||||||
MaxTokens int `json:"maxTokens,omitempty"`
|
MaxTokens int `json:"maxTokens,omitempty"`
|
||||||
MaxCompletionTokens int `json:"maxCompletionTokens,omitempty"`
|
MaxCompletionTokens int `json:"maxCompletionTokens,omitempty"`
|
||||||
// MaxSteps caps the number of tool-call rounds per turn. 0 means unlimited.
|
// MaxSteps caps the number of tool-call rounds per turn. 0 means unlimited.
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,8 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
|
||||||
if listErr != nil {
|
if listErr != nil {
|
||||||
messages = append(messages, fmt.Sprintf("list tools: %v", listErr))
|
messages = append(messages, fmt.Sprintf("list tools: %v", listErr))
|
||||||
}
|
}
|
||||||
// Only built-in executors (with InputSchema) become backend toolsrv.
|
// Only loaded tools become backend callable tools.
|
||||||
allTools = toolInfosToBackend(allToolInfos)
|
allTools = toolInfosToBackend(allToolInfos)
|
||||||
|
|
||||||
// Append named tool scripts for preamble listing only.
|
|
||||||
allToolInfos = append(allToolInfos, toolsrv.DiscoverTools()...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hooks := Hooks{}
|
hooks := Hooks{}
|
||||||
|
|
@ -139,8 +136,11 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
|
||||||
preamble += "\n# Available Tools\n\n" + toolListing.String()
|
preamble += "\n# Available Tools\n\n" + toolListing.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append 9P discovery instructions for loading additional tools.
|
||||||
|
preamble += "\n## Tool discovery\n\nUse the 9P filesystem to discover and load tools dynamically:\n- **List available tools** — `ollie-9p read session/{id}/agent/{aid}/tools`\n- **Load a tool** — `echo \"toolname\" | ollie-9p write session/{id}/agent/{aid}/tools`\n- **See loaded tools** — `ollie-9p read session/{id}/agent/{aid}/tools.loaded`\n"
|
||||||
|
|
||||||
return &Runtime{
|
return &Runtime{
|
||||||
ToolServer: srv,
|
ToolServer: srv,
|
||||||
Tools: allTools,
|
Tools: allTools,
|
||||||
Exec: exec,
|
Exec: exec,
|
||||||
ClassifyTool: classify,
|
ClassifyTool: classify,
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"ollie/toolsrv"
|
|
||||||
"ollie/tools/builtin"
|
"ollie/tools/builtin"
|
||||||
|
"ollie/toolsrv"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed sandbox/default.yaml
|
//go:embed sandbox/default.yaml
|
||||||
|
|
@ -98,6 +98,15 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal handling
|
// Signal handling
|
||||||
|
|
||||||
|
// Auto-load essential core tools into the registry so they are immediately
|
||||||
|
// available for agent execution without requiring a separate tool_load call.
|
||||||
|
for _, name := range []string{"shell", "reasoning_think"} {
|
||||||
|
if err := server.LoadTool(name); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "auto-load %s: %v\n", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
|
@ -194,6 +203,7 @@ type rpcResponse struct {
|
||||||
ID json.RawMessage `json:"id"`
|
ID json.RawMessage `json:"id"`
|
||||||
Result any `json:"result,omitempty"`
|
Result any `json:"result,omitempty"`
|
||||||
Error *rpcError `json:"error,omitempty"`
|
Error *rpcError `json:"error,omitempty"`
|
||||||
|
Stream bool `json:"stream,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type rpcError struct {
|
type rpcError struct {
|
||||||
|
|
@ -205,9 +215,20 @@ type outputNotif struct {
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type lockedEncoder struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
enc *json.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *lockedEncoder) Encode(v any) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
return e.enc.Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Writer) {
|
func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Writer) {
|
||||||
dec := json.NewDecoder(in)
|
dec := json.NewDecoder(in)
|
||||||
enc := json.NewEncoder(out)
|
enc := &lockedEncoder{enc: json.NewEncoder(out)}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var req rpcRequest
|
var req rpcRequest
|
||||||
|
|
@ -223,180 +244,203 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch req.Method {
|
go handleRPC(ctx, srv, req, enc)
|
||||||
case "list_tools":
|
}
|
||||||
tools, err := srv.ListTools()
|
}
|
||||||
if err != nil {
|
|
||||||
enc.Encode(rpcResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
ID: req.ID,
|
|
||||||
Error: &rpcError{Code: -32000, Message: err.Error()},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
enc.Encode(rpcResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
ID: req.ID,
|
|
||||||
Result: tools,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case "host_info":
|
func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lockedEncoder) {
|
||||||
info := map[string]any{
|
switch req.Method {
|
||||||
"platform": runtime.GOOS,
|
case "list_tools":
|
||||||
"arch": runtime.GOARCH,
|
tools, err := srv.ListTools()
|
||||||
"is_git_repo": isGitRepo(*cwd),
|
if err != nil {
|
||||||
}
|
|
||||||
enc.Encode(rpcResponse{
|
enc.Encode(rpcResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
ID: req.ID,
|
ID: req.ID,
|
||||||
Result: info,
|
Error: &rpcError{Code: -32000, Message: err.Error()},
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
case "ping":
|
|
||||||
enc.Encode(rpcResponse{
|
enc.Encode(rpcResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
ID: req.ID,
|
ID: req.ID,
|
||||||
Result: "pong",
|
Result: tools,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
case "set_env":
|
case "host_info":
|
||||||
var params struct {
|
info := map[string]any{
|
||||||
Key string `json:"key"`
|
"platform": runtime.GOOS,
|
||||||
Value string `json:"value"`
|
"arch": runtime.GOARCH,
|
||||||
}
|
"is_git_repo": isGitRepo(*cwd),
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
}
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
enc.Encode(rpcResponse{
|
||||||
continue
|
JSONRPC: "2.0",
|
||||||
}
|
ID: req.ID,
|
||||||
srv.SetEnv(params.Key, params.Value)
|
Result: info,
|
||||||
|
})
|
||||||
|
|
||||||
|
case "ping":
|
||||||
|
enc.Encode(rpcResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
|
ID: req.ID,
|
||||||
|
Result: "pong",
|
||||||
|
})
|
||||||
|
|
||||||
|
case "set_env":
|
||||||
|
var params struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srv.SetEnv(params.Key, params.Value)
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
||||||
|
|
||||||
|
case "set_cwd":
|
||||||
|
var params struct {
|
||||||
|
Dir string `json:"dir"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srv.SetCWD(params.Dir)
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
||||||
|
|
||||||
|
case "set_allow_tools":
|
||||||
|
var params struct {
|
||||||
|
Names []string `json:"names"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srv.SetAllowTools(params.Names)
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
||||||
|
|
||||||
|
case "detach":
|
||||||
|
ok := srv.Detach()
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||||
|
|
||||||
|
case "list_detached":
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: srv.ListDetachedRaw()})
|
||||||
|
|
||||||
|
case "signal_detached":
|
||||||
|
var params struct {
|
||||||
|
PID int `json:"pid"`
|
||||||
|
Signal int `json:"signal"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := srv.SignalDetached(params.PID, syscall.Signal(params.Signal)); err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
|
||||||
|
} else {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
||||||
|
}
|
||||||
|
|
||||||
case "set_cwd":
|
case "get_detached_output":
|
||||||
var params struct {
|
var params struct {
|
||||||
Dir string `json:"dir"`
|
PID int `json:"pid"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
srv.SetCWD(params.Dir)
|
out, err := srv.GetDetachedOutput(params.PID)
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
if err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
|
||||||
|
} else {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: out})
|
||||||
|
}
|
||||||
|
|
||||||
case "set_allow_tools":
|
case "dismiss_detached":
|
||||||
var params struct {
|
var params struct {
|
||||||
Names []string `json:"names"`
|
PID int `json:"pid"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
srv.SetAllowTools(params.Names)
|
ok := srv.DismissDetached(params.PID)
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||||
|
|
||||||
case "detach":
|
case "is_parallel_read":
|
||||||
ok := srv.Detach()
|
var params struct {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ok := srv.IsParallelRead(params.Name)
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||||
|
|
||||||
case "list_detached":
|
case "result_tier":
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: srv.ListDetachedRaw()})
|
var params struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tier := srv.ResultTier(params.Name)
|
||||||
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
|
||||||
|
|
||||||
case "signal_detached":
|
case "result_tier_args":
|
||||||
var params struct {
|
var params struct {
|
||||||
PID int `json:"pid"`
|
Name string `json:"name"`
|
||||||
Signal int `json:"signal"`
|
Args json.RawMessage `json:"args"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
if err := srv.SignalDetached(params.PID, syscall.Signal(params.Signal)); err != nil {
|
tier := srv.ResultTierArgs(params.Name, params.Args)
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
|
||||||
} else {
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
case "get_detached_output":
|
case "tool_load":
|
||||||
var params struct {
|
var params struct {
|
||||||
PID int `json:"pid"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
out, err := srv.GetDetachedOutput(params.PID)
|
if err := srv.LoadTool(params.Name); err != nil {
|
||||||
if err != nil {
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
|
} else {
|
||||||
} else {
|
res, _ := json.Marshal(map[string]string{"result": "loaded: " + params.Name})
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: out})
|
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(res)})
|
||||||
}
|
}
|
||||||
|
|
||||||
case "dismiss_detached":
|
default:
|
||||||
var params struct {
|
// Dispatch to named tool via the tool server's CallTool.
|
||||||
PID int `json:"pid"`
|
// Correlate streaming output with the request so multiplexed
|
||||||
}
|
// clients can route it to the correct caller.
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
streamCtx := toolsrv.WithOutputStream(ctx, func(data string) {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
enc.Encode(rpcResponse{
|
||||||
continue
|
JSONRPC: "2.0",
|
||||||
}
|
ID: req.ID,
|
||||||
ok := srv.DismissDetached(params.PID)
|
Result: outputNotif{Data: data},
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
Stream: true,
|
||||||
|
})
|
||||||
case "is_parallel_read":
|
})
|
||||||
var params struct {
|
result, err := srv.CallTool(streamCtx, req.Method, req.Params)
|
||||||
Name string `json:"name"`
|
if err != nil {
|
||||||
}
|
enc.Encode(rpcResponse{
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
JSONRPC: "2.0",
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
ID: req.ID,
|
||||||
continue
|
Error: &rpcError{Code: -32000, Message: err.Error()},
|
||||||
}
|
})
|
||||||
ok := srv.IsParallelRead(params.Name)
|
} else {
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
enc.Encode(rpcResponse{
|
||||||
|
JSONRPC: "2.0",
|
||||||
case "result_tier":
|
ID: req.ID,
|
||||||
var params struct {
|
Result: json.RawMessage(result),
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tier := srv.ResultTier(params.Name)
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
|
|
||||||
|
|
||||||
case "result_tier_args":
|
|
||||||
var params struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Args json.RawMessage `json:"args"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tier := srv.ResultTierArgs(params.Name, params.Args)
|
|
||||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Dispatch to named tool via the tool server's CallTool.
|
|
||||||
streamCtx := toolsrv.WithOutputStream(ctx, func(data string) {
|
|
||||||
enc.Encode(rpcResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Result: outputNotif{Data: data},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
result, err := srv.CallTool(streamCtx, req.Method, req.Params)
|
|
||||||
if err != nil {
|
|
||||||
enc.Encode(rpcResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
ID: req.ID,
|
|
||||||
Error: &rpcError{Code: -32000, Message: err.Error()},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
enc.Encode(rpcResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
ID: req.ID,
|
|
||||||
Result: json.RawMessage(result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,12 +101,6 @@ func runServer(sockPath string) {
|
||||||
agentsDirs := agent.AgentsDirs()
|
agentsDirs := agent.AgentsDirs()
|
||||||
sessionsDir := paths.DataDir() + "/sessions"
|
sessionsDir := paths.DataDir() + "/sessions"
|
||||||
|
|
||||||
// Create the tool registry
|
|
||||||
toolRegistry, regErr := toolsrv.NewRegistry()
|
|
||||||
if regErr != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "warning: tool registry: %v\n", regErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the skills registry
|
// Create the skills registry
|
||||||
skillsRegistry := skills.NewRegistry()
|
skillsRegistry := skills.NewRegistry()
|
||||||
|
|
||||||
|
|
@ -120,7 +114,6 @@ func runServer(sockPath string) {
|
||||||
|
|
||||||
rootTree := fs.NewRoot(fs.Config{
|
rootTree := fs.NewRoot(fs.Config{
|
||||||
Ctx: daemonCtx,
|
Ctx: daemonCtx,
|
||||||
ToolRegistry: toolRegistry,
|
|
||||||
SkillsRegistry: skillsRegistry,
|
SkillsRegistry: skillsRegistry,
|
||||||
AgentsDir: agentsDirs[0],
|
AgentsDir: agentsDirs[0],
|
||||||
SessionsDir: sessionsDir,
|
SessionsDir: sessionsDir,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,17 @@
|
||||||
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-copilot.md"
|
"$OLLIE_CFG_PATH/prompts/agent-copilot.md"
|
||||||
],
|
],
|
||||||
"tools": true,
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep"
|
||||||
|
],
|
||||||
"allowTools": ["file_read", "file_glob", "file_grep"],
|
"allowTools": ["file_read", "file_glob", "file_grep"],
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"turnError": [
|
"turnError": [
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,38 @@
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-coding.md",
|
"$OLLIE_CFG_PATH/prompts/agent-coding.md",
|
||||||
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
|
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
|
||||||
],
|
],
|
||||||
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_write",
|
||||||
|
"file_edit",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep",
|
||||||
|
"memory_recall",
|
||||||
|
"memory_remember",
|
||||||
|
"lsp_definition",
|
||||||
|
"lsp_diagnostics",
|
||||||
|
"lsp_hover",
|
||||||
|
"lsp_references",
|
||||||
|
"lsp_symbols",
|
||||||
|
"gui_screenshot",
|
||||||
|
"gui_windows",
|
||||||
|
"gui_clipboard",
|
||||||
|
"gui_notify",
|
||||||
|
"image_read",
|
||||||
|
"skill_list",
|
||||||
|
"skill_load",
|
||||||
|
"subagent_generate",
|
||||||
|
"process_list",
|
||||||
|
"process_output",
|
||||||
|
"process_signal",
|
||||||
|
"process_dismiss",
|
||||||
|
"logseq",
|
||||||
|
"system_logs",
|
||||||
|
"browser_screencap",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
"maxSteps": 50,
|
"maxSteps": 50,
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
"maxTokens": 16384,
|
"maxTokens": 16384,
|
||||||
|
|
@ -16,4 +48,4 @@
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,38 @@
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-coding.md",
|
"$OLLIE_CFG_PATH/prompts/agent-coding.md",
|
||||||
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
|
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
|
||||||
],
|
],
|
||||||
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_write",
|
||||||
|
"file_edit",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep",
|
||||||
|
"memory_recall",
|
||||||
|
"memory_remember",
|
||||||
|
"lsp_definition",
|
||||||
|
"lsp_diagnostics",
|
||||||
|
"lsp_hover",
|
||||||
|
"lsp_references",
|
||||||
|
"lsp_symbols",
|
||||||
|
"gui_screenshot",
|
||||||
|
"gui_windows",
|
||||||
|
"gui_clipboard",
|
||||||
|
"gui_notify",
|
||||||
|
"image_read",
|
||||||
|
"skill_list",
|
||||||
|
"skill_load",
|
||||||
|
"subagent_generate",
|
||||||
|
"process_list",
|
||||||
|
"process_output",
|
||||||
|
"process_signal",
|
||||||
|
"process_dismiss",
|
||||||
|
"logseq",
|
||||||
|
"system_logs",
|
||||||
|
"browser_screencap",
|
||||||
|
"route"
|
||||||
|
],
|
||||||
"backend": "openrouter",
|
"backend": "openrouter",
|
||||||
"model": "deepseek/deepseek-v4-flash",
|
"model": "deepseek/deepseek-v4-flash",
|
||||||
"maxSteps": 50,
|
"maxSteps": 50,
|
||||||
|
|
@ -14,4 +46,4 @@
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,19 @@
|
||||||
{
|
{
|
||||||
"prompt": [
|
"prompt": [
|
||||||
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-explorer.md",
|
"$OLLIE_CFG_PATH/prompts/agent-explorer.md"
|
||||||
|
],
|
||||||
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep",
|
||||||
|
"gui_screenshot",
|
||||||
|
"gui_windows",
|
||||||
|
"gui_clipboard",
|
||||||
|
"gui_notify",
|
||||||
|
"image_read"
|
||||||
],
|
],
|
||||||
"maxSteps": 40,
|
"maxSteps": 40,
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
|
|
@ -11,4 +23,4 @@
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
{
|
{
|
||||||
"prompt": [
|
"prompt": [
|
||||||
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-librarian.md",
|
"$OLLIE_CFG_PATH/prompts/agent-librarian.md"
|
||||||
|
],
|
||||||
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep",
|
||||||
|
"memory_recall",
|
||||||
|
"memory_remember"
|
||||||
],
|
],
|
||||||
"maxSteps": 20,
|
|
||||||
"temperature": 0.3,
|
|
||||||
"maxTokens": 16384,
|
|
||||||
"allowTools": [
|
"allowTools": [
|
||||||
"file_read",
|
"file_read",
|
||||||
"file_glob",
|
"file_glob",
|
||||||
|
|
@ -13,9 +19,12 @@
|
||||||
"memory_recall",
|
"memory_recall",
|
||||||
"memory_remember"
|
"memory_remember"
|
||||||
],
|
],
|
||||||
|
"maxSteps": 20,
|
||||||
|
"temperature": 0.3,
|
||||||
|
"maxTokens": 16384,
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"turnError": [
|
"turnError": [
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,20 @@
|
||||||
{
|
{
|
||||||
"prompt": [
|
"prompt": [
|
||||||
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-navigator.md",
|
"$OLLIE_CFG_PATH/prompts/agent-navigator.md"
|
||||||
|
],
|
||||||
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep",
|
||||||
|
"gui_screenshot",
|
||||||
|
"gui_windows",
|
||||||
|
"gui_clipboard",
|
||||||
|
"gui_notify",
|
||||||
|
"image_read",
|
||||||
|
"browser_screencap"
|
||||||
],
|
],
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
"maxTokens": 8192,
|
"maxTokens": 8192,
|
||||||
|
|
@ -11,4 +24,4 @@
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
{
|
{
|
||||||
"prompt": [
|
"prompt": [
|
||||||
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-taskmanager.md",
|
"$OLLIE_CFG_PATH/prompts/agent-taskmanager.md"
|
||||||
|
],
|
||||||
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep",
|
||||||
|
"file_write",
|
||||||
|
"file_edit",
|
||||||
|
"subagent_generate",
|
||||||
|
"subagent_spawn",
|
||||||
|
"memory_recall",
|
||||||
|
"memory_remember"
|
||||||
],
|
],
|
||||||
"maxSteps": 20,
|
|
||||||
"temperature": 0.3,
|
|
||||||
"maxTokens": 8192,
|
|
||||||
"allowTools": [
|
"allowTools": [
|
||||||
"file_read",
|
"file_read",
|
||||||
"file_glob",
|
"file_glob",
|
||||||
|
|
@ -17,9 +27,12 @@
|
||||||
"memory_recall",
|
"memory_recall",
|
||||||
"memory_remember"
|
"memory_remember"
|
||||||
],
|
],
|
||||||
|
"maxSteps": 20,
|
||||||
|
"temperature": 0.3,
|
||||||
|
"maxTokens": 8192,
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"turnError": [
|
"turnError": [
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,30 @@
|
||||||
{
|
{
|
||||||
"prompt": [
|
"prompt": [
|
||||||
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
"$OLLIE_CFG_PATH/prompts/user-preferences.md",
|
||||||
"$OLLIE_CFG_PATH/prompts/agent-theo.md",
|
"$OLLIE_CFG_PATH/prompts/agent-theo.md"
|
||||||
|
],
|
||||||
|
"autoLoad": [
|
||||||
|
"shell",
|
||||||
|
"reasoning_think",
|
||||||
|
"file_read",
|
||||||
|
"file_glob",
|
||||||
|
"file_grep",
|
||||||
|
"file_write",
|
||||||
|
"file_edit",
|
||||||
|
"gui_screenshot",
|
||||||
|
"gui_windows",
|
||||||
|
"gui_clipboard",
|
||||||
|
"gui_notify",
|
||||||
|
"image_read",
|
||||||
|
"memory_recall",
|
||||||
|
"memory_remember",
|
||||||
|
"lsp_definition",
|
||||||
|
"lsp_diagnostics",
|
||||||
|
"lsp_hover",
|
||||||
|
"lsp_references",
|
||||||
|
"lsp_symbols",
|
||||||
|
"skill_list",
|
||||||
|
"skill_load"
|
||||||
],
|
],
|
||||||
"maxSteps": 30,
|
"maxSteps": 30,
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
|
|
@ -11,4 +34,4 @@
|
||||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# reasoning_think — scratchpad for externalizing reasoning.
|
||||||
|
# The thought is recorded in conversation history but not shown to the user.
|
||||||
|
# Takes up practically zero context space — the value is in writing it.
|
||||||
|
# This is a no-op: the LLM handler logs the thought internally.
|
||||||
|
exit 0
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"description": "Scratchpad for externalizing reasoning. Recorded in history but not shown to the user.",
|
||||||
|
"prompt": "Scratchpad for externalizing reasoning. Recorded in conversation history but not shown to the user.",
|
||||||
|
"args": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["thought"],
|
||||||
|
"properties": {
|
||||||
|
"thought": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Your reasoning."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tier": "hot",
|
||||||
|
"readOnly": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Shell — execute a single bash command in a sandboxed environment.
|
||||||
|
# Already running inside the sandbox applied by toolsrv.
|
||||||
|
# Reads JSON args from stdin: {"cmd": "...", "timeout": 30, "elevated": false}
|
||||||
|
#
|
||||||
|
# This is the bootstrap primitive. All other tool scripts use this
|
||||||
|
# to execute commands on the system.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Parse JSON args from stdin.
|
||||||
|
args=$(cat)
|
||||||
|
|
||||||
|
# Extract fields using python (preferred) or jq (fallback).
|
||||||
|
if command -v python3 &>/dev/null; then
|
||||||
|
cmd=$(echo "$args" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('cmd',''))")
|
||||||
|
elif command -v python &>/dev/null; then
|
||||||
|
cmd=$(echo "$args" | python -c "import json,sys; d=json.load(sys.stdin); print(d.get('cmd',''))")
|
||||||
|
elif command -v jq &>/dev/null; then
|
||||||
|
cmd=$(echo "$args" | jq -r '.cmd // ""')
|
||||||
|
else
|
||||||
|
echo '{"isError":true,"content":[{"type":"text","text":"shell: need python3, python, or jq to parse args"}]}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$cmd" ]; then
|
||||||
|
echo '{"isError":true,"content":[{"type":"text","text":"shell: cmd is required"}]}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute the command. The sandbox (landlock) is already applied by toolsrv.
|
||||||
|
# We use eval to handle pipes, redirects, and compound commands.
|
||||||
|
eval "$cmd"
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"description": "Execute a single bash command in a sandboxed environment.",
|
||||||
|
"prompt": "Execute a single bash command in a sandboxed environment.\n\nUsage: {\"cmd\": \"your command here\"}\n\nSandbox prevents dangerous operations. Use for computation, builds, scripting.\ntimeout applies to each call (default: 30s). A non-zero exit is an error.",
|
||||||
|
"args": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["cmd"],
|
||||||
|
"properties": {
|
||||||
|
"cmd": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Bash command to execute."
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Timeout in seconds (default: 30). Use 0 for no timeout."
|
||||||
|
},
|
||||||
|
"sandbox": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Sandbox profile name (default: default)."
|
||||||
|
},
|
||||||
|
"elevated": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Run outside the sandbox via elevation broker."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tier": "hot",
|
||||||
|
"readOnly": false
|
||||||
|
}
|
||||||
|
|
@ -227,61 +227,37 @@ func listAgents(ctx HandlerCtx) ([]FsNodeDecl, error) {
|
||||||
return []byte("#!/bin/sh\nexec tail -f \"$(dirname \"$0\")/chat\"\n"), nil
|
return []byte("#!/bin/sh\nexec tail -f \"$(dirname \"$0\")/chat\"\n"), nil
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// tools — list all available tools
|
// tools — list all available tools (read) / load a tool (write)
|
||||||
Leaf("tools", 0666,
|
Leaf("tools", 0666,
|
||||||
Read(func(ctx HandlerCtx) ([]byte, error) {
|
Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||||
if ctx.ToolReg == nil {
|
// Use the session's dedicated tools management connection,
|
||||||
return []byte("registry not available\n"), nil
|
// NOT a.ToolServer() which would deadlock if the agent
|
||||||
}
|
// loop is mid-execution (it holds the Conn mutex).
|
||||||
var sb strings.Builder
|
if s.toolsConn == nil {
|
||||||
for _, ti := range ctx.ToolReg.Summaries() {
|
return []byte("(no tool server)\n"), nil
|
||||||
if ti.Description != "" {
|
|
||||||
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
|
|
||||||
} else {
|
|
||||||
sb.WriteString(ti.Name + "\n")
|
|
||||||
}
|
}
|
||||||
}
|
loaded, err := s.toolsConn.ListTools()
|
||||||
return []byte(sb.String()), nil
|
if err != nil {
|
||||||
}),
|
return []byte(fmt.Sprintf("(error: %v)\n", err)), nil
|
||||||
Write(func(ctx HandlerCtx, data []byte) error {
|
}
|
||||||
input := strings.TrimSpace(string(data))
|
var sb strings.Builder
|
||||||
if input == "" {
|
for _, ti := range loaded {
|
||||||
return nil
|
if ti.Description != "" {
|
||||||
}
|
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
|
||||||
if ctx.ToolReg == nil {
|
} else {
|
||||||
return fmt.Errorf("registry not available")
|
sb.WriteString(ti.Name + "\n")
|
||||||
}
|
}
|
||||||
return ctx.ToolReg.Load(s.id, input)
|
}
|
||||||
}),
|
return []byte(sb.String()), nil
|
||||||
),
|
}),
|
||||||
|
Write(func(ctx HandlerCtx, data []byte) error {
|
||||||
|
return s.loadTool(string(data), a)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// toolsrv.loaded — currently loaded tools
|
|
||||||
Leaf("toolsrv.loaded", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
|
||||||
if ctx.ToolReg == nil {
|
|
||||||
return []byte("registry not available\n"), nil
|
|
||||||
}
|
|
||||||
loaded := ctx.ToolReg.Loaded(s.id)
|
|
||||||
if len(loaded) == 0 {
|
|
||||||
return []byte("(no tools loaded)\n"), nil
|
|
||||||
}
|
|
||||||
var sb strings.Builder
|
|
||||||
for _, ti := range loaded {
|
|
||||||
if ti.Description != "" {
|
|
||||||
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
|
|
||||||
} else {
|
|
||||||
sb.WriteString(ti.Name + "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []byte(sb.String()), nil
|
|
||||||
})),
|
|
||||||
|
|
||||||
// toolsrv.rev — tool registry revision
|
|
||||||
Leaf("toolsrv.rev", 0444, Read(func(ctx HandlerCtx) ([]byte, error) {
|
|
||||||
if ctx.ToolReg == nil {
|
|
||||||
return []byte("0\n"), nil
|
|
||||||
}
|
|
||||||
return []byte(fmt.Sprintf("%d\n", ctx.ToolReg.Revision(s.id))), nil
|
|
||||||
})),
|
|
||||||
|
|
||||||
// name — agent name
|
// name — agent name
|
||||||
Leaf("name", 0666,
|
Leaf("name", 0666,
|
||||||
|
|
|
||||||
|
|
@ -510,7 +510,18 @@ func CreateAgent(rs *rootState, sessName string, args []string) error {
|
||||||
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
||||||
|
|
||||||
toolSrv := newToolServer()
|
toolSrv := newToolServer()
|
||||||
|
// Load configured tools before building the runtime. BuildRuntime snapshots
|
||||||
|
// the loaded-tool list to construct the agent's initial tool definitions.
|
||||||
|
if cfg != nil {
|
||||||
|
for _, tl := range cfg.AutoLoad {
|
||||||
|
if err := loadToolOnRunner(toolSrv, tl); err != nil {
|
||||||
|
rs.cfg.Log.Error("session %s: autoLoad tool %q: %v", sessName, tl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, sysPrompt, opModel, envBlock)
|
rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, sysPrompt, opModel, envBlock)
|
||||||
|
sess.toolsConn = toolSrv
|
||||||
|
|
||||||
al := NewAgentLog(remoteTarget)
|
al := NewAgentLog(remoteTarget)
|
||||||
|
|
||||||
|
|
@ -602,4 +613,4 @@ func CreateAgentFromRoot(root *Tree, sessName string, args []string) (string, er
|
||||||
return "", fmt.Errorf("agent not created")
|
return "", fmt.Errorf("agent not created")
|
||||||
}
|
}
|
||||||
return sess.Core.Agent().Name(), nil
|
return sess.Core.Agent().Name(), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"ollie/elevate"
|
"ollie/elevate"
|
||||||
olog "ollie/log"
|
olog "ollie/log"
|
||||||
"ollie/skills"
|
"ollie/skills"
|
||||||
"ollie/toolsrv"
|
|
||||||
coresession "ollie/session"
|
coresession "ollie/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -23,7 +22,6 @@ type Config struct {
|
||||||
MkdirAll func(string, os.FileMode) error
|
MkdirAll func(string, os.FileMode) error
|
||||||
NewCore func(sessionID, agentName, cwd string) (*coresession.Session, error)
|
NewCore func(sessionID, agentName, cwd string) (*coresession.Session, error)
|
||||||
Yolo bool
|
Yolo bool
|
||||||
ToolRegistry *toolsrv.Registry
|
|
||||||
SkillsRegistry *skills.Registry
|
SkillsRegistry *skills.Registry
|
||||||
ModelCache *ModelCache
|
ModelCache *ModelCache
|
||||||
ElevateBroker *elevate.Broker
|
ElevateBroker *elevate.Broker
|
||||||
|
|
@ -49,7 +47,6 @@ func NewRoot(cfg Config) *Tree {
|
||||||
ctx := HandlerCtx{
|
ctx := HandlerCtx{
|
||||||
Root: nil, // set after tree is built
|
Root: nil, // set after tree is built
|
||||||
Log: cfg.Log,
|
Log: cfg.Log,
|
||||||
ToolReg: cfg.ToolRegistry,
|
|
||||||
Skills: cfg.SkillsRegistry,
|
Skills: cfg.SkillsRegistry,
|
||||||
Models: cfg.ModelCache,
|
Models: cfg.ModelCache,
|
||||||
Elevate: cfg.ElevateBroker,
|
Elevate: cfg.ElevateBroker,
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
||||||
promptEnv = []string{
|
promptEnv = []string{
|
||||||
"PRIME_CWD=" + cwd,
|
"PRIME_CWD=" + cwd,
|
||||||
"PRIME_PLATFORM=" + proc.Info.Platform,
|
"PRIME_PLATFORM=" + proc.Info.Platform,
|
||||||
"PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", proc.Info.IsGitRepo),
|
"PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", proc.Info.IsGitRepo),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var dialOpts []toolsrv.Option
|
var dialOpts []toolsrv.Option
|
||||||
|
|
@ -212,6 +212,16 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
||||||
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
||||||
|
|
||||||
toolSrv := newToolServer()
|
toolSrv := newToolServer()
|
||||||
|
// Load configured tools before building the runtime. BuildRuntime snapshots
|
||||||
|
// the loaded-tool list to construct the restored agent's tool definitions.
|
||||||
|
if cfg != nil {
|
||||||
|
for _, tl := range cfg.AutoLoad {
|
||||||
|
if err := loadToolOnRunner(toolSrv, tl); err != nil {
|
||||||
|
rs.cfg.Log.Error("session %s: autoLoad tool %q: %v", sessID, tl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, sysPrompt, opModel, envBlock)
|
rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, sysPrompt, opModel, envBlock)
|
||||||
|
|
||||||
restoredSession := agent.RestoreHistory(ps)
|
restoredSession := agent.RestoreHistory(ps)
|
||||||
|
|
@ -253,9 +263,21 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
||||||
sess.uname = uname
|
sess.uname = uname
|
||||||
sess.SetAgentLog(uname, al)
|
sess.SetAgentLog(uname, al)
|
||||||
sess.proc = proc
|
sess.proc = proc
|
||||||
|
// Use the multiplexed connection for both agent execution and 9P management.
|
||||||
|
sess.toolsConn = toolSrv
|
||||||
|
|
||||||
replayMessagesToLog(sess.AgentLog(), ps.Messages)
|
replayMessagesToLog(sess.AgentLog(), ps.Messages)
|
||||||
|
|
||||||
|
// Pre-load autoLoad tools into the remote.
|
||||||
|
if cfg != nil {
|
||||||
|
ag := sess.Core.Agent()
|
||||||
|
for _, tl := range cfg.AutoLoad {
|
||||||
|
if err := sess.loadTool(tl, ag); err != nil {
|
||||||
|
rs.cfg.Log.Error("session %s: autoLoad tool %q: %v", sessID, tl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
name := sess.Name()
|
name := sess.Name()
|
||||||
rs.mu.Lock()
|
rs.mu.Lock()
|
||||||
rs.sessions[name] = sess
|
rs.sessions[name] = sess
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"ollie/agent"
|
"ollie/agent"
|
||||||
"ollie/backend"
|
"ollie/backend"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
|
"ollie/toolsrv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Root-level handlers ──────────────────────────────────────────
|
// ── Root-level handlers ──────────────────────────────────────────
|
||||||
|
|
@ -51,6 +52,20 @@ func readAgents(ctx HandlerCtx) ([]byte, error) {
|
||||||
return []byte(sb.String()), nil
|
return []byte(sb.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readTools lists all discoverable tools from disk.
|
||||||
|
// This is a global catalog — the same for all sessions and agents.
|
||||||
|
func readTools(ctx HandlerCtx) ([]byte, error) {
|
||||||
|
tools := toolsrv.DiscoverTools()
|
||||||
|
if len(tools) == 0 {
|
||||||
|
return []byte("(no tools found)\n"), nil
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, ti := range tools {
|
||||||
|
fmt.Fprintf(&sb, "%-24s %s\n", ti.Name, ti.Description)
|
||||||
|
}
|
||||||
|
return []byte(sb.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
func writeRootCtl(ctx HandlerCtx, data []byte) error {
|
func writeRootCtl(ctx HandlerCtx, data []byte) error {
|
||||||
cmd := strings.TrimSpace(string(data))
|
cmd := strings.TrimSpace(string(data))
|
||||||
switch cmd {
|
switch cmd {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ var treeSpec = Dir("/",
|
||||||
Leaf("help", 0444, Read(readHelp), GID("agent")),
|
Leaf("help", 0444, Read(readHelp), GID("agent")),
|
||||||
Leaf("models", 0444, Read(readModels), GID("agent")),
|
Leaf("models", 0444, Read(readModels), GID("agent")),
|
||||||
Leaf("agents", 0444, Read(readAgents), GID("agent")),
|
Leaf("agents", 0444, Read(readAgents), GID("agent")),
|
||||||
|
Leaf("tools", 0444, Read(readTools), GID("agent")),
|
||||||
Leaf("ctl", 0666, Write(writeRootCtl), GID("agent")),
|
Leaf("ctl", 0666, Write(writeRootCtl), GID("agent")),
|
||||||
Leaf("eventwait", 0444,
|
Leaf("eventwait", 0444,
|
||||||
Read(readEventwait),
|
Read(readEventwait),
|
||||||
|
|
@ -51,4 +52,4 @@ var treeSpec = Dir("/",
|
||||||
// nopRead returns empty content.
|
// nopRead returns empty content.
|
||||||
func nopRead(_ HandlerCtx) ([]byte, error) {
|
func nopRead(_ HandlerCtx) ([]byte, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
fs/types.go
69
fs/types.go
|
|
@ -2,6 +2,8 @@ package fs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -23,6 +25,10 @@ type Session struct {
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
proc *toolsrv.Process // toolsrv subprocess (session owns lifecycle)
|
proc *toolsrv.Process // toolsrv subprocess (session owns lifecycle)
|
||||||
|
|
||||||
|
// toolsConn is the multiplexed connection to the remote tool server,
|
||||||
|
// shared by agent execution and 9P tool-management handlers.
|
||||||
|
toolsConn toolsrv.Runner
|
||||||
|
|
||||||
// AgentLogs for the session's agents, keyed by agent ID.
|
// AgentLogs for the session's agents, keyed by agent ID.
|
||||||
// Backward compat: AgentLog() returns the first agent's log (by map iteration order).
|
// Backward compat: AgentLog() returns the first agent's log (by map iteration order).
|
||||||
AgentLogs map[string]*AgentLog
|
AgentLogs map[string]*AgentLog
|
||||||
|
|
@ -31,6 +37,10 @@ type Session struct {
|
||||||
modelsMu sync.Mutex
|
modelsMu sync.Mutex
|
||||||
modelsCache string
|
modelsCache string
|
||||||
modelsCacheAt time.Time
|
modelsCacheAt time.Time
|
||||||
|
|
||||||
|
// disallowTools is the set of tool names that cannot be loaded
|
||||||
|
// for agents in this session. Set from agent config at creation.
|
||||||
|
disallowTools map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentLog holds all per-agent state exposed via 9P.
|
// AgentLog holds all per-agent state exposed via 9P.
|
||||||
|
|
@ -130,6 +140,65 @@ func (sess *Session) InvalidateModelsCache() {
|
||||||
sess.modelsMu.Unlock()
|
sess.modelsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadTool loads a tool into the remote ollie-remote process via the
|
||||||
|
// session's management toolsConn. Returns an error if the tool is in
|
||||||
|
// the session's disallow list or if the remote rejects it.
|
||||||
|
//
|
||||||
|
// This is the single convergent path for tool loading — both the 9P
|
||||||
|
// agent/{id}/tools write handler and the autoLoad initialization call this.
|
||||||
|
func (sess *Session) loadTool(name string, ag *agent.Agent) error {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check disallow list.
|
||||||
|
sess.mu.RLock()
|
||||||
|
_, blocked := sess.disallowTools[name]
|
||||||
|
sess.mu.RUnlock()
|
||||||
|
if blocked {
|
||||||
|
return fmt.Errorf("tool %q is disallowed for this session", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load on the remote ollie-remote process (for agent execution).
|
||||||
|
// Prefer the session's management toolsConn (avoids deadlocking with
|
||||||
|
// the agent loop), fall back to the agent's ToolServer.
|
||||||
|
var runner toolsrv.Runner
|
||||||
|
if sess.toolsConn != nil {
|
||||||
|
runner = sess.toolsConn
|
||||||
|
} else if ag != nil {
|
||||||
|
runner = ag.ToolServer()
|
||||||
|
}
|
||||||
|
return loadToolOnRunner(runner, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadToolOnRunner(runner toolsrv.Runner, name string) error {
|
||||||
|
if runner == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctxTO, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
args, _ := json.Marshal(map[string]string{"name": name})
|
||||||
|
if _, err := runner.CallTool(ctxTO, "tool_load", json.RawMessage(args)); err != nil {
|
||||||
|
return fmt.Errorf("remote load: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDisallowTools replaces the session's disallow tool list.
|
||||||
|
func (sess *Session) SetDisallowTools(names []string) {
|
||||||
|
sess.mu.Lock()
|
||||||
|
defer sess.mu.Unlock()
|
||||||
|
if len(names) == 0 {
|
||||||
|
sess.disallowTools = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sess.disallowTools = make(map[string]struct{}, len(names))
|
||||||
|
for _, n := range names {
|
||||||
|
sess.disallowTools[n] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (sess *Session) RunnableID() string { return sess.id }
|
func (sess *Session) RunnableID() string { return sess.id }
|
||||||
func (sess *Session) Uname() string { return sess.uname }
|
func (sess *Session) Uname() string { return sess.uname }
|
||||||
func (sess *Session) Name() string {
|
func (sess *Session) Name() string {
|
||||||
|
|
|
||||||
2
justfile
2
justfile
|
|
@ -137,6 +137,8 @@ install-data:
|
||||||
# Tools (executables)
|
# Tools (executables)
|
||||||
mkdir -p {{cfg}}/tools/_lib
|
mkdir -p {{cfg}}/tools/_lib
|
||||||
install -m755 data/tools/browser_screencap {{cfg}}/tools/browser_screencap
|
install -m755 data/tools/browser_screencap {{cfg}}/tools/browser_screencap
|
||||||
|
install -m755 data/tools/reasoning_think {{cfg}}/tools/reasoning_think
|
||||||
|
install -m755 data/tools/shell {{cfg}}/tools/shell
|
||||||
install -m755 data/tools/file_edit {{cfg}}/tools/file_edit
|
install -m755 data/tools/file_edit {{cfg}}/tools/file_edit
|
||||||
install -m755 data/tools/file_glob {{cfg}}/tools/file_glob
|
install -m755 data/tools/file_glob {{cfg}}/tools/file_glob
|
||||||
install -m755 data/tools/file_grep {{cfg}}/tools/file_grep
|
install -m755 data/tools/file_grep {{cfg}}/tools/file_grep
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,11 @@ You have autonomous access to tools and skills. Proactively load what you need
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- **`tool_list`** — discover all available tools with descriptions.
|
Tools are loaded into your session at startup (configured in the agent's `autoLoad` list). Additional tools can be loaded dynamically via the 9P filesystem:
|
||||||
- **`tool_load`** — promote a tool to a native callable function: `{"name": "toolname"}`.
|
|
||||||
- **`tool_active`** — list tools currently loaded in your session.
|
- **List available tools** — write to `session/{id}/agent/{aid}/tools` (empty string lists all)
|
||||||
|
- **Load a tool** — write the tool name to `session/{id}/agent/{aid}/tools`
|
||||||
|
- **See loaded tools** — read from `session/{id}/agent/{aid}/tools`
|
||||||
|
|
||||||
Once loaded, a tool becomes a first-class function. Call it directly by name with JSON arguments matching its schema.
|
Once loaded, a tool becomes a first-class function. Call it directly by name with JSON arguments matching its schema.
|
||||||
|
|
||||||
|
|
@ -51,7 +53,7 @@ Skills are markdown modules that provide specialized domain knowledge, conventio
|
||||||
**Procedure** — before reaching for `shell`, follow this sequence:
|
**Procedure** — before reaching for `shell`, follow this sequence:
|
||||||
|
|
||||||
1. **Is a loaded tool already fit for purpose?** If so, use it directly.
|
1. **Is a loaded tool already fit for purpose?** If so, use it directly.
|
||||||
2. **Search available tools** (`tool_list`) — is there an unloaded tool that fits? Load it (`tool_load`) and use it.
|
2. **Search available tools** — check `session/{id}/agent/{aid}/tools` (read) to see what's available. Load any missing tool by writing its name there.
|
||||||
3. **Only if no tool exists** for the operation, fall back to `shell`.
|
3. **Only if no tool exists** for the operation, fall back to `shell`.
|
||||||
|
|
||||||
**Common mappings** (not exhaustive):
|
**Common mappings** (not exhaustive):
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ filesystem:
|
||||||
- "/dev"
|
- "/dev"
|
||||||
- "{HOME}/img"
|
- "{HOME}/img"
|
||||||
- "{OLLIE_CFG_PATH}"
|
- "{OLLIE_CFG_PATH}"
|
||||||
|
- "{OLLIE_SKILLS_PATH}"
|
||||||
rw:
|
rw:
|
||||||
- "{OLLIE_MEMORY_PATH}"
|
- "{OLLIE_MEMORY_PATH}"
|
||||||
- "{OLLIE_PLAN_PATH}"
|
- "{OLLIE_PLAN_PATH}"
|
||||||
|
|
@ -78,6 +79,8 @@ filesystem:
|
||||||
- "/dev/fuse"
|
- "/dev/fuse"
|
||||||
- "{OLLIE_DATA_PATH}"
|
- "{OLLIE_DATA_PATH}"
|
||||||
- "{HOME}/notes"
|
- "{HOME}/notes"
|
||||||
|
- "{OLLIE_AGENTS_PATH}"
|
||||||
|
- "{OLLIE_PROMPTS_PATH}"
|
||||||
rox:
|
rox:
|
||||||
- "/usr"
|
- "/usr"
|
||||||
- "/lib"
|
- "/lib"
|
||||||
|
|
@ -101,16 +104,7 @@ filesystem:
|
||||||
- "{HOME}/.cache/uv"
|
- "{HOME}/.cache/uv"
|
||||||
- "{HOME}/bin"
|
- "{HOME}/bin"
|
||||||
- "{OLLIE}"
|
- "{OLLIE}"
|
||||||
- "{OLLIE_CFG_PATH}"
|
|
||||||
- "{OLLIE_DATA_PATH}"
|
|
||||||
- "{OLLIE_MEMORY_PATH}"
|
|
||||||
- "{OLLIE_PLAN_PATH}"
|
|
||||||
- "{OLLIE_SKILLS_PATH}"
|
|
||||||
- "{OLLIE_TMP_PATH}"
|
- "{OLLIE_TMP_PATH}"
|
||||||
- "{OLLIE_TOOLS_PATH}"
|
|
||||||
- "{OLLIE_TRANSCRIPT_PATH}"
|
|
||||||
- "{OLLIE_AGENTS_PATH}"
|
|
||||||
- "{OLLIE_PROMPTS_PATH}"
|
|
||||||
# XDG defaults for OLLIE_* paths (in case env vars are unset)
|
# XDG defaults for OLLIE_* paths (in case env vars are unset)
|
||||||
- "{XDG_CONFIG_HOME}/ollie"
|
- "{XDG_CONFIG_HOME}/ollie"
|
||||||
- "{XDG_DATA_HOME}/ollie"
|
- "{XDG_DATA_HOME}/ollie"
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,8 @@ package builtin
|
||||||
import "ollie/toolsrv"
|
import "ollie/toolsrv"
|
||||||
|
|
||||||
// Builtins returns the default set of built-in tool handlers.
|
// Builtins returns the default set of built-in tool handlers.
|
||||||
|
// Returns nil — there are zero built-in tools. All tools are loaded
|
||||||
|
// dynamically via the 9P agent/{id}/tools write mechanism.
|
||||||
func Builtins() map[string]toolsrv.Handler {
|
func Builtins() map[string]toolsrv.Handler {
|
||||||
return map[string]toolsrv.Handler{
|
return nil
|
||||||
"shell": Shell,
|
}
|
||||||
"reasoning_think": Think,
|
|
||||||
"tool_list": ToolList,
|
|
||||||
"tool_load": ToolLoad,
|
|
||||||
"tool_active": ToolActive,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"ollie/toolsrv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Think is a scratchpad for externalizing reasoning. The thought is recorded
|
|
||||||
// in conversation history but not shown to the user. Takes up practically
|
|
||||||
// zero context space — the value is in writing it.
|
|
||||||
func Think(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
|
|
||||||
var a struct {
|
|
||||||
Thought string `json:"thought"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(args, &a); err != nil {
|
|
||||||
return "", fmt.Errorf("reasoning_think: bad args: %w", err)
|
|
||||||
}
|
|
||||||
if a.Thought == "" {
|
|
||||||
return "", fmt.Errorf("reasoning_think: thought is required")
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"ollie/toolsrv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Shell executes a single bash command in a sandboxed environment.
|
|
||||||
func Shell(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
|
|
||||||
var a struct {
|
|
||||||
Cmd string `json:"cmd"`
|
|
||||||
Timeout int `json:"timeout"`
|
|
||||||
Sandbox string `json:"sandbox"`
|
|
||||||
Elevated bool `json:"elevated"`
|
|
||||||
Detach bool `json:"detach"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(args, &a); err != nil {
|
|
||||||
return "", fmt.Errorf("shell: bad args: %w", err)
|
|
||||||
}
|
|
||||||
if a.Cmd == "" {
|
|
||||||
return "", fmt.Errorf("shell: cmd is required")
|
|
||||||
}
|
|
||||||
timeout := a.Timeout
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = 30
|
|
||||||
}
|
|
||||||
if a.Elevated {
|
|
||||||
return srv.ExecuteElevated(ctx, a.Cmd, srv.CWD(), timeout, a.Detach)
|
|
||||||
}
|
|
||||||
sandboxName := a.Sandbox
|
|
||||||
if sandboxName == "" {
|
|
||||||
sandboxName = "default"
|
|
||||||
}
|
|
||||||
return srv.ExecuteInSandbox(ctx, a.Cmd, "bash", timeout, sandboxName, false, "", a.Detach)
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package builtin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"ollie/toolsrv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToolList lists all available tools from the global registry.
|
|
||||||
func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
|
|
||||||
reg := srv.ToolRegistry()
|
|
||||||
if reg == nil {
|
|
||||||
return "", fmt.Errorf("tool_list: no registry available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh loaded tool schemas from disk so the model sees current definitions.
|
|
||||||
sid := srv.SessionID()
|
|
||||||
if sid != "" {
|
|
||||||
reg.RefreshLoaded(sid)
|
|
||||||
}
|
|
||||||
|
|
||||||
summaries := reg.Summaries()
|
|
||||||
if len(summaries) == 0 {
|
|
||||||
return "(no tools found)", nil
|
|
||||||
}
|
|
||||||
var out strings.Builder
|
|
||||||
for _, s := range summaries {
|
|
||||||
out.WriteString(s.Name)
|
|
||||||
if s.Description != "" {
|
|
||||||
out.WriteString(" — ")
|
|
||||||
out.WriteString(s.Description)
|
|
||||||
}
|
|
||||||
out.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify the agent that the tool listing may have changed so the
|
|
||||||
// preamble stays in sync with the directory contents.
|
|
||||||
if srv.OnToolsChanged != nil {
|
|
||||||
var listing strings.Builder
|
|
||||||
for _, s := range summaries {
|
|
||||||
if s.Description != "" {
|
|
||||||
fmt.Fprintf(&listing, "- **%s** — %s\n", s.Name, s.Description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
srv.OnToolsChanged(listing.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimRight(out.String(), "\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolLoad loads a tool into the current session.
|
|
||||||
func ToolLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
|
|
||||||
var a struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(args, &a); err != nil {
|
|
||||||
return "", fmt.Errorf("tool_load: bad args: %w", err)
|
|
||||||
}
|
|
||||||
if a.Name == "" {
|
|
||||||
return "", fmt.Errorf("tool_load: name is required")
|
|
||||||
}
|
|
||||||
reg := srv.ToolRegistry()
|
|
||||||
sid := srv.SessionID()
|
|
||||||
if reg == nil || sid == "" {
|
|
||||||
return "", fmt.Errorf("tool_load: no session registry")
|
|
||||||
}
|
|
||||||
if err := reg.Load(sid, a.Name); err != nil {
|
|
||||||
return "", fmt.Errorf("tool_load: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("loaded: %s", a.Name), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolActive lists tools currently loaded (promoted) in this session.
|
|
||||||
func ToolActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) {
|
|
||||||
reg := srv.ToolRegistry()
|
|
||||||
sid := srv.SessionID()
|
|
||||||
if reg == nil || sid == "" {
|
|
||||||
return "(no tools loaded)", nil
|
|
||||||
}
|
|
||||||
loaded := reg.Loaded(sid)
|
|
||||||
if len(loaded) == 0 {
|
|
||||||
return "(no tools loaded)", nil
|
|
||||||
}
|
|
||||||
var out strings.Builder
|
|
||||||
for _, t := range loaded {
|
|
||||||
out.WriteString(t.Name)
|
|
||||||
if t.Description != "" {
|
|
||||||
out.WriteString(" — ")
|
|
||||||
out.WriteString(t.Description)
|
|
||||||
}
|
|
||||||
out.WriteString("\n")
|
|
||||||
}
|
|
||||||
return strings.TrimRight(out.String(), "\n"), nil
|
|
||||||
}
|
|
||||||
132
toolsrv/conn.go
132
toolsrv/conn.go
|
|
@ -16,7 +16,13 @@ import (
|
||||||
// agent layer uses via type assertions. Both local and remote connections
|
// agent layer uses via type assertions. Both local and remote connections
|
||||||
// return the same *Conn — the only difference is the underlying transport.
|
// return the same *Conn — the only difference is the underlying transport.
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
mu sync.Mutex
|
writeMu sync.Mutex
|
||||||
|
pendingMu sync.Mutex
|
||||||
|
pending map[int64]*pendingCall
|
||||||
|
closed chan struct{}
|
||||||
|
closeOnce sync.Once
|
||||||
|
readErr error
|
||||||
|
|
||||||
enc *json.Encoder
|
enc *json.Encoder
|
||||||
dec *json.Decoder
|
dec *json.Decoder
|
||||||
nextID atomic.Int64
|
nextID atomic.Int64
|
||||||
|
|
@ -32,20 +38,28 @@ type Conn struct {
|
||||||
// NewConn wraps an io.ReadWriteCloser as a JSON-RPC connection.
|
// NewConn wraps an io.ReadWriteCloser as a JSON-RPC connection.
|
||||||
// The cleanup function is called when Close() is invoked.
|
// The cleanup function is called when Close() is invoked.
|
||||||
func NewConn(rwc io.ReadWriteCloser, cleanup func()) *Conn {
|
func NewConn(rwc io.ReadWriteCloser, cleanup func()) *Conn {
|
||||||
return &Conn{
|
c := &Conn{
|
||||||
enc: json.NewEncoder(rwc),
|
enc: json.NewEncoder(rwc),
|
||||||
dec: json.NewDecoder(bufio.NewReader(rwc)),
|
dec: json.NewDecoder(bufio.NewReader(rwc)),
|
||||||
cleanup: cleanup,
|
cleanup: cleanup,
|
||||||
|
pending: make(map[int64]*pendingCall),
|
||||||
|
closed: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
go c.readLoop()
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConnSplit creates a Conn from separate reader/writer (e.g. stdin/stdout pipes).
|
// NewConnSplit creates a Conn from separate reader/writer (e.g. stdin/stdout pipes).
|
||||||
func NewConnSplit(r io.Reader, w io.WriteCloser, cleanup func()) *Conn {
|
func NewConnSplit(r io.Reader, w io.WriteCloser, cleanup func()) *Conn {
|
||||||
return &Conn{
|
c := &Conn{
|
||||||
enc: json.NewEncoder(w),
|
enc: json.NewEncoder(w),
|
||||||
dec: json.NewDecoder(bufio.NewReader(r)),
|
dec: json.NewDecoder(bufio.NewReader(r)),
|
||||||
cleanup: cleanup,
|
cleanup: cleanup,
|
||||||
|
pending: make(map[int64]*pendingCall),
|
||||||
|
closed: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
go c.readLoop()
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Runner interface ---
|
// --- Runner interface ---
|
||||||
|
|
@ -84,9 +98,9 @@ func (c *Conn) SetAllowTools(names []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) SetOnToolsChanged(fn func(string)) {
|
func (c *Conn) SetOnToolsChanged(fn func(string)) {
|
||||||
c.mu.Lock()
|
c.pendingMu.Lock()
|
||||||
c.onToolsChanged = fn
|
c.onToolsChanged = fn
|
||||||
c.mu.Unlock()
|
c.pendingMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Detach() bool {
|
func (c *Conn) Detach() bool {
|
||||||
|
|
@ -175,6 +189,7 @@ func (c *Conn) ResultTierArgs(name string, args json.RawMessage) string {
|
||||||
|
|
||||||
// Close shuts down the connection and subprocess.
|
// Close shuts down the connection and subprocess.
|
||||||
func (c *Conn) Close() {
|
func (c *Conn) Close() {
|
||||||
|
c.fail(fmt.Errorf("connection closed"))
|
||||||
if c.cleanup != nil {
|
if c.cleanup != nil {
|
||||||
c.cleanup()
|
c.cleanup()
|
||||||
}
|
}
|
||||||
|
|
@ -199,6 +214,55 @@ func (c *Conn) FetchHostInfo() (HostInfo, error) {
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pendingCall struct {
|
||||||
|
response chan rpcResponse
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) readLoop() {
|
||||||
|
for {
|
||||||
|
var resp rpcResponse
|
||||||
|
if err := c.dec.Decode(&resp); err != nil {
|
||||||
|
c.fail(fmt.Errorf("rpc read: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
call := c.pending[resp.ID]
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
if call == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool output is sent as an intermediate response with the request ID.
|
||||||
|
var notif outputNotification
|
||||||
|
if resp.Stream && resp.Error == nil && json.Unmarshal(resp.Result, ¬if) == nil {
|
||||||
|
if notif.Data != "" {
|
||||||
|
StreamOutput(call.ctx, notif.Data)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
delete(c.pending, resp.ID)
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
call.response <- resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) fail(err error) {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
c.readErr = err
|
||||||
|
close(c.closed)
|
||||||
|
for id, call := range c.pending {
|
||||||
|
delete(c.pending, id)
|
||||||
|
call.response <- rpcResponse{Error: &rpcError{Code: -32000, Message: err.Error()}}
|
||||||
|
}
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- JSON-RPC internals ---
|
// --- JSON-RPC internals ---
|
||||||
|
|
||||||
func (c *Conn) call(method string, params json.RawMessage) (json.RawMessage, error) {
|
func (c *Conn) call(method string, params json.RawMessage) (json.RawMessage, error) {
|
||||||
|
|
@ -207,41 +271,45 @@ func (c *Conn) call(method string, params json.RawMessage) (json.RawMessage, err
|
||||||
|
|
||||||
func (c *Conn) callStreaming(ctx context.Context, method string, params json.RawMessage) (json.RawMessage, error) {
|
func (c *Conn) callStreaming(ctx context.Context, method string, params json.RawMessage) (json.RawMessage, error) {
|
||||||
id := c.nextID.Add(1)
|
id := c.nextID.Add(1)
|
||||||
req := rpcRequest{
|
call := &pendingCall{response: make(chan rpcResponse, 1), ctx: ctx}
|
||||||
JSONRPC: "2.0",
|
c.pendingMu.Lock()
|
||||||
ID: id,
|
select {
|
||||||
Method: method,
|
case <-c.closed:
|
||||||
Params: params,
|
c.pendingMu.Unlock()
|
||||||
|
return nil, fmt.Errorf("rpc %s: connection closed", method)
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
c.pending[id] = call
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
|
||||||
c.mu.Lock()
|
c.writeMu.Lock()
|
||||||
if err := c.enc.Encode(req); err != nil {
|
err := c.enc.Encode(rpcRequest{JSONRPC: "2.0", ID: id, Method: method, Params: params})
|
||||||
c.mu.Unlock()
|
c.writeMu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
delete(c.pending, id)
|
||||||
|
c.pendingMu.Unlock()
|
||||||
return nil, fmt.Errorf("rpc write %s: %w", method, err)
|
return nil, fmt.Errorf("rpc write %s: %w", method, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read responses, handling interleaved streaming notifications.
|
select {
|
||||||
for {
|
case resp := <-call.response:
|
||||||
var resp rpcResponse
|
|
||||||
if err := c.dec.Decode(&resp); err != nil {
|
|
||||||
c.mu.Unlock()
|
|
||||||
return nil, fmt.Errorf("rpc read %s: %w", method, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification: id == 0 means streaming output or event.
|
|
||||||
if resp.ID == 0 && resp.Result != nil {
|
|
||||||
var notif outputNotification
|
|
||||||
if json.Unmarshal(resp.Result, ¬if) == nil && notif.Data != "" {
|
|
||||||
StreamOutput(ctx, notif.Data)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actual response for our request.
|
|
||||||
c.mu.Unlock()
|
|
||||||
if resp.Error != nil {
|
if resp.Error != nil {
|
||||||
return nil, fmt.Errorf("rpc %s: %s", method, resp.Error.Message)
|
return nil, fmt.Errorf("rpc %s: %s", method, resp.Error.Message)
|
||||||
}
|
}
|
||||||
return resp.Result, nil
|
return resp.Result, nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
delete(c.pending, id)
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-c.closed:
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
err := c.readErr
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("connection closed")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("rpc %s: %w", method, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ type rpcResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Result json.RawMessage `json:"result,omitempty"`
|
Result json.RawMessage `json:"result,omitempty"`
|
||||||
Error *rpcError `json:"error,omitempty"`
|
Error *rpcError `json:"error,omitempty"`
|
||||||
|
Stream bool `json:"stream,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type rpcError struct {
|
type rpcError struct {
|
||||||
|
|
|
||||||
|
|
@ -143,80 +143,25 @@ func WithToolRegistry(r *Registry, sessionID string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadTool loads a tool by name into this server's tool registry.
|
||||||
// ListTools implements Server, returning shell plus any
|
// Returns an error if no registry is configured or the tool cannot be found.
|
||||||
// tools promoted in the session's tool registry.
|
func (e *Server) LoadTool(name string) error {
|
||||||
func (e *Server) ListTools() ([]ToolInfo, error) {
|
if e.toolRegistry == nil {
|
||||||
all := []ToolInfo{
|
return fmt.Errorf("no tool registry configured")
|
||||||
{
|
|
||||||
Name: "shell",
|
|
||||||
Description: `Execute a single bash command in a sandboxed environment.
|
|
||||||
|
|
||||||
Usage: {"cmd": "your command here"}
|
|
||||||
|
|
||||||
Sandbox prevents dangerous operations. Use for computation, builds, scripting.
|
|
||||||
timeout applies to each call (default: 30s). A non-zero exit is an error.`,
|
|
||||||
InputSchema: json.RawMessage(`{
|
|
||||||
"type": "object",
|
|
||||||
"required": ["cmd"],
|
|
||||||
"properties": {
|
|
||||||
"cmd": {"type": "string", "description": "Bash command to execute."},
|
|
||||||
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30). Use 0 for no timeout."},
|
|
||||||
"sandbox": {"type": "string", "description": "Sandbox profile name (default: default)."},
|
|
||||||
"elevated": {"type": "boolean", "description": "Run outside the sandbox via elevation broker."}
|
|
||||||
}
|
|
||||||
}`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "tool_list",
|
|
||||||
Description: `List all available tools that can be loaded.
|
|
||||||
|
|
||||||
Usage: {"name": "toolname"} — if name is provided, loads that tool.
|
|
||||||
Otherwise lists all tools with descriptions.`,
|
|
||||||
InputSchema: json.RawMessage(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "tool_load",
|
|
||||||
Description: `Load a tool by name into the current session.
|
|
||||||
|
|
||||||
Usage: {"name": "toolname"}
|
|
||||||
|
|
||||||
After loading, the tool becomes a native callable function.`,
|
|
||||||
InputSchema: json.RawMessage(`{
|
|
||||||
"type": "object",
|
|
||||||
"required": ["name"],
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string", "description": "Tool name to load."}
|
|
||||||
}
|
|
||||||
}`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "tool_active",
|
|
||||||
Description: `List tools currently loaded (promoted) in this session.
|
|
||||||
|
|
||||||
Usage: (no arguments)
|
|
||||||
|
|
||||||
Returns tools with descriptions, one per line.`,
|
|
||||||
InputSchema: json.RawMessage(`{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "reasoning_think",
|
|
||||||
Description: `Scratchpad for externalizing reasoning. Recorded in history but not shown to the user.`,
|
|
||||||
InputSchema: json.RawMessage(`{
|
|
||||||
"type": "object",
|
|
||||||
"required": ["thought"],
|
|
||||||
"properties": {
|
|
||||||
"thought": {"type": "string", "description": "Your reasoning."}
|
|
||||||
}
|
|
||||||
}`),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
if e.sessionID == "" {
|
||||||
|
return fmt.Errorf("no session ID configured")
|
||||||
|
}
|
||||||
|
return e.toolRegistry.Load(e.sessionID, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ListTools implements Server, returning tools loaded in the session's
|
||||||
|
// tool registry. With zero built-in tools, only dynamically loaded tools
|
||||||
|
// appear here.
|
||||||
|
func (e *Server) ListTools() ([]ToolInfo, error) {
|
||||||
|
var all []ToolInfo
|
||||||
|
|
||||||
if e.toolRegistry != nil && e.sessionID != "" {
|
if e.toolRegistry != nil && e.sessionID != "" {
|
||||||
all = append(all, e.toolRegistry.Loaded(e.sessionID)...)
|
all = append(all, e.toolRegistry.Loaded(e.sessionID)...)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue