Files
agents_and_robots/shell/effects/runner.go
T
egutierrez 45bd258be1 feat: mensajes progresivos en Matrix con ProgressReporter
Implementa la Fase 2 del issue 0036: mensajes de progreso en tiempo real
que muestran al usuario que herramientas esta usando el agente claude-code.

- SendMarkdownGetID en shell/matrix/client.go: envia mensaje y retorna
  el event ID para editarlo despues
- EditMessage en shell/matrix/client.go: edita un mensaje existente
  usando m.replace (m.relates_to con rel_type=m.replace)
- ProgressReporter en shell/effects/progress.go (NEW): recibe streaming
  events y actualiza un mensaje unico en Matrix mostrando el progreso
  (e.g. "🔧 Bash: ls -la" → "🔧 Read: file.go" → " Completado")
- Rate limiter integrado: max 1 edit/segundo para no saturar el homeserver
- Conectado en devagents/handler.go: cuando provider=claude-code y
  streaming+show_tool_progress habilitados, crea ProgressReporter y
  pasa StreamFunc al CompletionRequest
- MatrixSender interface actualizada con los nuevos metodos
- 10 tests nuevos para ProgressReporter, todos los existentes pasan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:58:03 +00:00

96 lines
3.1 KiB
Go

// Package effects interprets pure []decision.Action values into real side effects.
package effects
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/enmanuel/agents/pkg/decision"
"github.com/enmanuel/agents/shell/logger"
"github.com/enmanuel/agents/shell/ssh"
)
// Result holds the outcome of executing a single action.
type Result struct {
Action decision.Action
Output string
Err error
}
// MatrixSender is satisfied by shell/matrix.Client.
type MatrixSender interface {
SendText(ctx context.Context, roomID, text string) error
SendMarkdown(ctx context.Context, roomID, markdown string) error
SendMarkdownGetID(ctx context.Context, roomID, markdown string) (string, error)
EditMessage(ctx context.Context, roomID, originalEventID, markdown string) error
SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error
SendThreadMarkdown(ctx context.Context, roomID, threadRootID, inReplyTo, markdown string) error
SendTyping(ctx context.Context, roomID string, typing bool) error
}
// Runner interprets actions and executes them.
type Runner struct {
matrix MatrixSender
ssh *ssh.Executor
logger *slog.Logger
}
// NewRunner creates a Runner with the provided dependencies.
func NewRunner(matrix MatrixSender, ssh *ssh.Executor, logger *slog.Logger) *Runner {
return &Runner{matrix: matrix, ssh: ssh, logger: logger}
}
// Execute runs each action sequentially and returns results.
func (r *Runner) Execute(ctx context.Context, roomID string, actions []decision.Action) []Result {
r.logger.Debug("effects_batch", "room", roomID, "count", len(actions))
results := make([]Result, 0, len(actions))
for _, a := range actions {
start := time.Now()
res := r.executeOne(ctx, roomID, a)
ms := time.Since(start).Milliseconds()
results = append(results, res)
if res.Err != nil {
r.logger.Error("action_failed", logger.FieldAction, a.Kind, logger.FieldDurationMS, ms, "err", res.Err)
} else {
r.logger.Info("action_done", logger.FieldAction, a.Kind, logger.FieldDurationMS, ms)
}
}
return results
}
func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Action) Result {
switch a.Kind {
case decision.ActionKindReply:
if a.Reply == nil {
return Result{Action: a, Err: fmt.Errorf("nil reply action")}
}
var err error
switch {
case a.Reply.ThreadID != "":
// Thread reply: send as part of the thread with fallback in_reply_to
err = r.matrix.SendThreadMarkdown(ctx, roomID, a.Reply.ThreadID, a.Reply.InReplyTo, a.Reply.Content)
case a.Reply.InReplyTo != "":
err = r.matrix.SendReplyMarkdown(ctx, roomID, a.Reply.InReplyTo, a.Reply.Content)
default:
err = r.matrix.SendMarkdown(ctx, roomID, a.Reply.Content)
}
return Result{Action: a, Output: a.Reply.Content, Err: err}
case decision.ActionKindSSH:
if a.SSH == nil {
return Result{Action: a, Err: fmt.Errorf("nil ssh action")}
}
res := r.ssh.Execute(ctx, *a.SSH)
output := res.Stdout
if res.Stderr != "" {
output += "\nstderr: " + res.Stderr
}
return Result{Action: a, Output: output, Err: res.Err}
default:
return Result{Action: a, Err: fmt.Errorf("unhandled action kind: %s", a.Kind)}
}
}