feat: наблюдение за живой opencode-сессией (/status N показывает что делает агент)
LiveRegistry собирает live-шаги из stdout процесса opencode по taskID (через контекст, без изменения интерфейса Runner). /status N для running-задачи прикладывает живое состояние: активный субагент, давность последнего шага, последние text-шаги агента. Ничего не перезапускается и не убивается — только наблюдение по запросу.
This commit is contained in:
@@ -103,6 +103,9 @@ func New(configPath, version, updateToken string) (*App, error) {
|
||||
// Core — ядро машины состояний
|
||||
coreCtx := core.New(store, analystCtx)
|
||||
|
||||
// Live-журнал живых сессий: воркер пишет шаги агента, /status N их читает.
|
||||
live := opencode.NewLiveRegistry()
|
||||
|
||||
// Router — единый диспетчер входящих из всех каналов
|
||||
a := &App{
|
||||
Config: cfg,
|
||||
@@ -129,10 +132,14 @@ func New(configPath, version, updateToken string) (*App, error) {
|
||||
MaxJobs: 2,
|
||||
GitBaseURL: cfg.Git.BaseURL,
|
||||
GitToken: cfg.Git.Token,
|
||||
Live: live,
|
||||
}
|
||||
a.Router = router
|
||||
a.Worker = w
|
||||
|
||||
// /status N читает живое состояние сессии через liveProbe-адаптер.
|
||||
coreCtx.Live = &liveProbe{reg: live}
|
||||
|
||||
// Auto-обновление: .new/.old рядом с бинарником (Dir пуст → binDir() от os.Executable).
|
||||
// Токен: вшитый updateToken приоритетнее update.token из конфига.
|
||||
udToken := updateToken
|
||||
|
||||
46
internal/app/live.go
Normal file
46
internal/app/live.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kamelion/ratatoskr-go/internal/opencode"
|
||||
)
|
||||
|
||||
// liveProbe адаптирует *opencode.LiveRegistry под интерфейс core.LiveProber.
|
||||
// Формирует короткое человекочитаемое описание живой сессии задачи.
|
||||
type liveProbe struct {
|
||||
reg *opencode.LiveRegistry
|
||||
}
|
||||
|
||||
func (p *liveProbe) Probe(taskID int64) (string, bool) {
|
||||
if p == nil || p.reg == nil {
|
||||
return "", false
|
||||
}
|
||||
snap, ok := p.reg.Snap(taskID)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("Агент **%s** активен", snap.Agent))
|
||||
if idl := time.Since(snap.Last).Round(time.Second); idl > 0 {
|
||||
b.WriteString(fmt.Sprintf(" (последний шаг %s назад)", idl))
|
||||
}
|
||||
// последние text-шаги — что агент делает прямо сейчас
|
||||
n := 0
|
||||
for i := len(snap.Steps) - 1; i >= 0 && n < 3; i-- {
|
||||
st := snap.Steps[i]
|
||||
if st.Type != "text" || strings.TrimSpace(st.Text) == "" {
|
||||
continue
|
||||
}
|
||||
text := strings.TrimSpace(st.Text)
|
||||
if len(text) > 140 {
|
||||
text = text[:140] + "…"
|
||||
}
|
||||
text = strings.ReplaceAll(text, "\n", " ")
|
||||
b.WriteString("\n▸ " + text)
|
||||
n++
|
||||
}
|
||||
return b.String(), true
|
||||
}
|
||||
39
internal/app/live_test.go
Normal file
39
internal/app/live_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kamelion/ratatoskr-go/internal/opencode"
|
||||
)
|
||||
|
||||
func TestLiveProbe_Fmt(t *testing.T) {
|
||||
reg := opencode.NewLiveRegistry()
|
||||
p := &liveProbe{reg: reg}
|
||||
|
||||
// Нет сессии — Probe должен вернуть ok=false.
|
||||
if _, ok := p.Probe(1); ok {
|
||||
t.Error("Probe без сессии вернул ok=true")
|
||||
}
|
||||
|
||||
reg.Start(7, "dev")
|
||||
reg.Observe(7, opencode.LiveStep{Type: "text", Text: "анализирую код", At: time.Now()})
|
||||
reg.Observe(7, opencode.LiveStep{Type: "tool", At: time.Now()})
|
||||
reg.Observe(7, opencode.LiveStep{Type: "text", Text: "пишу фичу по задаче номер триста семь", At: time.Now()})
|
||||
|
||||
desc, ok := p.Probe(7)
|
||||
if !ok {
|
||||
t.Fatal("Probe с сессией вернул ok=false")
|
||||
}
|
||||
if !strings.Contains(desc, "dev") {
|
||||
t.Errorf("описание не содержит агента: %q", desc)
|
||||
}
|
||||
if !strings.Contains(desc, "пишу фичу") {
|
||||
t.Errorf("описание не содержит последний text-шаг: %q", desc)
|
||||
}
|
||||
// text-шаги: оба, tools пропущен
|
||||
if n := strings.Count(desc, "▸"); n != 2 {
|
||||
t.Errorf("ожидал 2 text-шага (▸), вижу %d: %q", n, desc)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user