Files
gnoma/internal/elf/elf.go
T
vikingowl 07c739795c feat: M7 Elfs — sub-agents with router-integrated spawning
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.
2026-04-03 19:16:46 +02:00

154 lines
3.3 KiB
Go

package elf
import (
"context"
"fmt"
"sync/atomic"
"time"
"somegit.dev/Owlibou/gnoma/internal/engine"
"somegit.dev/Owlibou/gnoma/internal/message"
"somegit.dev/Owlibou/gnoma/internal/stream"
)
// Status tracks the lifecycle of an elf.
type Status int
const (
StatusPending Status = iota
StatusRunning
StatusCompleted
StatusFailed
StatusCancelled
)
func (s Status) String() string {
switch s {
case StatusPending:
return "pending"
case StatusRunning:
return "running"
case StatusCompleted:
return "completed"
case StatusFailed:
return "failed"
case StatusCancelled:
return "cancelled"
default:
return "unknown"
}
}
// Result is the output of a completed elf.
type Result struct {
ID string
Status Status
Messages []message.Message
Usage message.Usage
Output string // final text output
Error error
Duration time.Duration
}
// Elf is a sub-agent with its own engine and conversation history.
type Elf interface {
// ID returns the unique elf identifier.
ID() string
// Status returns the current lifecycle status.
Status() Status
// Events returns a channel for streaming events (nil for sync elfs).
Events() <-chan stream.Event
// Wait blocks until the elf completes and returns its result.
Wait() Result
// Cancel aborts the elf.
Cancel()
}
var elfCounter atomic.Int64
func nextID(prefix string) string {
n := elfCounter.Add(1)
return fmt.Sprintf("%s-%d", prefix, n)
}
// BackgroundElf runs on its own goroutine with an independent engine.
type BackgroundElf struct {
id string
eng *engine.Engine
events chan stream.Event
result chan Result
cancel context.CancelFunc
status atomic.Int32
startAt time.Time
}
// SpawnBackground creates and starts a background elf.
func SpawnBackground(eng *engine.Engine, prompt string) *BackgroundElf {
ctx, cancel := context.WithCancel(context.Background())
elf := &BackgroundElf{
id: nextID("elf"),
eng: eng,
events: make(chan stream.Event, 64),
result: make(chan Result, 1),
cancel: cancel,
startAt: time.Now(),
}
elf.status.Store(int32(StatusRunning))
go elf.run(ctx, prompt)
return elf
}
func (e *BackgroundElf) run(ctx context.Context, prompt string) {
cb := func(evt stream.Event) {
select {
case e.events <- evt:
case <-ctx.Done():
}
}
turn, err := e.eng.Submit(ctx, prompt, cb)
close(e.events)
r := Result{
ID: e.id,
Duration: time.Since(e.startAt),
}
if ctx.Err() != nil {
r.Status = StatusCancelled
r.Error = ctx.Err()
e.status.Store(int32(StatusCancelled))
} else if err != nil {
r.Status = StatusFailed
r.Error = err
e.status.Store(int32(StatusFailed))
} else {
r.Status = StatusCompleted
r.Messages = turn.Messages
r.Usage = turn.Usage
// Extract final text from last assistant message
for i := len(turn.Messages) - 1; i >= 0; i-- {
if turn.Messages[i].Role == message.RoleAssistant {
r.Output = turn.Messages[i].TextContent()
break
}
}
e.status.Store(int32(StatusCompleted))
}
e.result <- r
}
func (e *BackgroundElf) ID() string { return e.id }
func (e *BackgroundElf) Status() Status { return Status(e.status.Load()) }
func (e *BackgroundElf) Events() <-chan stream.Event { return e.events }
func (e *BackgroundElf) Cancel() { e.cancel() }
func (e *BackgroundElf) Wait() Result {
return <-e.result
}