feat(events): внедрить Publisher в Core/Worker/Analyst + шины в app
- Core: setStatus публикует TaskStatusChanged на каждом переходе (handleStart/Cancel/Retry/Consent/Edit/Turn, runDecide). - Worker: setStatus публикует TaskStatusChanged (running/timeout/failed/ success, failTask), notify остаётся авто-уведомлением. - Analyst: Decide публикует AgentActivity (stage=decide). - app: создаёт доменную (256) и логовую (1024) шины, подключает их к Core/Worker/Analyst, редиректит log в обе (stderr + логовая шина).
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kamelion/ratatoskr-go/internal/events"
|
||||
"github.com/kamelion/ratatoskr-go/internal/opencode"
|
||||
"github.com/kamelion/ratatoskr-go/internal/storage"
|
||||
)
|
||||
@@ -56,6 +57,27 @@ type Worker struct {
|
||||
|
||||
// подменяемый poll для тестов
|
||||
pollFn PollTaskFunc
|
||||
|
||||
// Events — издатель доменных событий для UI. nil — события выключены.
|
||||
Events events.Publisher
|
||||
}
|
||||
|
||||
// publish отправляет доменное событие, если задан издатель.
|
||||
func (w *Worker) publish(e events.Event) {
|
||||
if w.Events != nil {
|
||||
w.Events.Publish(e)
|
||||
}
|
||||
}
|
||||
|
||||
// setStatus переводит задачу в новый статус: сохраняет в БД и публикует событие.
|
||||
func (w *Worker) setStatus(ctx context.Context, task *storage.Task, to storage.Status) error {
|
||||
from := task.Status
|
||||
task.Status = to
|
||||
if err := w.Store.UpdateTask(ctx, task); err != nil {
|
||||
return err
|
||||
}
|
||||
w.publish(events.TaskStatusChanged{ID: task.ID, From: from, To: to})
|
||||
return nil
|
||||
}
|
||||
|
||||
// runCtx оборачивает контекст запуска субагента, привязывая живое наблюдение
|
||||
@@ -192,8 +214,7 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
|
||||
}
|
||||
|
||||
// 2. ставим running (после валидации — чтобы плохие имена не жгли состояние)
|
||||
task.Status = storage.StatusRunning
|
||||
if err := w.Store.UpdateTask(ctx, task); err != nil {
|
||||
if err := w.setStatus(ctx, task, storage.StatusRunning); err != nil {
|
||||
return fmt.Errorf("%w: set running: %v", ErrUpdate, err)
|
||||
}
|
||||
w.notifyStatus(ctx, task, storage.StatusRunning)
|
||||
@@ -261,16 +282,14 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
|
||||
case 0:
|
||||
// продолжаем на ревью
|
||||
case -1:
|
||||
task.Status = storage.StatusTimeout
|
||||
if e := w.Store.UpdateTask(ctx, task); e != nil {
|
||||
if e := w.setStatus(ctx, task, storage.StatusTimeout); e != nil {
|
||||
return fmt.Errorf("%w: set %s: %v", ErrUpdate, task.Status, e)
|
||||
}
|
||||
w.notifyStatus(ctx, task, storage.StatusTimeout)
|
||||
w.finalizeTrace(ctx, traceID, storage.TraceTimeout, output)
|
||||
return nil
|
||||
default:
|
||||
task.Status = storage.StatusFailed
|
||||
if e := w.Store.UpdateTask(ctx, task); e != nil {
|
||||
if e := w.setStatus(ctx, task, storage.StatusFailed); e != nil {
|
||||
return fmt.Errorf("%w: set %s: %v", ErrUpdate, task.Status, e)
|
||||
}
|
||||
w.notifyStatus(ctx, task, storage.StatusFailed)
|
||||
@@ -309,9 +328,8 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
|
||||
|
||||
if verdict == nil {
|
||||
// невалидный JSON даже после retry → failed с объяснением.
|
||||
task.Status = storage.StatusFailed
|
||||
explain := "reviewer вернул невалидный/пустой вердикт (даже после повтора)."
|
||||
if e := w.Store.UpdateTask(ctx, task); e != nil {
|
||||
if e := w.setStatus(ctx, task, storage.StatusFailed); e != nil {
|
||||
return fmt.Errorf("%w: set %s: %v", ErrUpdate, task.Status, e)
|
||||
}
|
||||
w.notifyStatus(ctx, task, storage.StatusFailed)
|
||||
@@ -325,13 +343,12 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
|
||||
w.failTask(ctx, task)
|
||||
return pErr
|
||||
}
|
||||
task.Status = storage.StatusSuccess
|
||||
if e := w.Store.UpdateTask(ctx, task); e != nil {
|
||||
return fmt.Errorf("%w: set %s: %v", ErrUpdate, task.Status, e)
|
||||
}
|
||||
w.notifyStatus(ctx, task, storage.StatusSuccess)
|
||||
return nil
|
||||
if e := w.setStatus(ctx, task, storage.StatusSuccess); e != nil {
|
||||
return fmt.Errorf("%w: set %s: %v", ErrUpdate, task.Status, e)
|
||||
}
|
||||
w.notifyStatus(ctx, task, storage.StatusSuccess)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Не пройдено: если есть итерации — dev дорабатывает.
|
||||
if iter+1 < maxReviewIterations {
|
||||
@@ -341,8 +358,7 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
|
||||
}
|
||||
|
||||
// Лимит исчерпан → failed с объяснением.
|
||||
task.Status = storage.StatusFailed
|
||||
if e := w.Store.UpdateTask(ctx, task); e != nil {
|
||||
if e := w.setStatus(ctx, task, storage.StatusFailed); e != nil {
|
||||
return fmt.Errorf("%w: set %s: %v", ErrUpdate, task.Status, e)
|
||||
}
|
||||
w.notify(ctx, task, fmt.Sprintf("Задача #%d: failed — ревью не пройдено за %d итераций", task.ID, maxReviewIterations))
|
||||
@@ -375,8 +391,7 @@ func (w *Worker) reviewWithRetry(ctx context.Context, taskID int64, cwd, prompt
|
||||
|
||||
// failTask помечает задачу failed и уведомляет владельца.
|
||||
func (w *Worker) failTask(ctx context.Context, task *storage.Task) {
|
||||
task.Status = storage.StatusFailed
|
||||
if e := w.Store.UpdateTask(ctx, task); e != nil {
|
||||
if e := w.setStatus(ctx, task, storage.StatusFailed); e != nil {
|
||||
log.Printf("worker: task %d: set failed: %v", task.ID, e)
|
||||
}
|
||||
w.notifyStatus(ctx, task, storage.StatusFailed)
|
||||
|
||||
Reference in New Issue
Block a user