add doc.go files; decompose agent package

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)
This commit is contained in:
Levi Neely 2026-08-27 10:03:58 +02:00
parent fa219e06aa
commit 9b7de31a07
19 changed files with 697 additions and 243 deletions

View File

@ -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 {

View File

@ -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()
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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() }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

23
log/doc.go Normal file
View File

@ -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

33
skills/doc.go Normal file
View File

@ -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

33
toolsrv/metadata/doc.go Normal file
View File

@ -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

25
toolsrv/protocol/doc.go Normal file
View File

@ -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=<session-token>
// tool=<name>
// agent=<agent-id>
// path=/some/path
// content=escaped\nvalue
//
// Values are escaped: \n for newlines, \\ for backslashes. ParsePayload
// and escaping helpers handle the conversion.
package protocol

20
util/doc.go Normal file
View File

@ -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