From 9b7de31a07d89a3aa671fa6a5bb3873dd73d0d64 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 27 Aug 2026 10:03:58 +0200 Subject: [PATCH] add doc.go files; decompose agent package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit doc.go: - agent, backend, session, fs, bypass, toolclient (olliesrv) - server, sandbox, registry (toolsrv) - protocol, metadata (toolsrv shared) - log, util, skills agent package decomposition: - chat.go: chat log, streaming, plan methods - state.go: State, Reply, WaitChange, SignalCh, emit - peer.go: AddPeer, RemovePeer, Peers - subagent.go: Depth, IncChildren, DecChildren, ActiveChildren - agent.go: 881 → 638 lines (core struct, identity, backend, runtime) --- cmd/olliesrv/internal/agent/agent.go | 243 ------------------------ cmd/olliesrv/internal/agent/chat.go | 105 ++++++++++ cmd/olliesrv/internal/agent/doc.go | 41 ++++ cmd/olliesrv/internal/agent/peer.go | 32 ++++ cmd/olliesrv/internal/agent/state.go | 105 ++++++++++ cmd/olliesrv/internal/agent/subagent.go | 16 ++ cmd/olliesrv/internal/backend/doc.go | 32 ++++ cmd/olliesrv/internal/bypass/doc.go | 31 +++ cmd/olliesrv/internal/fs/doc.go | 45 +++++ cmd/olliesrv/internal/session/doc.go | 35 ++++ cmd/olliesrv/internal/toolclient/doc.go | 30 +++ cmd/toolsrv/internal/registry/doc.go | 27 +++ cmd/toolsrv/internal/sandbox/doc.go | 35 ++++ cmd/toolsrv/internal/server/doc.go | 29 +++ log/doc.go | 23 +++ skills/doc.go | 33 ++++ toolsrv/metadata/doc.go | 33 ++++ toolsrv/protocol/doc.go | 25 +++ util/doc.go | 20 ++ 19 files changed, 697 insertions(+), 243 deletions(-) create mode 100644 cmd/olliesrv/internal/agent/chat.go create mode 100644 cmd/olliesrv/internal/agent/doc.go create mode 100644 cmd/olliesrv/internal/agent/peer.go create mode 100644 cmd/olliesrv/internal/agent/state.go create mode 100644 cmd/olliesrv/internal/agent/subagent.go create mode 100644 cmd/olliesrv/internal/backend/doc.go create mode 100644 cmd/olliesrv/internal/bypass/doc.go create mode 100644 cmd/olliesrv/internal/fs/doc.go create mode 100644 cmd/olliesrv/internal/session/doc.go create mode 100644 cmd/olliesrv/internal/toolclient/doc.go create mode 100644 cmd/toolsrv/internal/registry/doc.go create mode 100644 cmd/toolsrv/internal/sandbox/doc.go create mode 100644 cmd/toolsrv/internal/server/doc.go create mode 100644 log/doc.go create mode 100644 skills/doc.go create mode 100644 toolsrv/metadata/doc.go create mode 100644 toolsrv/protocol/doc.go create mode 100644 util/doc.go diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index fa250d0..4539c71 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -141,147 +141,6 @@ func (ag *Agent) ID() string { return ag.id } // ParentID returns the immutable ID of the agent that spawned this agent. func (ag *Agent) ParentID() string { return ag.parentID } -// Depth returns the sub-agent depth (0=top-level). -func (ag *Agent) Depth() int { return ag.depth } - -// SetDepth sets the sub-agent depth. -func (ag *Agent) SetDepth(d int) { ag.depth = d } - -// IncChildren increments the active child sub-agent counter. -func (ag *Agent) IncChildren() { ag.activeChildren.Add(1) } - -// DecChildren decrements the active child sub-agent counter. -func (ag *Agent) DecChildren() { ag.activeChildren.Add(-1) } - -// ActiveChildren returns the number of currently-running child sub-agents. -func (ag *Agent) ActiveChildren() int32 { return ag.activeChildren.Load() } - -// AddPeer adds a peer agent by name. -func (ag *Agent) AddPeer(name string) { - ag.peerMu.Lock() - if ag.peers == nil { - ag.peers = make(map[string]struct{}) - } - ag.peers[name] = struct{}{} - ag.peerMu.Unlock() -} - -// RemovePeer removes a peer agent by name. -func (ag *Agent) RemovePeer(name string) { - ag.peerMu.Lock() - delete(ag.peers, name) - ag.peerMu.Unlock() -} - -// Peers returns the sorted list of peer agent names. -func (ag *Agent) Peers() []string { - ag.peerMu.RLock() - defer ag.peerMu.RUnlock() - out := make([]string, 0, len(ag.peers)) - for name := range ag.peers { - out = append(out, name) - } - slices.Sort(out) - return out -} - -// --- Chat log methods --- - -const maxChatLogBytes = 16 * 1024 * 1024 - -// AppendChat appends data to the chat log and notifies stream readers. -func (ag *Agent) AppendChat(data []byte) { - if len(data) == 0 { - return - } - ag.chatMu.Lock() - ag.chatLog = append(ag.chatLog, data...) - if len(ag.chatLog) > maxChatLogBytes { - drop := len(ag.chatLog) - maxChatLogBytes - ag.chatLog = append([]byte(nil), ag.chatLog[drop:]...) - ag.chatStart += drop - } - ag.chatVers++ - ag.chatMu.Unlock() - ag.chatCond.Broadcast() - ag.chatSignalMu.Lock() - close(ag.chatSignalCh) - ag.chatSignalCh = make(chan struct{}) - ag.chatSignalMu.Unlock() -} - -// EnsureTrailingNewline appends a newline if the log doesn't end with one. -func (ag *Agent) EnsureTrailingNewline() { - ag.chatMu.Lock() - if len(ag.chatLog) > 0 && ag.chatLog[len(ag.chatLog)-1] != '\n' { - ag.chatLog = append(ag.chatLog, '\n') - } - ag.chatVers++ - ag.chatMu.Unlock() -} - -// ChatMu returns the chat log mutex for external locking (streaming). -func (ag *Agent) ChatMu() *sync.RWMutex { return &ag.chatMu } - -// ChatCond returns the condvar for blocking chat readers. -func (ag *Agent) ChatCond() *sync.Cond { return ag.chatCond } - -// ChatSignal returns the current chat signal channel (closed on new chat data). -func (ag *Agent) ChatSignal() <-chan struct{} { - ag.chatSignalMu.Lock() - ch := ag.chatSignalCh - ag.chatSignalMu.Unlock() - return ch -} - -// ChatLog returns the raw chat log bytes (caller must hold ChatMu.RLock). -func (ag *Agent) ChatLog() []byte { return ag.chatLog } - -// ChatRead returns new chat data since the given offset (base). -// Returns (data, nextBase, error). If no new data, returns empty data. -func (ag *Agent) ChatRead(base string) ([]byte, string, error) { - var offset int - if base != "" { - fmt.Sscanf(base, "%d", &offset) - } else { - ag.chatMu.RLock() - offset = len(ag.chatLog) + ag.chatStart - ag.chatMu.RUnlock() - } - ag.chatMu.RLock() - if offset < ag.chatStart { - offset = ag.chatStart - } - localOffset := offset - ag.chatStart - log := ag.chatLog - if len(log) <= localOffset { - ag.chatMu.RUnlock() - return nil, fmt.Sprintf("%d", offset), nil - } - data := make([]byte, len(log)-localOffset) - copy(data, log[localOffset:]) - newOffset := ag.chatStart + len(log) - ag.chatMu.RUnlock() - return data, fmt.Sprintf("%d", newOffset), nil -} - -// Plan returns a copy of the plan. -func (ag *Agent) Plan() []byte { - ag.chatMu.RLock() - p := make([]byte, len(ag.plan)) - copy(p, ag.plan) - ag.chatMu.RUnlock() - return p -} - -// SetPlan replaces the plan. -func (ag *Agent) SetPlan(data []byte) { - ag.chatMu.Lock() - ag.plan = make([]byte, len(data)) - copy(ag.plan, data) - ag.chatMu.Unlock() -} - // BackendName returns the name of the active backend. func (ag *Agent) BackendName() string { if ag.runtime.Backend == nil { @@ -323,102 +182,6 @@ func (ag *Agent) SwitchBackend(name string) error { return nil } -// State returns the agent's current execution state. -func (ag *Agent) State() string { - ag.stateMu.RLock() - s := ag.state - ag.stateMu.RUnlock() - return s -} - -// SetState sets the agent's execution state and notifies waiters. -func (ag *Agent) SetState(state string) { - ag.stateMu.Lock() - ag.state = state - ag.stateMu.Unlock() - ag.notifyChange() - if ag.onStateChange != nil { - ag.onStateChange(ag.id, state) - } -} - -// Reply returns the agent's last assistant response. -func (ag *Agent) Reply() string { - ag.stateMu.RLock() - r := ag.reply - ag.stateMu.RUnlock() - return r -} - -// SetOnStateChange sets a callback invoked whenever agent state changes. -func (ag *Agent) SetOnStateChange(fn func(agentID, state string)) { - ag.onStateChange = fn -} - -// SetReply sets the agent's last response. -func (ag *Agent) setReply(reply string) { - ag.stateMu.Lock() - ag.reply = reply - ag.stateMu.Unlock() -} - -// notifyChange wakes all goroutines waiting on state changes. -func (ag *Agent) notifyChange() { - ag.signalMu.Lock() - close(ag.signalCh) - ag.signalCh = make(chan struct{}) - ag.signalMu.Unlock() -} - -// SignalCh returns the current signal channel (closed on any change). -func (ag *Agent) SignalCh() <-chan struct{} { - ag.signalMu.Lock() - ch := ag.signalCh - ag.signalMu.Unlock() - return ch -} - -// WaitChange blocks until the agent's state differs from current. -// Returns the new value and true, or ("", false) if ctx is cancelled. -func (ag *Agent) WaitChange(ctx context.Context, field, current string) (string, bool) { - for { - // Snapshot the signal channel before reading state. - ag.signalMu.Lock() - ch := ag.signalCh - ag.signalMu.Unlock() - - var val string - switch field { - case WatchState: - val = ag.State() - case WatchFeed: - val = ag.Feed.Hash() - if val == "" { - val = current // no data yet — block - } - default: - return "", false - } - if val != current { - return val, true - } - // Wait for either a state change or context cancellation. - select { - case <-ch: - // Changed — loop to re-check. - case <-ctx.Done(): - return "", false - } - } -} - -// emit sends an event to the agent's output handler. -func (ag *Agent) emit(ev Event) { - if ag.output != nil { - ag.output(ev) - } -} - // SetToolServer updates the tool server connection and factory. // Used when resuming a paused session that was restored without infra. func (ag *Agent) SetToolServer(newToolServer func() *toolclient.ToolsrvConn, conn *toolclient.ToolsrvConn) { @@ -582,12 +345,6 @@ type actionHandle struct { done chan struct{} } -// WatchField names supported by Agent.WaitChange. -const ( - WatchState = "state" - WatchFeed = "feed" -) - // SetSessionEnv injects session env vars into the execute server. func (ag *Agent) SetSessionEnv(sessionID string) { if ag.runtime == nil || ag.runtime.ToolServer == nil { diff --git a/cmd/olliesrv/internal/agent/chat.go b/cmd/olliesrv/internal/agent/chat.go new file mode 100644 index 0000000..730dc3d --- /dev/null +++ b/cmd/olliesrv/internal/agent/chat.go @@ -0,0 +1,105 @@ +package agent + +import ( + "fmt" + "sync" +) + +// --- Chat log methods --- + +const maxChatLogBytes = 16 * 1024 * 1024 + +// AppendChat appends data to the chat log and notifies stream readers. +func (ag *Agent) AppendChat(data []byte) { + if len(data) == 0 { + return + } + ag.chatMu.Lock() + ag.chatLog = append(ag.chatLog, data...) + if len(ag.chatLog) > maxChatLogBytes { + drop := len(ag.chatLog) - maxChatLogBytes + ag.chatLog = append([]byte(nil), ag.chatLog[drop:]...) + ag.chatStart += drop + } + ag.chatVers++ + ag.chatMu.Unlock() + ag.chatCond.Broadcast() + ag.chatSignalMu.Lock() + close(ag.chatSignalCh) + ag.chatSignalCh = make(chan struct{}) + ag.chatSignalMu.Unlock() +} + +// EnsureTrailingNewline appends a newline if the log doesn't end with one. +func (ag *Agent) EnsureTrailingNewline() { + ag.chatMu.Lock() + if len(ag.chatLog) > 0 && ag.chatLog[len(ag.chatLog)-1] != '\n' { + ag.chatLog = append(ag.chatLog, '\n') + } + ag.chatVers++ + ag.chatMu.Unlock() +} + +// ChatMu returns the chat log mutex for external locking (streaming). +func (ag *Agent) ChatMu() *sync.RWMutex { return &ag.chatMu } + +// ChatCond returns the condvar for blocking chat readers. +func (ag *Agent) ChatCond() *sync.Cond { return ag.chatCond } + +// ChatSignal returns the current chat signal channel (closed on new chat data). +func (ag *Agent) ChatSignal() <-chan struct{} { + ag.chatSignalMu.Lock() + ch := ag.chatSignalCh + ag.chatSignalMu.Unlock() + return ch +} + +// ChatLog returns the raw chat log bytes (caller must hold ChatMu.RLock). +func (ag *Agent) ChatLog() []byte { return ag.chatLog } + +// ChatRead returns new chat data since the given offset (base). +// Returns (data, nextBase, error). If no new data, returns empty data. +func (ag *Agent) ChatRead(base string) ([]byte, string, error) { + var offset int + if base != "" { + fmt.Sscanf(base, "%d", &offset) + } else { + ag.chatMu.RLock() + offset = len(ag.chatLog) + ag.chatStart + ag.chatMu.RUnlock() + } + ag.chatMu.RLock() + if offset < ag.chatStart { + offset = ag.chatStart + } + localOffset := offset - ag.chatStart + log := ag.chatLog + if len(log) <= localOffset { + ag.chatMu.RUnlock() + return nil, fmt.Sprintf("%d", offset), nil + } + data := make([]byte, len(log)-localOffset) + copy(data, log[localOffset:]) + newOffset := ag.chatStart + len(log) + ag.chatMu.RUnlock() + return data, fmt.Sprintf("%d", newOffset), nil +} + +// --- Plan methods --- + +// Plan returns a copy of the plan. +func (ag *Agent) Plan() []byte { + ag.chatMu.RLock() + p := make([]byte, len(ag.plan)) + copy(p, ag.plan) + ag.chatMu.RUnlock() + return p +} + +// SetPlan replaces the plan. +func (ag *Agent) SetPlan(data []byte) { + ag.chatMu.Lock() + ag.plan = make([]byte, len(data)) + copy(ag.plan, data) + ag.chatMu.Unlock() +} diff --git a/cmd/olliesrv/internal/agent/doc.go b/cmd/olliesrv/internal/agent/doc.go new file mode 100644 index 0000000..a670c59 --- /dev/null +++ b/cmd/olliesrv/internal/agent/doc.go @@ -0,0 +1,41 @@ +// Package agent implements the core agent loop for ollie. +// +// An Agent owns history, runtime state, and the model/tool execution loop. +// It processes user prompts, streams LLM responses, executes tool calls, +// and manages conversation history with automatic compaction. +// +// # Core Types +// +// Agent is the main type. Create one via NewAgent, submit prompts via +// Submit, and observe events via the EventHandler callback. State +// transitions are visible through State() and WaitChange(). +// +// History tracks messages, token usage, cost, and supports multi-zone +// compaction (cold/warm/hot) to manage context window limits. +// +// Runtime holds the active preamble, tool schemas, and backend reference. +// It is rebuilt on profile switches or tool registry changes. +// +// # Execution Model +// +// Submit queues a prompt. The agent loop streams the LLM response, parses +// tool calls (native or text-based), executes them through toolsrv with +// scope-based conflict detection, appends results to history, and repeats +// until the model produces a final response or an error limit is reached. +// +// Tool calls are batched for parallel execution when their scopes don't +// conflict: reads run in parallel, writes to different paths run in +// parallel, but writes to the same path or global-scope tools serialize. +// +// # Context Management +// +// The agent tracks token usage and triggers compaction when approaching +// context limits. Compaction summarizes older messages while preserving +// recent context (hot zone) and decision summaries (warm zone). +// +// # Sub-Agents +// +// Sub-agents are transient child agents with independent context windows. +// They receive a snapshot of the parent's history and return only their +// final reply. Use them for parallel work or to preserve main context. +package agent diff --git a/cmd/olliesrv/internal/agent/peer.go b/cmd/olliesrv/internal/agent/peer.go new file mode 100644 index 0000000..791d6b7 --- /dev/null +++ b/cmd/olliesrv/internal/agent/peer.go @@ -0,0 +1,32 @@ +package agent + +import "slices" + +// AddPeer adds a peer agent by name. +func (ag *Agent) AddPeer(name string) { + ag.peerMu.Lock() + if ag.peers == nil { + ag.peers = make(map[string]struct{}) + } + ag.peers[name] = struct{}{} + ag.peerMu.Unlock() +} + +// RemovePeer removes a peer agent by name. +func (ag *Agent) RemovePeer(name string) { + ag.peerMu.Lock() + delete(ag.peers, name) + ag.peerMu.Unlock() +} + +// Peers returns the sorted list of peer agent names. +func (ag *Agent) Peers() []string { + ag.peerMu.RLock() + defer ag.peerMu.RUnlock() + out := make([]string, 0, len(ag.peers)) + for name := range ag.peers { + out = append(out, name) + } + slices.Sort(out) + return out +} diff --git a/cmd/olliesrv/internal/agent/state.go b/cmd/olliesrv/internal/agent/state.go new file mode 100644 index 0000000..26c730c --- /dev/null +++ b/cmd/olliesrv/internal/agent/state.go @@ -0,0 +1,105 @@ +package agent + +import "context" + +// WatchField names supported by Agent.WaitChange. +const ( + WatchState = "state" + WatchFeed = "feed" +) + +// State returns the agent's current execution state. +func (ag *Agent) State() string { + ag.stateMu.RLock() + s := ag.state + ag.stateMu.RUnlock() + return s +} + +// SetState sets the agent's execution state and notifies waiters. +func (ag *Agent) SetState(state string) { + ag.stateMu.Lock() + ag.state = state + ag.stateMu.Unlock() + ag.notifyChange() + if ag.onStateChange != nil { + ag.onStateChange(ag.id, state) + } +} + +// Reply returns the agent's last assistant response. +func (ag *Agent) Reply() string { + ag.stateMu.RLock() + r := ag.reply + ag.stateMu.RUnlock() + return r +} + +// SetOnStateChange sets a callback invoked whenever agent state changes. +func (ag *Agent) SetOnStateChange(fn func(agentID, state string)) { + ag.onStateChange = fn +} + +// setReply sets the agent's last response. +func (ag *Agent) setReply(reply string) { + ag.stateMu.Lock() + ag.reply = reply + ag.stateMu.Unlock() +} + +// notifyChange wakes all goroutines waiting on state changes. +func (ag *Agent) notifyChange() { + ag.signalMu.Lock() + close(ag.signalCh) + ag.signalCh = make(chan struct{}) + ag.signalMu.Unlock() +} + +// SignalCh returns the current signal channel (closed on any change). +func (ag *Agent) SignalCh() <-chan struct{} { + ag.signalMu.Lock() + ch := ag.signalCh + ag.signalMu.Unlock() + return ch +} + +// WaitChange blocks until the agent's state differs from current. +// Returns the new value and true, or ("", false) if ctx is cancelled. +func (ag *Agent) WaitChange(ctx context.Context, field, current string) (string, bool) { + for { + // Snapshot the signal channel before reading state. + ag.signalMu.Lock() + ch := ag.signalCh + ag.signalMu.Unlock() + + var val string + switch field { + case WatchState: + val = ag.State() + case WatchFeed: + val = ag.Feed.Hash() + if val == "" { + val = current // no data yet — block + } + default: + return "", false + } + if val != current { + return val, true + } + // Wait for either a state change or context cancellation. + select { + case <-ch: + // Changed — loop to re-check. + case <-ctx.Done(): + return "", false + } + } +} + +// emit sends an event to the agent's output handler. +func (ag *Agent) emit(ev Event) { + if ag.output != nil { + ag.output(ev) + } +} diff --git a/cmd/olliesrv/internal/agent/subagent.go b/cmd/olliesrv/internal/agent/subagent.go new file mode 100644 index 0000000..ff8eed5 --- /dev/null +++ b/cmd/olliesrv/internal/agent/subagent.go @@ -0,0 +1,16 @@ +package agent + +// Depth returns the sub-agent depth (0=top-level). +func (ag *Agent) Depth() int { return ag.depth } + +// SetDepth sets the sub-agent depth. +func (ag *Agent) SetDepth(d int) { ag.depth = d } + +// IncChildren increments the active child count. +func (ag *Agent) IncChildren() { ag.activeChildren.Add(1) } + +// DecChildren decrements the active child count. +func (ag *Agent) DecChildren() { ag.activeChildren.Add(-1) } + +// ActiveChildren returns the number of currently-running child sub-agents. +func (ag *Agent) ActiveChildren() int32 { return ag.activeChildren.Load() } diff --git a/cmd/olliesrv/internal/backend/doc.go b/cmd/olliesrv/internal/backend/doc.go new file mode 100644 index 0000000..4220916 --- /dev/null +++ b/cmd/olliesrv/internal/backend/doc.go @@ -0,0 +1,32 @@ +// Package backend defines the Backend interface and implementations for +// LLM providers. +// +// All backends implement ChatStream for streaming completions. Provider- +// specific wire formats are handled inside each implementation; the agent +// loop sees only the canonical Message and StreamEvent types. +// +// # Supported Backends +// +// - anthropic: Claude models via the Anthropic API +// - openai: GPT models via the OpenAI API +// - openrouter: Multi-model routing via OpenRouter +// - ollama: Local models via Ollama +// - gemini: Gemini models via Google AI +// - copilot: GitHub Copilot +// - kiro: Kiro API +// +// # Error Types +// +// Backends return typed errors for conditions the caller can handle: +// +// - RateLimitError: HTTP 429; includes RetryAfter hint +// - TransientError: 5xx or network errors; safe to retry +// - ContextOverflowError: request exceeds context window; compact and retry +// - ToolUnsupportedError: model doesn't support tool calling +// +// # Configuration +// +// Backend selection and credentials come from backends.conf. The file +// format is INI-style with sections per backend. Environment variables +// provide fallbacks for API keys. +package backend diff --git a/cmd/olliesrv/internal/bypass/doc.go b/cmd/olliesrv/internal/bypass/doc.go new file mode 100644 index 0000000..3962ce0 --- /dev/null +++ b/cmd/olliesrv/internal/bypass/doc.go @@ -0,0 +1,31 @@ +// Package bypass provides the sandbox bypass broker for olliesrv. +// +// When a tool needs to escape the Landlock sandbox (e.g., to access a +// path outside the allowed set), it requests bypass approval. The broker +// manages pending requests, policy evaluation, user notification, and +// resolution. +// +// # Flow +// +// 1. Tool calls bypass.Request() with command and path +// 2. Broker checks policy — auto-approve, auto-deny, or prompt user +// 3. If prompting, request goes to pending queue +// 4. Frontend reads bypass/pending and shows approval UI +// 5. User approves/denies; frontend writes bypass/resolve +// 6. Broker unblocks the waiting tool with the decision +// +// # Policy +// +// The policy file ($XDG_CONFIG_HOME/ollie/bypass-policy.conf) contains +// glob patterns for auto-approval or auto-denial. Format: +// +// allow /home/user/projects/* +// deny /etc/* +// +// Paths not matching any rule require interactive approval. +// +// # Rate Limiting +// +// The broker rate-limits requests per session to prevent runaway tools +// from flooding the user with approval prompts. +package bypass diff --git a/cmd/olliesrv/internal/fs/doc.go b/cmd/olliesrv/internal/fs/doc.go new file mode 100644 index 0000000..7b246e3 --- /dev/null +++ b/cmd/olliesrv/internal/fs/doc.go @@ -0,0 +1,45 @@ +// Package fs defines the 9P namespace for olliesrv. +// +// The namespace is declared in spec.go using the virtfs EDSL. All session +// and agent state is exposed as files: creating sessions, submitting +// prompts, reading chat history, and observing state changes are all +// file operations. +// +// # Namespace Structure +// +// Root level: +// +// /agents - available agent profile names +// /backends - available backend names +// /models - available models (backend\tmodel per line) +// /ctl - server control (invalidate, kill) +// /generate - one-shot generation (rdwr) +// /session/ - session directory +// +// Session level (/session/{name}/): +// +// env - session environment variables +// goal - session objective (write triggers workflow) +// agent/ - agents within this session +// agent/new - create agent (rdwr) +// +// Agent level (/session/{name}/agent/{agent}/): +// +// prompt - submit prompt (write) +// chat - conversation history (streaming read) +// statewait - block until state changes +// plan - agent's plan (survives compaction) +// ctl - agent control commands +// +// # Blocking Reads +// +// Files like statewait and eventwait block until their condition is met. +// This is the primary coordination mechanism — clients read these files +// in a loop to react to state changes. +// +// # Implementation +// +// The spec is pure declaration; handlers are closures that capture their +// targets. NewRoot() builds the virtfs.Tree from the spec. The tree is +// served directly by the 9P server. +package fs diff --git a/cmd/olliesrv/internal/session/doc.go b/cmd/olliesrv/internal/session/doc.go new file mode 100644 index 0000000..e515159 --- /dev/null +++ b/cmd/olliesrv/internal/session/doc.go @@ -0,0 +1,35 @@ +// Package session owns session lifecycle, persistence, and agent management. +// +// A Session groups agents, manages the tool server connection, and provides +// the context for cancellation propagation. Sessions can be paused (releasing +// resources) and resumed. +// +// # Lifecycle +// +// Sessions are created via the registry (NewSession or restored from disk). +// Each session owns: +// +// - A unique ID and optional friendly name +// - A list of agents +// - A tool server process and connection (local or remote) +// - A goal (session-level objective) that can trigger workflows +// - Autosave state for persistence +// +// # Persistence +// +// Sessions are saved to JSON files under the sessions directory. Save +// happens automatically on state changes (debounced). Restore rebuilds +// agents from saved history and reconnects to tool servers. +// +// # Tool Server +// +// Each session owns a toolsrv process. The session manages spawning, +// connection, and the bypass approval loop. On pause, the tool server +// is stopped; on resume, a new one is spawned. +// +// # Events +// +// The package publishes session events (pause, resume, agent changes) +// through an internal event bus. Use SubscribeWait/UnsubscribeWait for +// blocking waits on specific events. +package session diff --git a/cmd/olliesrv/internal/toolclient/doc.go b/cmd/olliesrv/internal/toolclient/doc.go new file mode 100644 index 0000000..43fd9d9 --- /dev/null +++ b/cmd/olliesrv/internal/toolclient/doc.go @@ -0,0 +1,30 @@ +// Package toolclient manages the toolsrv connection from olliesrv. +// +// It handles process spawning (local or remote via SSH), connection +// lifecycle, and the 9P client for tool operations. +// +// # Process Management +// +// ProcessKeeper maintains a running toolsrv process, respawning it if +// it dies. It provides stable connection dialing even across restarts. +// +// For remote execution, toolsrv is deployed over SSH with socket +// forwarding. The local olliesrv talks to the remote toolsrv as if +// it were local. +// +// # Connection +// +// ToolsrvConn wraps a 9P connection to toolsrv. It provides typed +// methods for common operations: +// +// - ListTools, LoadTool, UnloadTool — tool registry +// - CallTool, CallToolBackground — tool execution +// - SignalDetached, GetDetachedOutput — background process management +// - ReadBypassPending, ResolveBypass — sandbox bypass +// +// # Authentication +// +// Connections are authenticated with a shared secret established on +// first connection. Subsequent connections (e.g., after respawn) must +// provide the same secret. +package toolclient diff --git a/cmd/toolsrv/internal/registry/doc.go b/cmd/toolsrv/internal/registry/doc.go new file mode 100644 index 0000000..9d25fd9 --- /dev/null +++ b/cmd/toolsrv/internal/registry/doc.go @@ -0,0 +1,27 @@ +// Package registry manages per-agent tool registries within a toolsrv session. +// +// Each agent in a session has its own set of loaded tools. The registry +// tracks which tools are loaded for each agent and provides metadata for +// prompt generation. +// +// # Per-Agent Scoping +// +// Tools are scoped to agents via their agent ID. Operations: +// +// - LoadTool(agentID, name): add tool to agent's set +// - UnloadTool(agentID, name): remove tool from agent's set +// - ClearTools(agentID): remove all tools for an agent +// - GetTools(agentID): list loaded tools with metadata +// +// # Metadata +// +// Tool metadata comes from .meta files in the tools directory. Metadata +// includes name, description, input schema, scope (read/write/global), +// timeout, and output format. +// +// # Revision Tracking +// +// Each agent's tool set has a revision number that increments on any +// change. Clients can poll this to detect when to refresh their tool +// list. +package registry diff --git a/cmd/toolsrv/internal/sandbox/doc.go b/cmd/toolsrv/internal/sandbox/doc.go new file mode 100644 index 0000000..6eedb23 --- /dev/null +++ b/cmd/toolsrv/internal/sandbox/doc.go @@ -0,0 +1,35 @@ +// Package sandbox provides Landlock-based filesystem sandboxing for tool execution. +// +// Tools run in a restricted environment where filesystem access is limited +// to explicitly allowed paths. The sandbox is configured via sandbox.yaml +// and enforced using Linux Landlock. +// +// # Configuration +// +// sandbox.yaml defines named profiles with path rules: +// +// default: +// ro: +// - /usr +// - /lib +// rw: +// - ${cwd} +// - /tmp +// +// Environment variables in paths are expanded at runtime. The special +// variable ${cwd} refers to the agent's working directory. +// +// # Enforcement +// +// On Linux, a native helper binary applies Landlock restrictions before +// exec'ing the tool. The helper receives the sandbox profile via base64- +// encoded JSON on the command line. +// +// On non-Linux platforms, sandboxing is a no-op (the tool runs unrestricted). +// +// # Bypass +// +// Tools can request bypass for operations outside the sandbox. These +// requests are routed to the bypass broker in olliesrv for policy +// evaluation and optional user approval. +package sandbox diff --git a/cmd/toolsrv/internal/server/doc.go b/cmd/toolsrv/internal/server/doc.go new file mode 100644 index 0000000..193cc3b --- /dev/null +++ b/cmd/toolsrv/internal/server/doc.go @@ -0,0 +1,29 @@ +// Package server provides the 9P namespace and process state for toolsrv. +// +// The namespace is declared using virtfs and exposes tool execution, +// process management, and bypass request handling. +// +// # Namespace Structure +// +// /ctl - control commands (load, unload, clear) +// /tools - tool listing (rdwr: write agent ID, read JSON) +// /all - all available tools on disk +// /info - host info (platform, arch) +// /bypass/ - bypass request handling +// /proc/ - process management +// /proc/new - execute tool (blocking rdwr) +// /proc/new.bg - execute in background (returns PID) +// /proc/{pid}/ - per-process files (out, wait, stat, ctl) +// +// # Process Execution +// +// Tools are executed via /proc/new (foreground) or /proc/new.bg (background). +// The request includes token, tool name, and arguments. Foreground calls +// block until completion; background calls return immediately with a PID. +// +// # Path Locking +// +// Write-scope tools acquire path-based locks to prevent concurrent writes +// to the same file. Global-scope tools acquire a global lock that serializes +// with everything. Read-scope tools never lock. +package server diff --git a/log/doc.go b/log/doc.go new file mode 100644 index 0000000..5296c38 --- /dev/null +++ b/log/doc.go @@ -0,0 +1,23 @@ +// Package log provides structured logging for ollie. +// +// The package supports log levels (debug, info, warn, error) and can +// be configured via environment variables or programmatically. +// +// # Usage +// +// log := log.New("session") +// log.Info("starting session %s", id) +// log.Debug("verbose detail: %v", data) +// +// # Configuration +// +// Set OLLIE_LOG_LEVEL to control output: debug, info, warn, error. +// Default is info. Debug output is useful for development but verbose +// in production. +// +// # Output Format +// +// Messages include timestamp, level, logger name, and message: +// +// 2024-01-15T10:30:00 INFO [session] starting session abc123 +package log diff --git a/skills/doc.go b/skills/doc.go new file mode 100644 index 0000000..d79a681 --- /dev/null +++ b/skills/doc.go @@ -0,0 +1,33 @@ +// Package skills provides skill discovery and semantic matching. +// +// Skills are markdown knowledge modules with YAML frontmatter. They inject +// domain expertise into the agent's context when semantically relevant to +// the user's request. +// +// # Skill Format +// +// Skills are markdown files with YAML frontmatter: +// +// --- +// name: git +// description: Git version control operations +// tags: [vcs, commit, branch] +// --- +// +// # Git Skill +// +// Common operations... +// +// # Discovery +// +// LoadSkills scans the skills directory ($XDG_CONFIG_HOME/ollie/skills) +// and parses skill metadata. Skills are indexed by name for lookup and +// by description for semantic matching. +// +// # Matching +// +// MatchSkills uses embedding similarity to find skills relevant to a +// query. The top matches (up to a configured limit) are injected into +// the agent's context. This enables capability discovery without +// requiring exact skill names. +package skills diff --git a/toolsrv/metadata/doc.go b/toolsrv/metadata/doc.go new file mode 100644 index 0000000..7192e67 --- /dev/null +++ b/toolsrv/metadata/doc.go @@ -0,0 +1,33 @@ +// Package metadata provides tool discovery and metadata loading for toolsrv. +// +// Tools are external executables with associated .meta JSON files that +// describe their interface. This package discovers tools on disk and +// parses their metadata. +// +// # Discovery +// +// DiscoverTools scans the tools directory ($XDG_CONFIG_HOME/ollie/tools) +// for executable files with matching .meta sidecar files. Both compiled +// Go tools and script tools use the same discovery mechanism. +// +// # Metadata Format +// +// The .meta file is JSON: +// +// { +// "description": "Short description for prompts", +// "prompt": "## tool_name\n\nDetailed usage...", +// "args": { "path": {"type": "string", "description": "..."} }, +// "tier": "hot", +// "scope": "write", +// "timeout": 30 +// } +// +// Fields: +// - description: one-line summary for tool listing +// - prompt: full documentation injected into system prompt +// - args: JSON Schema for tool arguments +// - tier: hot (always injected), warm, cold (on-demand) +// - scope: read, write, global (for conflict detection) +// - timeout: default execution timeout in seconds +package metadata diff --git a/toolsrv/protocol/doc.go b/toolsrv/protocol/doc.go new file mode 100644 index 0000000..90fbcaf --- /dev/null +++ b/toolsrv/protocol/doc.go @@ -0,0 +1,25 @@ +// Package protocol defines shared types and wire formats for toolsrv communication. +// +// These types are used by both olliesrv (toolclient) and toolsrv to ensure +// consistent serialization of tool metadata, results, and requests. +// +// # Types +// +// - ToolInfo: tool metadata sent from toolsrv to olliesrv +// - ToolResult: structured result from tool execution +// - ToolResultContent: single content item (text or image) +// - BypassRequest: sandbox bypass request +// +// # Payload Format +// +// Tool execution uses a key=value payload format over 9P: +// +// token= +// tool= +// agent= +// path=/some/path +// content=escaped\nvalue +// +// Values are escaped: \n for newlines, \\ for backslashes. ParsePayload +// and escaping helpers handle the conversion. +package protocol diff --git a/util/doc.go b/util/doc.go new file mode 100644 index 0000000..a42b916 --- /dev/null +++ b/util/doc.go @@ -0,0 +1,20 @@ +// Package util provides common utilities for ollie. +// +// # Path Helpers +// +// - CfgDir: returns $XDG_CONFIG_HOME/ollie +// - DataDir: returns $XDG_DATA_HOME/ollie +// - RuntimeDir: returns $XDG_RUNTIME_DIR or /run/user/UID +// - ExpandHome: expands ~ to home directory +// - IsGitRepo: checks if directory contains .git +// +// # UUID Generation +// +// - NewUUID: generates a random UUIDv4 string +// +// # Environment +// +// The package follows XDG Base Directory Specification for locating +// configuration and data files. Environment variables take precedence; +// standard fallbacks are used when unset. +package util