feat: renderizar Markdown a HTML en mensajes Matrix con goldmark

Se reemplaza SendText por SendMarkdown en todos los puntos donde el agente
envía respuestas: runtime.go (comandos built-in y tareas orquestadas),
effects/runner.go (acciones Reply) y tools/matrix.go (matrix_send tool).

shell/matrix/client.go ahora usa goldmark para convertir Markdown a HTML real
en el campo FormattedBody del evento Matrix, cumpliendo con la spec de Matrix
para mensajes formateados. El Body conserva el markdown raw como fallback.

Se añade dependencia github.com/yuin/goldmark v1.7.16.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 02:21:06 +00:00
parent e2f14e7abb
commit 828eb175fe
6 changed files with 25 additions and 6 deletions
+3 -3
View File
@@ -391,7 +391,7 @@ func (a *Agent) handleTaskEvent(ctx context.Context, msg bus.AgentMessage) {
}
// Send reply to Matrix room
if sendErr := a.matrix.SendText(ctx, roomID, reply); sendErr != nil {
if sendErr := a.matrix.SendMarkdown(ctx, roomID, reply); sendErr != nil {
a.logger.Error("failed to send orchestrated reply to Matrix", "err", sendErr)
}
@@ -453,13 +453,13 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
if handler, ok := a.commands[cmdName]; ok {
a.logger.Info("command_executed", "command", cmdName)
reply := handler(ctx, msgCtx)
_ = a.matrix.SendText(ctx, roomID, reply)
_ = a.matrix.SendMarkdown(ctx, roomID, reply)
return
}
// Unknown command — never falls through to rules or LLM
a.logger.Info("command_unknown", "command", msgCtx.Command)
_ = a.matrix.SendText(ctx, roomID,
_ = a.matrix.SendMarkdown(ctx, roomID,
fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command))
return
}
+1
View File
@@ -51,6 +51,7 @@ require (
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
go.mau.fi/util v0.8.1 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.30.0 // indirect
+2
View File
@@ -110,6 +110,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.8.1 h1:Ga43cz6esQBYqcjZ/onRoVnYWoUwjWbsxVeJg2jOTSo=
go.mau.fi/util v0.8.1/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+2 -1
View File
@@ -22,6 +22,7 @@ type Result struct {
// 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
SendTyping(ctx context.Context, roomID string, typing bool) error
}
@@ -65,7 +66,7 @@ func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Actio
if a.Reply.ThreadID != "" {
target = a.Reply.ThreadID
}
err := r.matrix.SendText(ctx, target, a.Reply.Content)
err := r.matrix.SendMarkdown(ctx, target, a.Reply.Content)
return Result{Action: a, Output: a.Reply.Content, Err: err}
case decision.ActionKindSSH:
+15 -1
View File
@@ -3,6 +3,7 @@ package matrix
import (
"context"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
@@ -12,6 +13,7 @@ import (
"path/filepath"
"strings"
"github.com/yuin/goldmark"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/cryptohelper"
@@ -269,17 +271,29 @@ func (c *Client) SendText(ctx context.Context, roomID, text string) error {
}
// SendMarkdown sends a formatted (Markdown) message to a room.
// Body contains the raw markdown (plaintext fallback per Matrix spec).
// FormattedBody contains the HTML rendered by goldmark.
func (c *Client) SendMarkdown(ctx context.Context, roomID, markdown string) error {
html := mdToHTML(markdown)
content := event.MessageEventContent{
MsgType: event.MsgText,
Body: markdown,
Format: event.FormatHTML,
FormattedBody: markdown, // mautrix can render markdown -> HTML if needed
FormattedBody: html,
}
_, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
return err
}
// mdToHTML converts a Markdown string to HTML using goldmark.
func mdToHTML(md string) string {
var buf bytes.Buffer
if err := goldmark.Convert([]byte(md), &buf); err != nil {
return md // fallback to raw markdown on error
}
return buf.String()
}
// SendReaction sends a reaction to an event.
func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error {
_, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction)
+2 -1
View File
@@ -9,6 +9,7 @@ import (
// Satisfied by shell/matrix.Client.
type MatrixSender interface {
SendText(ctx context.Context, roomID, text string) error
SendMarkdown(ctx context.Context, roomID, markdown string) error
}
// NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room.
@@ -29,7 +30,7 @@ func NewMatrixSend(sender MatrixSender) Tool {
return Result{Err: fmt.Errorf("matrix_send: room_id and message are required")}
}
if err := sender.SendText(ctx, roomID, message); err != nil {
if err := sender.SendMarkdown(ctx, roomID, message); err != nil {
return Result{Err: fmt.Errorf("matrix_send: %w", err)}
}