This introduces a small event layer for the Captain V2 conversation lifecycle. A new `Captain::ConversationEvents` facade dispatches five normalized events (`captain.conversation.engaged`, `captain.conversation.handed_off`, `captain.conversation.resolved`, `captain.response.completed`, `captain.response.failed`) from the points where Captain engages a conversation, replies, fails, hands off, or auto-resolves. Each event carries the conversation, assistant, timestamp, and a `source`/`reason_category` where relevant. ## Why this, why now The Captain V2 flow is about to gain several observers at once: conversation outcome tracking, agent session capture, and analytics all need to know when Captain engages, replies, fails, hands off, or resolves. Wiring each of them directly into `ResponseBuilderJob`, `HookExecutionService`, and the tools would tangle secondary bookkeeping into the paths that deliver customer-facing behavior, and every future consumer would deepen that. Landing the event layer first as its own PR means the flow announces these moments once and stays otherwise untouched: customer-visible behavior (messages, status changes, handoffs, usage enforcement) remains synchronous, while secondary effects subscribe through listeners. The upcoming conversation outcomes PR then reduces to a listener plus a model instead of another round of edits to the core flow, which is why this ships now, before that work merges. The existing inference reporting behavior is folded into this layer: the `conversation.captain_inference_*` events and their dispatch helpers on `Enterprise::Conversation` are removed, and a dedicated `Captain::ReportingEventListener` (registered on the enterprise async dispatcher) maps `source: 'inference'` events to the same stored reporting event names, so recorded analytics and the assistant stats builder are unaffected. ## What changed - New `Captain::ConversationEvents` facade and event type constants - Event emission from `HookExecutionService` (engagement, usage-limit handoff), `ResponseBuilderJob` (response completed/failed, generation-failure handoff), `HandoffTool` (tool handoff), and `InboxPendingConversationsResolutionJob` (inference resolved/handoff) - A dedicated `Captain::ReportingEventListener` preserves inference reporting events through the new event names, removing captain logic from the OSS listener
73 lines
2.0 KiB
Ruby
73 lines
2.0 KiB
Ruby
class Captain::ConversationEvents
|
|
module Sources
|
|
TOOL = 'tool'.freeze
|
|
GENERATION_FAILURE = 'generation_failure'.freeze
|
|
TIME_BASED = 'time_based'.freeze
|
|
INFERENCE = 'inference'.freeze
|
|
USAGE_LIMIT = 'usage_limit'.freeze
|
|
end
|
|
|
|
class << self
|
|
def engaged(conversation:, assistant:, at:)
|
|
dispatch(
|
|
Events::Types::CAPTAIN_CONVERSATION_ENGAGED,
|
|
at: at,
|
|
conversation: conversation,
|
|
assistant: assistant
|
|
)
|
|
end
|
|
|
|
def handed_off(conversation:, assistant:, source:, at:, reason_category: nil)
|
|
dispatch(
|
|
Events::Types::CAPTAIN_CONVERSATION_HANDED_OFF,
|
|
at: at,
|
|
conversation: conversation,
|
|
assistant: assistant,
|
|
source: source,
|
|
reason_category: reason_category
|
|
)
|
|
end
|
|
|
|
def resolved(conversation:, assistant:, source:, at:)
|
|
dispatch(
|
|
Events::Types::CAPTAIN_CONVERSATION_RESOLVED,
|
|
at: at,
|
|
conversation: conversation,
|
|
assistant: assistant,
|
|
source: source
|
|
)
|
|
end
|
|
|
|
def response_completed(conversation:, assistant:, message:, at:)
|
|
dispatch(
|
|
Events::Types::CAPTAIN_RESPONSE_COMPLETED,
|
|
at: at,
|
|
conversation: conversation,
|
|
assistant: assistant,
|
|
message: message
|
|
)
|
|
end
|
|
|
|
def response_failed(conversation:, assistant:, reason:, at:)
|
|
dispatch(
|
|
Events::Types::CAPTAIN_RESPONSE_FAILED,
|
|
at: at,
|
|
conversation: conversation,
|
|
assistant: assistant,
|
|
reason: reason
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
# Lifecycle events are secondary effects: a dispatch failure must never alter
|
|
# the customer-facing Captain flow (blocking a response from being scheduled,
|
|
# or turning an already-delivered reply into a spurious handoff).
|
|
def dispatch(event_name, at:, **data)
|
|
Rails.configuration.dispatcher.dispatch(event_name, at, data)
|
|
rescue StandardError => e
|
|
ChatwootExceptionTracker.new(e, account: data[:conversation]&.account).capture_exception
|
|
end
|
|
end
|
|
end
|