07c739795c
internal/elf/: - BackgroundElf: runs on own goroutine with independent engine, history, and provider. No shared mutable state. - Manager: spawns elfs via router.Select() (picks best arm per task type), tracks lifecycle, WaitAll(), CancelAll(), Cleanup(). internal/tool/agent/: - Agent tool: LLM can call 'agent' to spawn sub-agents. Supports task_type hint for routing, wait/background mode. 5-minute timeout, context cancellation propagated. Concurrent tool execution: - Read-only tools (fs.read, fs.grep, fs.glob, etc.) execute in parallel via goroutines. - Write tools (bash, fs.write, fs.edit) execute sequentially. - Partition by tool.IsReadOnly(). TUI: /elf command explains how to use sub-agents. 5 elf tests. Exit criteria: parent spawns 3 background elfs on different providers, collects and synthesizes results.
185 lines
3.9 KiB
Go
185 lines
3.9 KiB
Go
package elf
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"somegit.dev/Owlibou/gnoma/internal/engine"
|
|
"somegit.dev/Owlibou/gnoma/internal/provider"
|
|
"somegit.dev/Owlibou/gnoma/internal/router"
|
|
"somegit.dev/Owlibou/gnoma/internal/tool"
|
|
)
|
|
|
|
// Manager spawns, tracks, and manages elfs.
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
elfs map[string]Elf
|
|
router *router.Router
|
|
tools *tool.Registry
|
|
logger *slog.Logger
|
|
}
|
|
|
|
type ManagerConfig struct {
|
|
Router *router.Router
|
|
Tools *tool.Registry
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
func NewManager(cfg ManagerConfig) *Manager {
|
|
logger := cfg.Logger
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &Manager{
|
|
elfs: make(map[string]Elf),
|
|
router: cfg.Router,
|
|
tools: cfg.Tools,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Spawn creates a new background elf with a router-selected provider.
|
|
// The elf gets its own engine, history, and tools — no shared state.
|
|
func (m *Manager) Spawn(ctx context.Context, taskType router.TaskType, prompt, systemPrompt string) (Elf, error) {
|
|
// Ask router for the best arm for this task type
|
|
task := router.Task{
|
|
Type: taskType,
|
|
RequiresTools: true,
|
|
Priority: router.PriorityNormal,
|
|
EstimatedTokens: 4000,
|
|
}
|
|
|
|
decision := m.router.Select(task)
|
|
if decision.Error != nil {
|
|
return nil, fmt.Errorf("no arm available for elf: %w", decision.Error)
|
|
}
|
|
|
|
arm := decision.Arm
|
|
m.logger.Info("spawning elf",
|
|
"arm", arm.ID,
|
|
"task_type", taskType,
|
|
"model", arm.ModelName,
|
|
)
|
|
|
|
// Create independent engine for the elf
|
|
eng, err := engine.New(engine.Config{
|
|
Provider: arm.Provider,
|
|
Tools: m.tools,
|
|
System: systemPrompt,
|
|
Model: arm.ModelName,
|
|
MaxTurns: 20,
|
|
Logger: m.logger,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create elf engine: %w", err)
|
|
}
|
|
|
|
elf := SpawnBackground(eng, prompt)
|
|
|
|
m.mu.Lock()
|
|
m.elfs[elf.ID()] = elf
|
|
m.mu.Unlock()
|
|
|
|
m.logger.Info("elf spawned", "id", elf.ID(), "arm", arm.ID)
|
|
return elf, nil
|
|
}
|
|
|
|
// SpawnWithProvider creates an elf using a specific provider (bypasses router).
|
|
func (m *Manager) SpawnWithProvider(prov provider.Provider, model, prompt, systemPrompt string) (Elf, error) {
|
|
eng, err := engine.New(engine.Config{
|
|
Provider: prov,
|
|
Tools: m.tools,
|
|
System: systemPrompt,
|
|
Model: model,
|
|
MaxTurns: 20,
|
|
Logger: m.logger,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create elf engine: %w", err)
|
|
}
|
|
|
|
elf := SpawnBackground(eng, prompt)
|
|
|
|
m.mu.Lock()
|
|
m.elfs[elf.ID()] = elf
|
|
m.mu.Unlock()
|
|
|
|
m.logger.Info("elf spawned (direct)", "id", elf.ID(), "model", model)
|
|
return elf, nil
|
|
}
|
|
|
|
// Get returns an elf by ID.
|
|
func (m *Manager) Get(id string) (Elf, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
e, ok := m.elfs[id]
|
|
return e, ok
|
|
}
|
|
|
|
// List returns all tracked elfs.
|
|
func (m *Manager) List() []Elf {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
elfs := make([]Elf, 0, len(m.elfs))
|
|
for _, e := range m.elfs {
|
|
elfs = append(elfs, e)
|
|
}
|
|
return elfs
|
|
}
|
|
|
|
// Active returns elfs that are still running.
|
|
func (m *Manager) Active() []Elf {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
var active []Elf
|
|
for _, e := range m.elfs {
|
|
if e.Status() == StatusRunning {
|
|
active = append(active, e)
|
|
}
|
|
}
|
|
return active
|
|
}
|
|
|
|
// CancelAll cancels all running elfs.
|
|
func (m *Manager) CancelAll() {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
for _, e := range m.elfs {
|
|
if e.Status() == StatusRunning {
|
|
e.Cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
// WaitAll waits for all elfs to complete and returns their results.
|
|
func (m *Manager) WaitAll() []Result {
|
|
elfs := m.List()
|
|
results := make([]Result, len(elfs))
|
|
var wg sync.WaitGroup
|
|
|
|
for i, e := range elfs {
|
|
wg.Add(1)
|
|
go func(idx int, elf Elf) {
|
|
defer wg.Done()
|
|
results[idx] = elf.Wait()
|
|
}(i, e)
|
|
}
|
|
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
// Cleanup removes completed/failed/cancelled elfs from tracking.
|
|
func (m *Manager) Cleanup() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for id, e := range m.elfs {
|
|
s := e.Status()
|
|
if s == StatusCompleted || s == StatusFailed || s == StatusCancelled {
|
|
delete(m.elfs, id)
|
|
}
|
|
}
|
|
}
|