Files
gnoma/internal/session/local.go
vikingowl 706363f94b feat: rate limit pools, elf tree view, permission prompts, dep updates
Rate limits:
- Add PoolRPS/PoolTPM/PoolTokensMonth/PoolCostMonth pool kinds
- Provider defaults for Mistral/Anthropic/OpenAI/Google (tier-aware)
- Config override via [rate_limits.<provider>] TOML section
- Pools auto-attached to arms on registration

Elf tree view (CC-style):
- Structured elf.Progress type replaces flat string channel
- Tree with ├─/└─ branches, per-elf stats (tool uses, tokens)
- Live activity updates: tool calls, "generating… (N chars)"
- Completed elfs stay in tree with "Done (duration)" until turn ends
- Suppress raw elf output from chat (tree + LLM summary instead)
- Remove background elf mode (wait: false) — always wait
- Truncate elf results to 2000 chars for parent context
- Parallel hint in system prompt and tool description

Permission prompts:
- Show actual command in prompt: "bash wants to execute: find . -name '*.go'"
- Compact hint in separator bar: "⚠ bash: find . | wc -l [y/n]"
- PermReqMsg carries tool name + args

Other:
- Fix /model not updating status bar (session.Local.SetModel)
- Add make targets: run, check, install
- Update deps: BurntSushi/toml v1.6.0, chroma v2.23.1, x/text v0.35.0, cloud.google.com/go v0.123.0
2026-04-03 20:54:48 +02:00

144 lines
2.5 KiB
Go

package session
import (
"context"
"fmt"
"sync"
"somegit.dev/Owlibou/gnoma/internal/engine"
"somegit.dev/Owlibou/gnoma/internal/stream"
)
// Local implements Session using goroutines and channels within the same process.
type Local struct {
mu sync.Mutex
eng *engine.Engine
state SessionState
events chan stream.Event
// Current turn context
cancel context.CancelFunc
turn *engine.Turn
err error
// Stats
provider string
model string
turnCount int
}
// NewLocal creates a channel-based in-process session.
func NewLocal(eng *engine.Engine, providerName, model string) *Local {
return &Local{
eng: eng,
state: StateIdle,
provider: providerName,
model: model,
}
}
func (s *Local) Send(input string) error {
s.mu.Lock()
if s.state != StateIdle {
s.mu.Unlock()
return fmt.Errorf("session not idle (state: %s)", s.state)
}
s.state = StateStreaming
s.events = make(chan stream.Event, 64)
s.turn = nil
s.err = nil
ctx, cancel := context.WithCancel(context.Background())
s.cancel = cancel
s.turnCount++
s.mu.Unlock()
// Run engine in background goroutine
go func() {
cb := func(evt stream.Event) {
select {
case s.events <- evt:
case <-ctx.Done():
}
}
turn, err := s.eng.Submit(ctx, input, cb)
s.mu.Lock()
s.turn = turn
s.err = err
if err != nil && ctx.Err() != nil {
s.state = StateCancelled
} else if err != nil {
s.state = StateError
} else {
s.state = StateIdle
}
s.mu.Unlock()
close(s.events)
}()
return nil
}
func (s *Local) Events() <-chan stream.Event {
s.mu.Lock()
defer s.mu.Unlock()
return s.events
}
func (s *Local) TurnResult() (*engine.Turn, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.turn, s.err
}
func (s *Local) Cancel() {
s.mu.Lock()
defer s.mu.Unlock()
if s.cancel != nil {
s.cancel()
}
}
func (s *Local) Close() error {
s.Cancel()
s.mu.Lock()
defer s.mu.Unlock()
s.state = StateClosed
return nil
}
// SetModel updates the displayed model name.
func (s *Local) SetModel(model string) {
s.mu.Lock()
defer s.mu.Unlock()
s.model = model
}
func (s *Local) Status() Status {
s.mu.Lock()
defer s.mu.Unlock()
st := Status{
State: s.state,
Provider: s.provider,
Model: s.model,
TokensUsed: s.eng.Usage().TotalTokens(),
TurnCount: s.turnCount,
TokenState: "ok",
}
if w := s.eng.ContextWindow(); w != nil {
tr := w.Tracker()
st.TokensMax = tr.MaxTokens()
st.TokenPercent = tr.PercentUsed()
st.TokenState = tr.State().String()
}
return st
}