This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-core/main.go

299 lines
7.4 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"ollie/agent"
"ollie/backend"
"ollie/config"
execpkg "ollie/exec"
)
const systemPrompt = `You are an autonomous agent. You have one tool: execute_code.
execute_code runs shell code in a sandboxed environment. Three invocation modes:
- code: inline bash (default language)
- tool + args: run a named tool script
- pipe: a sequence of {tool, args} steps run in order
The sandbox restricts certain operations by policy. A permission error does not
necessarily mean a file lacks execute permission — it may be a sandbox boundary.
If you hit one, try a different approach or tool rather than retrying the same call.
Skills (domain-specific scripts and knowledge) are discoverable and loadable
through execute_code itself: use discover_skill.sh to find them, load_skill.sh
to activate them.`
// executeCodeTool is the single built-in tool exposed to the model.
var executeCodeTool = backend.Tool{
Name: "execute_code",
Description: "Execute shell code or a named tool script in a sandboxed environment. " +
"Use 'code' for inline bash, 'tool'+'args' for a named script, " +
"or 'pipe' for a sequence of {tool, args} steps.",
Parameters: json.RawMessage(`{
"type": "object",
"properties": {
"code": {"type": "string", "description": "Inline shell code to run (bash)."},
"language": {"type": "string", "description": "Language interpreter (default: bash)."},
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."},
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."},
"tool": {"type": "string", "description": "Named tool script to run instead of inline code."},
"args": {"type": "array", "items": {"type": "string"}, "description": "Arguments for the tool script."},
"pipe": {"type": "array", "description": "Pipeline: array of {tool, args} objects run in sequence."}
}
}`),
}
type model struct {
textarea textarea.Model
viewport viewport.Model
session *agent.Session
loopcfg agent.Config
display []string
ready bool
hooks map[string]string
}
func main() {
modelName := os.Getenv("OLLIE_MODEL")
if modelName == "" && len(os.Args) > 1 {
modelName = os.Args[1]
}
if modelName == "" {
modelName = "qwen3:8b"
}
hooks := make(map[string]string)
if len(os.Args) > 2 {
cfg, err := config.Load(os.Args[2])
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
if cfg.Hooks != nil {
hooks = cfg.Hooks
}
}
be, err := backend.New()
if err != nil {
log.Fatalf("Failed to create backend: %v", err)
}
home, _ := os.UserHomeDir()
builtinExec := execpkg.New(
home+"/.local/state/ollie",
home+"/.cache/ollie/exec",
)
loopcfg := agent.Config{
Backend: be,
Model: modelName,
SystemPrompt: systemPrompt,
Tools: []backend.Tool{executeCodeTool},
Exec: func(name string, args json.RawMessage) (string, error) {
if name == "execute_code" {
return dispatchBuiltinExec(builtinExec, args)
}
return "", fmt.Errorf("unknown tool: %s", name)
},
MaxSteps: 20,
}
ta := textarea.New()
ta.Placeholder = "Type your message..."
ta.Prompt = ""
ta.ShowLineNumbers = false
ta.CharLimit = 0
ta.SetHeight(5)
ta.KeyMap.InsertNewline = key.NewBinding(key.WithKeys("ctrl+j"))
ta.Focus()
p := tea.NewProgram(model{
textarea: ta,
loopcfg: loopcfg,
hooks: hooks,
})
if hook := hooks["agentSpawn"]; hook != "" {
exec.Command("sh", "-c", hook).Run()
}
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
// -- tea model --
func (m model) Init() tea.Cmd {
return textarea.Blink
}
type responseMsg struct {
display []string
session *agent.Session
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
if !m.ready {
m.viewport = viewport.New(msg.Width, msg.Height-5)
m.ready = true
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - 5
}
m.textarea.SetWidth(msg.Width)
case responseMsg:
m.display = msg.display
m.session = msg.session
m.viewport.SetContent(strings.Join(m.display, "\n"))
m.viewport.GotoBottom()
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
return m, tea.Quit
case "enter":
input := strings.TrimSpace(m.textarea.Value())
if input == "" {
return m, nil
}
m.display = append(m.display, "You: "+input)
m.viewport.SetContent(strings.Join(m.display, "\n"))
m.viewport.GotoBottom()
m.textarea.Reset()
if hook := m.hooks["userPromptSubmit"]; hook != "" {
exec.Command("sh", "-c", hook).Run()
}
return m, m.runLoop(input)
}
}
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
func (m model) runLoop(input string) tea.Cmd {
loopcfg := m.loopcfg
session := m.session
hooks := m.hooks
display := append([]string{}, m.display...)
return func() tea.Msg {
if session == nil {
session = agent.NewSession(input)
} else {
session.AppendUserMessage(input)
}
var lines []string
loopcfg.Output = func(msg agent.OutputMsg) {
switch msg.Role {
case "assistant":
lines = append(lines, "Bot: "+msg.Content)
case "tool":
lines = append(lines, fmt.Sprintf("→ %s: %s", msg.Name, msg.Content))
case "error":
lines = append(lines, "Error: "+msg.Content)
}
}
if err := agent.New(loopcfg).Run(context.Background(), session); err != nil {
display = append(display, "Error: "+err.Error())
} else {
display = append(display, lines...)
}
if hook := hooks["stop"]; hook != "" {
exec.Command("sh", "-c", hook).Run()
}
return responseMsg{display: display, session: session}
}
}
func (m model) View() string {
if !m.ready {
return "Loading..."
}
return m.viewport.View() + "\n" + m.textarea.View()
}
// -- helpers --
// dispatchBuiltinExec handles execute_code natively.
func dispatchBuiltinExec(e *execpkg.Executor, args json.RawMessage) (string, error) {
var a struct {
Code string `json:"code"`
Language string `json:"language"`
Timeout int `json:"timeout"`
Sandbox string `json:"sandbox"`
Tool string `json:"tool"`
Args []string `json:"args"`
Pipe []execpkg.PipeStep `json:"pipe"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("execute_code: bad args: %w", err)
}
code := a.Code
trusted := false
switch {
case len(a.Pipe) > 0:
var err error
code, trusted, err = execpkg.BuildPipeline(a.Pipe)
if err != nil {
return "", err
}
case a.Tool != "":
toolCode, err := execpkg.ReadTool(a.Tool)
if err != nil {
return "", err
}
code = toolCode
trusted = true
if len(a.Args) > 0 {
var escaped []string
for _, arg := range a.Args {
escaped = append(escaped, "'"+strings.ReplaceAll(arg, "'", "'\\''")+"'")
}
code = fmt.Sprintf("set -- %s\n%s", strings.Join(escaped, " "), code)
}
}
if code == "" {
return "", fmt.Errorf("execute_code: one of 'code', 'tool', or 'pipe' is required")
}
if a.Language == "" {
a.Language = "bash"
}
if a.Timeout <= 0 {
a.Timeout = 30
}
if a.Sandbox == "" {
a.Sandbox = "default"
}
return e.Execute(code, a.Language, a.Timeout, a.Sandbox, trusted)
}