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:
parent
fa219e06aa
commit
9b7de31a07
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue