From 0a977431d6445551253c8677ec230beffdf215fe Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Sun, 31 May 2026 22:36:20 -0400 Subject: [PATCH] chore(server): report background prompt failures via events Add a prompt failure event that can be delivered after the HTTP request has already returned. Remote clients and subscribers now receive the same failure message through the event stream. Co-Authored-By: Charm Crush --- internal/agent/notify/notify.go | 6 ++++++ internal/server/events.go | 18 ++++++++++++------ internal/workspace/client_workspace.go | 16 ++++++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/internal/agent/notify/notify.go b/internal/agent/notify/notify.go index ac7f724c0f07f552d9759247821a2555c9e12524..1a217a6d00650fe1134b24d9d779821015513063 100644 --- a/internal/agent/notify/notify.go +++ b/internal/agent/notify/notify.go @@ -12,6 +12,9 @@ const ( // TypeReAuthenticate indicates the agent encountered an // authentication error and the user needs to re-authenticate. TypeReAuthenticate Type = "re_authenticate" + // TypeAgentError indicates the agent's turn terminated with an + // error. The error text is carried in Notification.Message. + TypeAgentError Type = "error" ) // Notification represents a domain event published by the agent. @@ -20,6 +23,9 @@ type Notification struct { SessionTitle string Type Type ProviderID string + // Message carries the error text for TypeAgentError. Other + // notification types ignore it. + Message string } // RunComplete is the authoritative end-of-run signal for a session. diff --git a/internal/server/events.go b/internal/server/events.go index 4e3d6a1a262ae7b3399f0fa765ae863160ba57c8..fd085c5a415c0ef0fc402673ad23fff8435f1db6 100644 --- a/internal/server/events.go +++ b/internal/server/events.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "errors" "fmt" "log/slog" @@ -85,13 +86,18 @@ func wrapEvent(ev any) *pubsub.Payload { Payload: fileToProto(e.Payload), }) case pubsub.Event[notify.Notification]: + payload := proto.AgentEvent{ + SessionID: e.Payload.SessionID, + SessionTitle: e.Payload.SessionTitle, + Type: proto.AgentEventType(e.Payload.Type), + } + if e.Payload.Type == notify.TypeAgentError { + payload.Type = proto.AgentEventTypeError + payload.Error = errors.New(e.Payload.Message) + } return envelope(pubsub.PayloadTypeAgentEvent, pubsub.Event[proto.AgentEvent]{ - Type: e.Type, - Payload: proto.AgentEvent{ - SessionID: e.Payload.SessionID, - SessionTitle: e.Payload.SessionTitle, - Type: proto.AgentEventType(e.Payload.Type), - }, + Type: e.Type, + Payload: payload, }) case pubsub.Event[notify.RunComplete]: return envelope(pubsub.PayloadTypeRunComplete, pubsub.Event[proto.RunComplete]{ diff --git a/internal/workspace/client_workspace.go b/internal/workspace/client_workspace.go index 2018fab8a7dcc2cb3aeb0f44fc0920c1db72d852..a6a43731675698083671cae95f983d7a3a724a5d 100644 --- a/internal/workspace/client_workspace.go +++ b/internal/workspace/client_workspace.go @@ -703,13 +703,17 @@ func (w *ClientWorkspace) translateEvent(ev any) tea.Msg { Payload: protoToFile(e.Payload), } case pubsub.Event[proto.AgentEvent]: + n := notify.Notification{ + SessionID: e.Payload.SessionID, + SessionTitle: e.Payload.SessionTitle, + Type: notify.Type(e.Payload.Type), + } + if e.Payload.Error != nil { + n.Message = e.Payload.Error.Error() + } return pubsub.Event[notify.Notification]{ - Type: e.Type, - Payload: notify.Notification{ - SessionID: e.Payload.SessionID, - SessionTitle: e.Payload.SessionTitle, - Type: notify.Type(e.Payload.Type), - }, + Type: e.Type, + Payload: n, } case pubsub.Event[proto.RunComplete]: // Translate the wire-level proto.RunComplete back into the