// 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 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")} } target := roomID if a.Reply.ThreadID != "" { target = a.Reply.ThreadID } err := r.matrix.SendText(ctx, target, 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)} } }