diff --git a/agent/bead.go b/agent/bead.go deleted file mode 100644 index 56ec1d5..0000000 --- a/agent/bead.go +++ /dev/null @@ -1,109 +0,0 @@ -package agent - -import ( - "encoding/json" - "fmt" - "strings" - - "ollie/backend" -) - -// BeadRef identifies a bead by its mount and ID. -type BeadRef struct { - Mount string // e.g. "pcloudcc-lneely" - ID string // e.g. "bd-abc" -} - -// beadJSON is the wire format returned by read_bead.sh --property json. -type beadJSON struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Status string `json:"status"` - Assignee string `json:"assignee"` - Priority int `json:"priority"` - Blockers []string `json:"blockers,omitempty"` - Labels []string `json:"labels,omitempty"` - CloseReason string `json:"close_reason,omitempty"` -} - -// ScriptRunner executes a named tool script with the given arguments, -// returning its stdout. Implemented by the caller using ollie/exec. -type ScriptRunner func(tool string, args []string) (string, error) - -// BeadState is a bead-backed State implementation. -// The bead provides the goal (title + description) and tracks status. -// Conversation history is kept in-memory for the duration of the session. -type BeadState struct { - ref BeadRef - run ScriptRunner - bead beadJSON - history []backend.Message - complete bool -} - -// LoadBead reads a bead from 9beads and returns a BeadState ready for use. -// It claims the bead before returning. -func LoadBead(ref BeadRef, run ScriptRunner) (*BeadState, error) { - out, err := run("read_bead.sh", []string{ - "--mount", ref.Mount, - "--id", ref.ID, - "--property", "json", - }) - if err != nil { - return nil, fmt.Errorf("read bead %s/%s: %w", ref.Mount, ref.ID, err) - } - - var b beadJSON - if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &b); err != nil { - return nil, fmt.Errorf("parse bead JSON: %w", err) - } - - if _, err := run("claim_bead.sh", []string{ - "--mount", ref.Mount, - "--id", ref.ID, - }); err != nil { - return nil, fmt.Errorf("claim bead %s/%s: %w", ref.Mount, ref.ID, err) - } - - goal := b.Title - if b.Description != "" { - goal = b.Title + "\n\n" + b.Description - } - - return &BeadState{ - ref: ref, - run: run, - bead: b, - history: []backend.Message{ - {Role: "user", Content: goal}, - }, - }, nil -} - -func (s *BeadState) Goal() string { return s.history[0].Content } -func (s *BeadState) History() []backend.Message { return s.history } -func (s *BeadState) IsComplete() bool { return s.complete } - -func (s *BeadState) Update(assistant backend.Message, results []ToolResult) error { - s.history = append(s.history, assistant) - for _, r := range results { - s.history = append(s.history, backend.Message{ - Role: "tool", - Content: r.Content, - ToolCallID: r.ToolCallID, - }) - } - return nil -} - -func (s *BeadState) MarkComplete() error { - if _, err := s.run("complete_bead.sh", []string{ - "--mount", s.ref.Mount, - "--id", s.ref.ID, - }); err != nil { - return fmt.Errorf("complete bead %s/%s: %w", s.ref.Mount, s.ref.ID, err) - } - s.complete = true - return nil -} diff --git a/agent/loop.go b/agent/loop.go index ff3bdde..6e12a43 100644 --- a/agent/loop.go +++ b/agent/loop.go @@ -22,12 +22,13 @@ type OutputMsg struct { // Config holds everything the loop needs to run. type Config struct { - Backend backend.Backend - Model string - Tools []backend.Tool - Exec ToolExecutor - MaxSteps int // 0 → default 1 for sessions, caller sets higher for beads - Output func(msg OutputMsg) // nil → discard + Backend backend.Backend + Model string + Tools []backend.Tool + Exec ToolExecutor + MaxSteps int // 0 → default 1 + Output func(msg OutputMsg) // nil → discard + SystemPrompt string // prepended as system message if non-empty } // Loop implements observe → decide → act → update → terminate. @@ -52,7 +53,11 @@ func (l *Loop) Run(ctx context.Context, state State) error { // 1. Observe: history is the context; already up to date in state. // 2. Decide: call the model. - resp, err := l.cfg.Backend.Chat(ctx, l.cfg.Model, state.History(), l.cfg.Tools) + history := state.History() + if l.cfg.SystemPrompt != "" { + history = append([]backend.Message{{Role: "system", Content: l.cfg.SystemPrompt}}, history...) + } + resp, err := l.cfg.Backend.Chat(ctx, l.cfg.Model, history, l.cfg.Tools) if err != nil { return fmt.Errorf("step %d decide: %w", step, err) } diff --git a/main.go b/main.go index 358297a..17aa6d5 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,21 @@ import ( 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", @@ -82,9 +97,10 @@ func main() { ) loopcfg := agent.Config{ - Backend: be, - Model: modelName, - Tools: []backend.Tool{executeCodeTool}, + 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) diff --git a/ollie b/ollie new file mode 100755 index 0000000..313f0b0 Binary files /dev/null and b/ollie differ