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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (ag *Agent) Queue(prompt string) {
|
||||
ag.fifo.Push(prompt)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ type AgentConfig struct {
|
|||
Model string `json:"model,omitempty"`
|
||||
Tools *bool `json:"tools,omitempty"`
|
||||
AllowTools []string `json:"allowTools,omitempty"`
|
||||
AutoLoad []string `json:"autoLoad,omitempty"`
|
||||
DisallowTools []string `json:"disallowTools,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
MaxCompletionTokens int `json:"maxCompletionTokens,omitempty"`
|
||||
// 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 {
|
||||
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)
|
||||
|
||||
// Append named tool scripts for preamble listing only.
|
||||
allToolInfos = append(allToolInfos, toolsrv.DiscoverTools()...)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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{
|
||||
ToolServer: srv,
|
||||
ToolServer: srv,
|
||||
Tools: allTools,
|
||||
Exec: exec,
|
||||
ClassifyTool: classify,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ import (
|
|||
"sync"
|
||||
"syscall"
|
||||
|
||||
"ollie/toolsrv"
|
||||
"ollie/tools/builtin"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
//go:embed sandbox/default.yaml
|
||||
|
|
@ -98,6 +98,15 @@ func main() {
|
|||
}
|
||||
|
||||
// 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())
|
||||
defer cancel()
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
|
|
@ -194,6 +203,7 @@ type rpcResponse struct {
|
|||
ID json.RawMessage `json:"id"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *rpcError `json:"error,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
}
|
||||
|
||||
type rpcError struct {
|
||||
|
|
@ -205,9 +215,20 @@ type outputNotif struct {
|
|||
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) {
|
||||
dec := json.NewDecoder(in)
|
||||
enc := json.NewEncoder(out)
|
||||
enc := &lockedEncoder{enc: json.NewEncoder(out)}
|
||||
|
||||
for {
|
||||
var req rpcRequest
|
||||
|
|
@ -223,180 +244,203 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri
|
|||
continue
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
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,
|
||||
})
|
||||
}
|
||||
go handleRPC(ctx, srv, req, enc)
|
||||
}
|
||||
}
|
||||
|
||||
case "host_info":
|
||||
info := map[string]any{
|
||||
"platform": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"is_git_repo": isGitRepo(*cwd),
|
||||
}
|
||||
func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lockedEncoder) {
|
||||
switch req.Method {
|
||||
case "list_tools":
|
||||
tools, err := srv.ListTools()
|
||||
if err != nil {
|
||||
enc.Encode(rpcResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: req.ID,
|
||||
Result: info,
|
||||
Error: &rpcError{Code: -32000, Message: err.Error()},
|
||||
})
|
||||
|
||||
case "ping":
|
||||
} else {
|
||||
enc.Encode(rpcResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: req.ID,
|
||||
Result: "pong",
|
||||
Result: tools,
|
||||
})
|
||||
}
|
||||
|
||||
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()}})
|
||||
continue
|
||||
}
|
||||
srv.SetEnv(params.Key, params.Value)
|
||||
case "host_info":
|
||||
info := map[string]any{
|
||||
"platform": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"is_git_repo": isGitRepo(*cwd),
|
||||
}
|
||||
enc.Encode(rpcResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: req.ID,
|
||||
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})
|
||||
}
|
||||
|
||||
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()}})
|
||||
continue
|
||||
}
|
||||
srv.SetCWD(params.Dir)
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
||||
case "get_detached_output":
|
||||
var params struct {
|
||||
PID int `json:"pid"`
|
||||
}
|
||||
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
|
||||
}
|
||||
out, err := srv.GetDetachedOutput(params.PID)
|
||||
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":
|
||||
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()}})
|
||||
continue
|
||||
}
|
||||
srv.SetAllowTools(params.Names)
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
|
||||
case "dismiss_detached":
|
||||
var params struct {
|
||||
PID int `json:"pid"`
|
||||
}
|
||||
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.DismissDetached(params.PID)
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||
|
||||
case "detach":
|
||||
ok := srv.Detach()
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||
case "is_parallel_read":
|
||||
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
|
||||
}
|
||||
ok := srv.IsParallelRead(params.Name)
|
||||
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 "result_tier":
|
||||
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":
|
||||
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()}})
|
||||
continue
|
||||
}
|
||||
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})
|
||||
}
|
||||
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()}})
|
||||
return
|
||||
}
|
||||
tier := srv.ResultTierArgs(params.Name, params.Args)
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
|
||||
|
||||
case "get_detached_output":
|
||||
var params struct {
|
||||
PID int `json:"pid"`
|
||||
}
|
||||
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
|
||||
}
|
||||
out, err := srv.GetDetachedOutput(params.PID)
|
||||
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 "tool_load":
|
||||
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
|
||||
}
|
||||
if err := srv.LoadTool(params.Name); err != nil {
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
|
||||
} else {
|
||||
res, _ := json.Marshal(map[string]string{"result": "loaded: " + params.Name})
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(res)})
|
||||
}
|
||||
|
||||
case "dismiss_detached":
|
||||
var params struct {
|
||||
PID int `json:"pid"`
|
||||
}
|
||||
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
|
||||
}
|
||||
ok := srv.DismissDetached(params.PID)
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||
|
||||
case "is_parallel_read":
|
||||
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()}})
|
||||
continue
|
||||
}
|
||||
ok := srv.IsParallelRead(params.Name)
|
||||
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
|
||||
|
||||
case "result_tier":
|
||||
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()}})
|
||||
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},
|
||||
})
|
||||
default:
|
||||
// Dispatch to named tool via the tool server's CallTool.
|
||||
// Correlate streaming output with the request so multiplexed
|
||||
// clients can route it to the correct caller.
|
||||
streamCtx := toolsrv.WithOutputStream(ctx, func(data string) {
|
||||
enc.Encode(rpcResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: req.ID,
|
||||
Result: outputNotif{Data: data},
|
||||
Stream: true,
|
||||
})
|
||||
})
|
||||
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),
|
||||
})
|
||||
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()
|
||||
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
|
||||
skillsRegistry := skills.NewRegistry()
|
||||
|
||||
|
|
@ -120,7 +114,6 @@ func runServer(sockPath string) {
|
|||
|
||||
rootTree := fs.NewRoot(fs.Config{
|
||||
Ctx: daemonCtx,
|
||||
ToolRegistry: toolRegistry,
|
||||
SkillsRegistry: skillsRegistry,
|
||||
AgentsDir: agentsDirs[0],
|
||||
SessionsDir: sessionsDir,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,17 @@
|
|||
"$OLLIE_CFG_PATH/prompts/user-preferences.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"],
|
||||
"hooks": {
|
||||
"turnError": [
|
||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,38 @@
|
|||
"$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"
|
||||
],
|
||||
"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,
|
||||
"temperature": 0.5,
|
||||
"maxTokens": 16384,
|
||||
|
|
@ -16,4 +48,4 @@
|
|||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,38 @@
|
|||
"$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"
|
||||
],
|
||||
"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",
|
||||
"model": "deepseek/deepseek-v4-flash",
|
||||
"maxSteps": 50,
|
||||
|
|
@ -14,4 +46,4 @@
|
|||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,19 @@
|
|||
{
|
||||
"prompt": [
|
||||
"$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,
|
||||
"temperature": 0.3,
|
||||
|
|
@ -11,4 +23,4 @@
|
|||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
{
|
||||
"prompt": [
|
||||
"$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": [
|
||||
"file_read",
|
||||
"file_glob",
|
||||
|
|
@ -13,9 +19,12 @@
|
|||
"memory_recall",
|
||||
"memory_remember"
|
||||
],
|
||||
"maxSteps": 20,
|
||||
"temperature": 0.3,
|
||||
"maxTokens": 16384,
|
||||
"hooks": {
|
||||
"turnError": [
|
||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,20 @@
|
|||
{
|
||||
"prompt": [
|
||||
"$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,
|
||||
"maxTokens": 8192,
|
||||
|
|
@ -11,4 +24,4 @@
|
|||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,21 @@
|
|||
{
|
||||
"prompt": [
|
||||
"$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": [
|
||||
"file_read",
|
||||
"file_glob",
|
||||
|
|
@ -17,9 +27,12 @@
|
|||
"memory_recall",
|
||||
"memory_remember"
|
||||
],
|
||||
"maxSteps": 20,
|
||||
"temperature": 0.3,
|
||||
"maxTokens": 8192,
|
||||
"hooks": {
|
||||
"turnError": [
|
||||
"$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,30 @@
|
|||
{
|
||||
"prompt": [
|
||||
"$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,
|
||||
"temperature": 0.3,
|
||||
|
|
@ -11,4 +34,4 @@
|
|||
"$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
|
||||
})),
|
||||
|
||||
// tools — list all available tools
|
||||
Leaf("tools", 0666,
|
||||
Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
if ctx.ToolReg == nil {
|
||||
return []byte("registry not available\n"), nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, ti := range ctx.ToolReg.Summaries() {
|
||||
if ti.Description != "" {
|
||||
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
|
||||
} else {
|
||||
sb.WriteString(ti.Name + "\n")
|
||||
// tools — list all available tools (read) / load a tool (write)
|
||||
Leaf("tools", 0666,
|
||||
Read(func(ctx HandlerCtx) ([]byte, error) {
|
||||
// Use the session's dedicated tools management connection,
|
||||
// NOT a.ToolServer() which would deadlock if the agent
|
||||
// loop is mid-execution (it holds the Conn mutex).
|
||||
if s.toolsConn == nil {
|
||||
return []byte("(no tool server)\n"), nil
|
||||
}
|
||||
}
|
||||
return []byte(sb.String()), nil
|
||||
}),
|
||||
Write(func(ctx HandlerCtx, data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
if ctx.ToolReg == nil {
|
||||
return fmt.Errorf("registry not available")
|
||||
}
|
||||
return ctx.ToolReg.Load(s.id, input)
|
||||
}),
|
||||
),
|
||||
loaded, err := s.toolsConn.ListTools()
|
||||
if err != nil {
|
||||
return []byte(fmt.Sprintf("(error: %v)\n", err)), 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
|
||||
}),
|
||||
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
|
||||
Leaf("name", 0666,
|
||||
|
|
|
|||
|
|
@ -510,7 +510,18 @@ func CreateAgent(rs *rootState, sessName string, args []string) error {
|
|||
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
||||
|
||||
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)
|
||||
sess.toolsConn = toolSrv
|
||||
|
||||
al := NewAgentLog(remoteTarget)
|
||||
|
||||
|
|
@ -602,4 +613,4 @@ func CreateAgentFromRoot(root *Tree, sessName string, args []string) (string, er
|
|||
return "", fmt.Errorf("agent not created")
|
||||
}
|
||||
return sess.Core.Agent().Name(), nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"ollie/elevate"
|
||||
olog "ollie/log"
|
||||
"ollie/skills"
|
||||
"ollie/toolsrv"
|
||||
coresession "ollie/session"
|
||||
)
|
||||
|
||||
|
|
@ -23,7 +22,6 @@ type Config struct {
|
|||
MkdirAll func(string, os.FileMode) error
|
||||
NewCore func(sessionID, agentName, cwd string) (*coresession.Session, error)
|
||||
Yolo bool
|
||||
ToolRegistry *toolsrv.Registry
|
||||
SkillsRegistry *skills.Registry
|
||||
ModelCache *ModelCache
|
||||
ElevateBroker *elevate.Broker
|
||||
|
|
@ -49,7 +47,6 @@ func NewRoot(cfg Config) *Tree {
|
|||
ctx := HandlerCtx{
|
||||
Root: nil, // set after tree is built
|
||||
Log: cfg.Log,
|
||||
ToolReg: cfg.ToolRegistry,
|
||||
Skills: cfg.SkillsRegistry,
|
||||
Models: cfg.ModelCache,
|
||||
Elevate: cfg.ElevateBroker,
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
|||
promptEnv = []string{
|
||||
"PRIME_CWD=" + cwd,
|
||||
"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 {
|
||||
var dialOpts []toolsrv.Option
|
||||
|
|
@ -212,6 +212,16 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
|||
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
||||
|
||||
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)
|
||||
|
||||
restoredSession := agent.RestoreHistory(ps)
|
||||
|
|
@ -253,9 +263,21 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
|||
sess.uname = uname
|
||||
sess.SetAgentLog(uname, al)
|
||||
sess.proc = proc
|
||||
// Use the multiplexed connection for both agent execution and 9P management.
|
||||
sess.toolsConn = toolSrv
|
||||
|
||||
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()
|
||||
rs.mu.Lock()
|
||||
rs.sessions[name] = sess
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"ollie/agent"
|
||||
"ollie/backend"
|
||||
"ollie/paths"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
// ── Root-level handlers ──────────────────────────────────────────
|
||||
|
|
@ -51,6 +52,20 @@ func readAgents(ctx HandlerCtx) ([]byte, error) {
|
|||
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 {
|
||||
cmd := strings.TrimSpace(string(data))
|
||||
switch cmd {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ var treeSpec = Dir("/",
|
|||
Leaf("help", 0444, Read(readHelp), GID("agent")),
|
||||
Leaf("models", 0444, Read(readModels), GID("agent")),
|
||||
Leaf("agents", 0444, Read(readAgents), GID("agent")),
|
||||
Leaf("tools", 0444, Read(readTools), GID("agent")),
|
||||
Leaf("ctl", 0666, Write(writeRootCtl), GID("agent")),
|
||||
Leaf("eventwait", 0444,
|
||||
Read(readEventwait),
|
||||
|
|
@ -51,4 +52,4 @@ var treeSpec = Dir("/",
|
|||
// nopRead returns empty content.
|
||||
func nopRead(_ HandlerCtx) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
fs/types.go
69
fs/types.go
|
|
@ -2,6 +2,8 @@ package fs
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -23,6 +25,10 @@ type Session struct {
|
|||
cancel context.CancelFunc
|
||||
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.
|
||||
// Backward compat: AgentLog() returns the first agent's log (by map iteration order).
|
||||
AgentLogs map[string]*AgentLog
|
||||
|
|
@ -31,6 +37,10 @@ type Session struct {
|
|||
modelsMu sync.Mutex
|
||||
modelsCache string
|
||||
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.
|
||||
|
|
@ -130,6 +140,65 @@ func (sess *Session) InvalidateModelsCache() {
|
|||
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) Uname() string { return sess.uname }
|
||||
func (sess *Session) Name() string {
|
||||
|
|
|
|||
2
justfile
2
justfile
|
|
@ -137,6 +137,8 @@ install-data:
|
|||
# Tools (executables)
|
||||
mkdir -p {{cfg}}/tools/_lib
|
||||
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_glob {{cfg}}/tools/file_glob
|
||||
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
|
||||
|
||||
- **`tool_list`** — discover all available tools with descriptions.
|
||||
- **`tool_load`** — promote a tool to a native callable function: `{"name": "toolname"}`.
|
||||
- **`tool_active`** — list tools currently loaded in your session.
|
||||
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:
|
||||
|
||||
- **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.
|
||||
|
||||
|
|
@ -51,7 +53,7 @@ Skills are markdown modules that provide specialized domain knowledge, conventio
|
|||
**Procedure** — before reaching for `shell`, follow this sequence:
|
||||
|
||||
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`.
|
||||
|
||||
**Common mappings** (not exhaustive):
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ filesystem:
|
|||
- "/dev"
|
||||
- "{HOME}/img"
|
||||
- "{OLLIE_CFG_PATH}"
|
||||
- "{OLLIE_SKILLS_PATH}"
|
||||
rw:
|
||||
- "{OLLIE_MEMORY_PATH}"
|
||||
- "{OLLIE_PLAN_PATH}"
|
||||
|
|
@ -78,6 +79,8 @@ filesystem:
|
|||
- "/dev/fuse"
|
||||
- "{OLLIE_DATA_PATH}"
|
||||
- "{HOME}/notes"
|
||||
- "{OLLIE_AGENTS_PATH}"
|
||||
- "{OLLIE_PROMPTS_PATH}"
|
||||
rox:
|
||||
- "/usr"
|
||||
- "/lib"
|
||||
|
|
@ -101,16 +104,7 @@ filesystem:
|
|||
- "{HOME}/.cache/uv"
|
||||
- "{HOME}/bin"
|
||||
- "{OLLIE}"
|
||||
- "{OLLIE_CFG_PATH}"
|
||||
- "{OLLIE_DATA_PATH}"
|
||||
- "{OLLIE_MEMORY_PATH}"
|
||||
- "{OLLIE_PLAN_PATH}"
|
||||
- "{OLLIE_SKILLS_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_CONFIG_HOME}/ollie"
|
||||
- "{XDG_DATA_HOME}/ollie"
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ package builtin
|
|||
import "ollie/toolsrv"
|
||||
|
||||
// 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 {
|
||||
return map[string]toolsrv.Handler{
|
||||
"shell": Shell,
|
||||
"reasoning_think": Think,
|
||||
"tool_list": ToolList,
|
||||
"tool_load": ToolLoad,
|
||||
"tool_active": ToolActive,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -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
|
||||
// return the same *Conn — the only difference is the underlying transport.
|
||||
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
|
||||
dec *json.Decoder
|
||||
nextID atomic.Int64
|
||||
|
|
@ -32,20 +38,28 @@ type Conn struct {
|
|||
// NewConn wraps an io.ReadWriteCloser as a JSON-RPC connection.
|
||||
// The cleanup function is called when Close() is invoked.
|
||||
func NewConn(rwc io.ReadWriteCloser, cleanup func()) *Conn {
|
||||
return &Conn{
|
||||
c := &Conn{
|
||||
enc: json.NewEncoder(rwc),
|
||||
dec: json.NewDecoder(bufio.NewReader(rwc)),
|
||||
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).
|
||||
func NewConnSplit(r io.Reader, w io.WriteCloser, cleanup func()) *Conn {
|
||||
return &Conn{
|
||||
c := &Conn{
|
||||
enc: json.NewEncoder(w),
|
||||
dec: json.NewDecoder(bufio.NewReader(r)),
|
||||
cleanup: cleanup,
|
||||
pending: make(map[int64]*pendingCall),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
go c.readLoop()
|
||||
return c
|
||||
}
|
||||
|
||||
// --- Runner interface ---
|
||||
|
|
@ -84,9 +98,9 @@ func (c *Conn) SetAllowTools(names []string) {
|
|||
}
|
||||
|
||||
func (c *Conn) SetOnToolsChanged(fn func(string)) {
|
||||
c.mu.Lock()
|
||||
c.pendingMu.Lock()
|
||||
c.onToolsChanged = fn
|
||||
c.mu.Unlock()
|
||||
c.pendingMu.Unlock()
|
||||
}
|
||||
|
||||
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.
|
||||
func (c *Conn) Close() {
|
||||
c.fail(fmt.Errorf("connection closed"))
|
||||
if c.cleanup != nil {
|
||||
c.cleanup()
|
||||
}
|
||||
|
|
@ -199,6 +214,55 @@ func (c *Conn) FetchHostInfo() (HostInfo, error) {
|
|||
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 ---
|
||||
|
||||
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) {
|
||||
id := c.nextID.Add(1)
|
||||
req := rpcRequest{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Method: method,
|
||||
Params: params,
|
||||
call := &pendingCall{response: make(chan rpcResponse, 1), ctx: ctx}
|
||||
c.pendingMu.Lock()
|
||||
select {
|
||||
case <-c.closed:
|
||||
c.pendingMu.Unlock()
|
||||
return nil, fmt.Errorf("rpc %s: connection closed", method)
|
||||
default:
|
||||
}
|
||||
c.pending[id] = call
|
||||
c.pendingMu.Unlock()
|
||||
|
||||
c.mu.Lock()
|
||||
if err := c.enc.Encode(req); err != nil {
|
||||
c.mu.Unlock()
|
||||
c.writeMu.Lock()
|
||||
err := c.enc.Encode(rpcRequest{JSONRPC: "2.0", ID: id, Method: method, Params: params})
|
||||
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)
|
||||
}
|
||||
|
||||
// Read responses, handling interleaved streaming notifications.
|
||||
for {
|
||||
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()
|
||||
select {
|
||||
case resp := <-call.response:
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("rpc %s: %s", method, resp.Error.Message)
|
||||
}
|
||||
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"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *rpcError `json:"error,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
}
|
||||
|
||||
type rpcError struct {
|
||||
|
|
|
|||
|
|
@ -143,80 +143,25 @@ func WithToolRegistry(r *Registry, sessionID string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// ListTools implements Server, returning shell plus any
|
||||
// tools promoted in the session's tool registry.
|
||||
func (e *Server) ListTools() ([]ToolInfo, error) {
|
||||
all := []ToolInfo{
|
||||
{
|
||||
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."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
// LoadTool loads a tool by name into this server's tool registry.
|
||||
// Returns an error if no registry is configured or the tool cannot be found.
|
||||
func (e *Server) LoadTool(name string) error {
|
||||
if e.toolRegistry == nil {
|
||||
return fmt.Errorf("no tool registry configured")
|
||||
}
|
||||
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 != "" {
|
||||
all = append(all, e.toolRegistry.Loaded(e.sessionID)...)
|
||||
|
|
|
|||
Loading…
Reference in New Issue