execute_tool: replace 9P mount with filesystem read; drop 9fans dep
ReadTool now reads from OLLIE_TOOLS_PATH (default ~/.local/share/ollie/tools) as a plain filesystem path, compatible with 9pfuse mounts or symlinks. ToolsPath() is exported and used to expand tool descriptions and the system prompt template at runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c7d2684cfc
commit
8e2b8083bb
68
README.md
68
README.md
|
|
@ -1,38 +1,42 @@
|
|||
# Ollie
|
||||
# ollie
|
||||
|
||||
An agentic CLI built on a sandboxed `execute_code` tool and a common LLM backend interface. Supports local (Ollama) and OpenAI-compatible backends, optional MCP server connections, and a skill system for domain-specific capabilities.
|
||||
A Go library for building agentic systems. Provides a sandboxed `execute_code` tool, a common LLM backend interface, MCP server support, and a skill system for domain-specific capabilities.
|
||||
|
||||
Intended to be used with [anvillm](https://github.com/lneely/anvillm), which provides the skill system, tool scripts, and multi-agent infrastructure that ollie builds on.
|
||||
|
||||
## Build
|
||||
The reference frontend is [ollie-tui](https://github.com/lneely/ollie-tui), a terminal UI built on top of this library.
|
||||
|
||||
```bash
|
||||
go build
|
||||
## Packages
|
||||
|
||||
```
|
||||
pkg/agent/ — Core interface, agent loop, session management
|
||||
pkg/backend/ — Backend interface + implementations (Ollama, OpenAI, Anthropic, Copilot, Kiro)
|
||||
pkg/config/ — Config struct and loader
|
||||
pkg/mcp/ — MCP client
|
||||
pkg/tools/ — Executor interface + MCPExecutor
|
||||
pkg/tools/execute/ — Built-in sandbox executor (execute_code, execute_tool, execute_pipe)
|
||||
```
|
||||
|
||||
Requires Go 1.21+.
|
||||
## Install
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
./ollie [model]
|
||||
```
|
||||
mk
|
||||
```
|
||||
|
||||
Model defaults to `qwen3:8b`. Override with `OLLIE_MODEL` or pass as the first argument:
|
||||
|
||||
```bash
|
||||
OLLIE_BACKEND=openai ./ollie qwen/qwen3-235b-a22b
|
||||
```
|
||||
Copies agent configs to `~/.config/ollie/agents/`.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment: `~/.config/ollie/env`
|
||||
|
||||
```
|
||||
OLLIE_BACKEND=openai # ollama | openai (default: ollama)
|
||||
OLLIE_BACKEND=openai # ollama | openai | openrouter | anthropic | copilot | kiro (default: ollama)
|
||||
OLLIE_OLLAMA_URL= # base URL for Ollama (default: http://localhost:11434)
|
||||
OLLIE_OPENAI_URL=https://openrouter.ai/api # any OpenAI-compatible endpoint
|
||||
OLLIE_OPENAI_URL=https://openrouter.ai/api
|
||||
OLLIE_OPENAI_KEY=sk-or-...
|
||||
OLLIE_ANTHROPIC_KEY=sk-ant-...
|
||||
OLLIE_COPILOT_TOKEN=...
|
||||
OLLIE_KIRO_TOKEN=... # bearer token or sqlite:// path (auto-detected from Kiro CLI if unset)
|
||||
OLLIE_MODEL=qwen/qwen3-235b-a22b
|
||||
```
|
||||
|
||||
|
|
@ -59,33 +63,17 @@ Shell environment variables take precedence over the env file.
|
|||
}
|
||||
```
|
||||
|
||||
MCP server `env` values support `${VAR}` expansion from the parent environment. Only whitelisted vars are passed to the subprocess.
|
||||
|
||||
An alternate config path can be provided as a second argument:
|
||||
|
||||
```bash
|
||||
./ollie qwen3:8b /path/to/config.json
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
- **Enter** — submit message
|
||||
- **Ctrl+J** — insert newline
|
||||
- **PageUp / PageDown** — scroll chat history
|
||||
- **Ctrl+U / Ctrl+D** — half-page scroll
|
||||
- **Ctrl+C / Esc** — quit
|
||||
|
||||
Token usage is shown after each response: `[↑N ↓N tokens]`.
|
||||
MCP server `env` values support `${VAR}` expansion from the parent environment.
|
||||
|
||||
## Tools
|
||||
|
||||
The agent has one built-in tool: `execute_code`. It runs shell code in a sandboxed environment and supports three invocation modes:
|
||||
Three built-in tool modes:
|
||||
|
||||
- `code` — inline bash
|
||||
- `tool` + `args` — named tool script
|
||||
- `pipe` — sequence of `{tool, args}` steps
|
||||
- `execute_code` — run inline shell code in a sandbox
|
||||
- `execute_tool` — run a named tool script from the tools directory
|
||||
- `execute_pipe` — chain steps as a pipeline
|
||||
|
||||
MCP server tools are discovered at startup and available alongside `execute_code`.
|
||||
MCP server tools are discovered at startup and available alongside the built-ins.
|
||||
|
||||
## Skills
|
||||
|
||||
|
|
@ -96,8 +84,6 @@ Skills are domain-specific tool scripts and knowledge discoverable at runtime:
|
|||
{"tool": "load_skill.sh", "args": ["skill-name"]}
|
||||
```
|
||||
|
||||
The agent discovers and loads relevant skills automatically at the start of each task.
|
||||
|
||||
## License
|
||||
|
||||
GPLv3
|
||||
|
|
|
|||
5
go.mod
5
go.mod
|
|
@ -2,7 +2,4 @@ module ollie
|
|||
|
||||
go 1.25.6
|
||||
|
||||
require (
|
||||
9fans.net/go v0.0.7
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
|
|
|
|||
34
go.sum
34
go.sum
|
|
@ -1,37 +1,3 @@
|
|||
9fans.net/go v0.0.7 h1:H5CsYJTf99C8EYAQr+uSoEJnLP/iZU8RmDuhyk30iSM=
|
||||
9fans.net/go v0.0.7/go.mod h1:Rxvbbc1e+1TyGMjAvLthGTyO97t+6JMQ6ly+Lcs9Uf0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20210405174845-4513512abef3/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"crypto/rand"
|
||||
|
|
@ -32,7 +33,7 @@ Do not attempt tasks outside your tools.
|
|||
Do not use hedging language ("it looks like", "it appears", "it seems", "likely", "probably"). If you are uncertain, use tools to find out. Give definite answers based on evidence.
|
||||
Do not re-read or re-fetch any file or resource that already has a result in the conversation history. Use the existing result.
|
||||
Use tools to gather information before asking the user for clarification. Explore files, run commands, and investigate the environment first. Only ask when you have exhausted what you can discover on your own.
|
||||
Use execute_code for all shell commands and scripts. Use execute_tool only for named scripts in ~/mnt/anvillm/tools. Use execute_pipe to chain steps: use {code: "cmd --flags"} for shell commands, {tool, args} only for named scripts in ~/mnt/anvillm/tools.
|
||||
Use execute_code for all shell commands and scripts. Use execute_tool only for named scripts in {{.ToolsPath}}. Use execute_pipe to chain steps: use {code: "cmd --flags"} for shell commands, {tool, args} only for named scripts in {{.ToolsPath}}.
|
||||
Use grep or execute_code to search and explore files. Use file_read only when you need to write — it reads the full file and is required before file_write. Never use shell commands to read or write files.
|
||||
|
||||
Tool call examples:
|
||||
|
|
@ -120,6 +121,12 @@ func buildFirstPrompt(input string) string {
|
|||
}
|
||||
|
||||
// systemPrompt builds the full system prompt for a given tool set.
|
||||
var systemPromptTmpl = template.Must(template.New("system").Parse(systemPromptBase))
|
||||
|
||||
type systemPromptData struct {
|
||||
ToolsPath string
|
||||
}
|
||||
|
||||
func systemPrompt(allTools []backend.Tool) string {
|
||||
cwd, _ := os.Getwd()
|
||||
now := time.Now().Format("2006-01-02 15:04:05 MST")
|
||||
|
|
@ -127,13 +134,20 @@ func systemPrompt(allTools []backend.Tool) string {
|
|||
for i, t := range allTools {
|
||||
names[i] = t.Name
|
||||
}
|
||||
return systemPromptBase + "\n\nWorking directory: " + cwd +
|
||||
var buf strings.Builder
|
||||
systemPromptTmpl.Execute(&buf, systemPromptData{ //nolint:errcheck
|
||||
ToolsPath: execute.ToolsPath(),
|
||||
})
|
||||
return buf.String() + "\n\nWorking directory: " + cwd +
|
||||
"\nCurrent time: " + now +
|
||||
"\nAvailable tools: " + strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// builtinTools is the set of tools provided by the agent core.
|
||||
var builtinTools = []backend.Tool{
|
||||
// builtinTools returns the set of tools provided by the agent core,
|
||||
// with path-dependent descriptions resolved at call time.
|
||||
func builtinTools() []backend.Tool {
|
||||
tp := execute.ToolsPath()
|
||||
return []backend.Tool{
|
||||
{
|
||||
Name: "execute_code",
|
||||
Description: "Run inline code in a sandboxed environment.",
|
||||
|
|
@ -150,7 +164,7 @@ var builtinTools = []backend.Tool{
|
|||
},
|
||||
{
|
||||
Name: "execute_tool",
|
||||
Description: "Run a named tool script from ~/mnt/anvillm/tools in a sandboxed environment. Use this only for named scripts, not for inline shell commands.",
|
||||
Description: "Run a named tool script from " + tp + " in a sandboxed environment. Use this only for named scripts, not for inline shell commands.",
|
||||
Parameters: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["tool"],
|
||||
|
|
@ -164,7 +178,7 @@ var builtinTools = []backend.Tool{
|
|||
},
|
||||
{
|
||||
Name: "execute_pipe",
|
||||
Description: "Run a pipeline of steps, piping stdout of each into stdin of the next. Use {code: \"cmd --flags\"} for shell commands; use {tool, args} only for named scripts in ~/mnt/anvillm/tools.",
|
||||
Description: "Run a pipeline of steps, piping stdout of each into stdin of the next. Use {code: \"cmd --flags\"} for shell commands; use {tool, args} only for named scripts in " + tp + ".",
|
||||
Parameters: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["pipe"],
|
||||
|
|
@ -210,6 +224,7 @@ var builtinTools = []backend.Tool{
|
|||
}
|
||||
}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AgentEnv holds the runtime state derived from an agent config file.
|
||||
|
|
@ -259,7 +274,7 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execute.Executor) AgentEnv {
|
|||
serverOf[t.Name] = t.Server
|
||||
}
|
||||
|
||||
allTools := append(mcpToolsToBackend(mcpTools), builtinTools...)
|
||||
allTools := append(mcpToolsToBackend(mcpTools), builtinTools()...)
|
||||
|
||||
hooks := Hooks{}
|
||||
agentPrompt := ""
|
||||
|
|
@ -291,8 +306,9 @@ func BuildAgentEnv(cfg *config.Config, builtinExec *execute.Executor) AgentEnv {
|
|||
overhead += len(t.Name) + len(t.Description) + len(t.Parameters)
|
||||
}
|
||||
|
||||
builtinNames := make(map[string]struct{}, len(builtinTools))
|
||||
for _, t := range builtinTools {
|
||||
bt := builtinTools()
|
||||
builtinNames := make(map[string]struct{}, len(bt))
|
||||
for _, t := range bt {
|
||||
builtinNames[t.Name] = struct{}{}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,39 +5,31 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"9fans.net/go/plan9/client"
|
||||
)
|
||||
|
||||
// ReadTool reads a named tool script from the 9P tools directory.
|
||||
// ToolsPath returns the directory to search for named tool scripts.
|
||||
// Resolved in order: OLLIE_TOOLS_PATH env var, then ~/.local/share/ollie/tools.
|
||||
func ToolsPath() string {
|
||||
if p := os.Getenv("OLLIE_TOOLS_PATH"); p != "" {
|
||||
return p
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".local", "share", "ollie", "tools")
|
||||
}
|
||||
|
||||
// ReadTool reads a named tool script from the tools directory.
|
||||
func ReadTool(name string) (string, error) {
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "..") {
|
||||
return "", fmt.Errorf("invalid tool name")
|
||||
}
|
||||
|
||||
ns := fmt.Sprintf("/tmp/ns.%s.:0", os.Getenv("USER"))
|
||||
fsys, err := client.Mount("unix", filepath.Join(ns, "anvillm"))
|
||||
path := filepath.Join(ToolsPath(), name)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to mount 9P: %v", err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fid, err := fsys.Open("/tools/"+name, 0)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tool not found: %s", name)
|
||||
}
|
||||
defer fid.Close()
|
||||
|
||||
var buf []byte
|
||||
tmp := make([]byte, 8192)
|
||||
for {
|
||||
n, err := fid.Read(tmp)
|
||||
if n > 0 {
|
||||
buf = append(buf, tmp[:n]...)
|
||||
}
|
||||
if err != nil || n < len(tmp) {
|
||||
break
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("tool not found: %s", name)
|
||||
}
|
||||
return "", fmt.Errorf("read tool %s: %w", name, err)
|
||||
}
|
||||
return string(buf), nil
|
||||
return string(data), nil
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue