feat: respuestas como reply de Matrix + presencia online/offline
Añade soporte para que las respuestas de los bots sean replies nativos de Matrix (m.in_reply_to) en lugar de mensajes sueltos. Los clientes Matrix mostrarán el mensaje original citado. Cambios: - EventID en MessageContext para capturar el ID del evento entrante - InReplyTo en ReplyAction para indicar a qué evento responder - SendReplyMarkdown en el cliente Matrix (shell/matrix/client.go) - Runner usa SendReplyMarkdown cuando InReplyTo está presente - runtime.go pasa InReplyTo en todas las respuestas LLM y comandos - SetPresence online al arrancar, offline al apagar (graceful) No se tocan: herramientas, TUI, configuración de agentes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+18
-6
@@ -285,6 +285,18 @@ func (a *Agent) Run(ctx context.Context) error {
|
|||||||
"tools", a.toolReg.Names(),
|
"tools", a.toolReg.Names(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set presence to online
|
||||||
|
if err := a.matrix.SetPresence(ctx, event.PresenceOnline); err != nil {
|
||||||
|
a.logger.Warn("failed to set presence online", "err", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// Use background context since ctx is already cancelled at shutdown
|
||||||
|
offlineCtx := context.Background()
|
||||||
|
if err := a.matrix.SetPresence(offlineCtx, event.PresenceOffline); err != nil {
|
||||||
|
a.logger.Warn("failed to set presence offline", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Start bus listener if connected to the orchestration bus
|
// Start bus listener if connected to the orchestration bus
|
||||||
if a.agentBus != nil {
|
if a.agentBus != nil {
|
||||||
ch := a.agentBus.Subscribe(bus.AgentID(a.cfg.Agent.ID))
|
ch := a.agentBus.Subscribe(bus.AgentID(a.cfg.Agent.ID))
|
||||||
@@ -453,14 +465,14 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
|
|||||||
if handler, ok := a.commands[cmdName]; ok {
|
if handler, ok := a.commands[cmdName]; ok {
|
||||||
a.logger.Info("command_executed", "command", cmdName)
|
a.logger.Info("command_executed", "command", cmdName)
|
||||||
reply := handler(ctx, msgCtx)
|
reply := handler(ctx, msgCtx)
|
||||||
_ = a.matrix.SendMarkdown(ctx, roomID, reply)
|
_ = a.matrix.SendReplyMarkdown(ctx, roomID, msgCtx.EventID, reply)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown command — never falls through to rules or LLM
|
// Unknown command — never falls through to rules or LLM
|
||||||
a.logger.Info("command_unknown", "command", msgCtx.Command)
|
a.logger.Info("command_unknown", "command", msgCtx.Command)
|
||||||
_ = a.matrix.SendMarkdown(ctx, roomID,
|
_ = a.matrix.SendReplyMarkdown(ctx, roomID, msgCtx.EventID,
|
||||||
fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command))
|
fmt.Sprintf("Comando desconocido: `!%s`. Usa `!help` para ver comandos disponibles.", msgCtx.Command))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +514,7 @@ func (a *Agent) executeActions(ctx context.Context, roomID string, msgCtx decisi
|
|||||||
a.logger.Warn("LLM action requested but no LLM configured")
|
a.logger.Warn("LLM action requested but no LLM configured")
|
||||||
expanded = append(expanded, decision.Action{
|
expanded = append(expanded, decision.Action{
|
||||||
Kind: decision.ActionKindReply,
|
Kind: decision.ActionKindReply,
|
||||||
Reply: &decision.ReplyAction{Content: "Este bot no tiene LLM configurado."},
|
Reply: &decision.ReplyAction{Content: "Este bot no tiene LLM configurado.", InReplyTo: msgCtx.EventID},
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -518,12 +530,12 @@ func (a *Agent) executeActions(ctx context.Context, roomID string, msgCtx decisi
|
|||||||
a.logger.Error("llm error", "err", err)
|
a.logger.Error("llm error", "err", err)
|
||||||
expanded = append(expanded, decision.Action{
|
expanded = append(expanded, decision.Action{
|
||||||
Kind: decision.ActionKindReply,
|
Kind: decision.ActionKindReply,
|
||||||
Reply: &decision.ReplyAction{Content: "Sorry, I encountered an error."},
|
Reply: &decision.ReplyAction{Content: "Sorry, I encountered an error.", InReplyTo: msgCtx.EventID},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
expanded = append(expanded, decision.Action{
|
expanded = append(expanded, decision.Action{
|
||||||
Kind: decision.ActionKindReply,
|
Kind: decision.ActionKindReply,
|
||||||
Reply: &decision.ReplyAction{Content: reply},
|
Reply: &decision.ReplyAction{Content: reply, InReplyTo: msgCtx.EventID},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Memory: append assistant reply after LLM call
|
// Memory: append assistant reply after LLM call
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type MessageContext struct {
|
|||||||
SenderID string
|
SenderID string
|
||||||
SenderName string
|
SenderName string
|
||||||
RoomID string
|
RoomID string
|
||||||
|
EventID string // Matrix event ID of the incoming message
|
||||||
Content string
|
Content string
|
||||||
Command string // parsed command name, e.g. "deploy"
|
Command string // parsed command name, e.g. "deploy"
|
||||||
Args []string // parsed arguments
|
Args []string // parsed arguments
|
||||||
@@ -47,9 +48,10 @@ type Action struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ReplyAction struct {
|
type ReplyAction struct {
|
||||||
Content string
|
Content string
|
||||||
ThreadID string // empty = new thread
|
ThreadID string // empty = new thread
|
||||||
Reaction string // optional Matrix reaction
|
InReplyTo string // Matrix event ID to reply to (m.in_reply_to)
|
||||||
|
Reaction string // optional Matrix reaction
|
||||||
}
|
}
|
||||||
|
|
||||||
type LLMAction struct {
|
type LLMAction struct {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type Result struct {
|
|||||||
type MatrixSender interface {
|
type MatrixSender interface {
|
||||||
SendText(ctx context.Context, roomID, text string) error
|
SendText(ctx context.Context, roomID, text string) error
|
||||||
SendMarkdown(ctx context.Context, roomID, markdown string) error
|
SendMarkdown(ctx context.Context, roomID, markdown string) error
|
||||||
|
SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error
|
||||||
SendTyping(ctx context.Context, roomID string, typing bool) error
|
SendTyping(ctx context.Context, roomID string, typing bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +67,12 @@ func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Actio
|
|||||||
if a.Reply.ThreadID != "" {
|
if a.Reply.ThreadID != "" {
|
||||||
target = a.Reply.ThreadID
|
target = a.Reply.ThreadID
|
||||||
}
|
}
|
||||||
err := r.matrix.SendMarkdown(ctx, target, a.Reply.Content)
|
var err error
|
||||||
|
if a.Reply.InReplyTo != "" {
|
||||||
|
err = r.matrix.SendReplyMarkdown(ctx, target, a.Reply.InReplyTo, a.Reply.Content)
|
||||||
|
} else {
|
||||||
|
err = r.matrix.SendMarkdown(ctx, target, a.Reply.Content)
|
||||||
|
}
|
||||||
return Result{Action: a, Output: a.Reply.Content, Err: err}
|
return Result{Action: a, Output: a.Reply.Content, Err: err}
|
||||||
|
|
||||||
case decision.ActionKindSSH:
|
case decision.ActionKindSSH:
|
||||||
|
|||||||
@@ -305,6 +305,21 @@ func mdToHTML(md string) string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendReplyMarkdown sends a formatted message as a reply to a specific event.
|
||||||
|
// It sets m.in_reply_to so Matrix clients show the original message as a quote.
|
||||||
|
func (c *Client) SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error {
|
||||||
|
html := mdToHTML(markdown)
|
||||||
|
content := event.MessageEventContent{
|
||||||
|
MsgType: event.MsgText,
|
||||||
|
Body: markdown,
|
||||||
|
Format: event.FormatHTML,
|
||||||
|
FormattedBody: html,
|
||||||
|
RelatesTo: (&event.RelatesTo{}).SetReplyTo(id.EventID(inReplyTo)),
|
||||||
|
}
|
||||||
|
_, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// SendReaction sends a reaction to an event.
|
// SendReaction sends a reaction to an event.
|
||||||
func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error {
|
func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error {
|
||||||
_, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction)
|
_, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction)
|
||||||
@@ -448,6 +463,11 @@ func truncateKey(key string) string {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPresence sets the bot's presence status (online, unavailable, offline).
|
||||||
|
func (c *Client) SetPresence(ctx context.Context, status event.Presence) error {
|
||||||
|
return c.raw.SetPresence(ctx, status)
|
||||||
|
}
|
||||||
|
|
||||||
// Raw returns the underlying mautrix.Client for advanced use.
|
// Raw returns the underlying mautrix.Client for advanced use.
|
||||||
func (c *Client) Raw() *mautrix.Client {
|
func (c *Client) Raw() *mautrix.Client {
|
||||||
return c.raw
|
return c.raw
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ func (l *Listener) Run(ctx context.Context) error {
|
|||||||
isDM,
|
isDM,
|
||||||
opts,
|
opts,
|
||||||
)
|
)
|
||||||
|
msgCtx.EventID = evt.ID.String()
|
||||||
|
|
||||||
l.logger.Debug("message parsed",
|
l.logger.Debug("message parsed",
|
||||||
"sender", msgCtx.SenderID,
|
"sender", msgCtx.SenderID,
|
||||||
|
|||||||
Reference in New Issue
Block a user