Create a type-level enforcement that all button/command actions must produce conversation context.
Make it impossible to add an action that bypasses the message flow.
-- An action that can be triggered by button or command
data Action = Action
{ actionId :: Text -- e.g. "start_task"
, actionExecute :: ActionInput -> IO ActionResult
}
-- What the action needs
data ActionInput = ActionInput
{ aiUserId :: UserId
, aiChatId :: ChatId
, aiPayload :: Value -- action-specific data
}
-- What the action MUST return (enforced by type)
data ActionResult = ActionResult
{ arUserMessage :: Text -- What to log as 'user said'
, arAssistantMessage :: Text -- What to log as 'ava said'
, arOutcome :: ActionOutcome -- Success/Failure/Pending
}
data ActionOutcome
= ActionSuccess
| ActionFailed Text
| ActionPending Text -- for async operations
-- All actions must be registered here
actionRegistry :: Map Text Action
actionRegistry = Map.fromList
[ ("start_task", startTaskAction)
, ("stop_task", stopTaskAction)
, ("show_ready", showReadyAction)
]
-- Dispatch function - ONLY way to execute actions
executeAction :: Text -> ActionInput -> IO (Maybe ActionResult)
executeAction actionId input = case Map.lookup actionId actionRegistry of
Nothing -> pure Nothing
Just action -> Just <$> actionExecute action input
handleCallback :: CallbackQuery -> IO ()
handleCallback cq = do
let (actionId, payload) = parseCallbackData (cqData cq)
result <- executeAction actionId (ActionInput userId chatId payload)
case result of
Nothing -> answerCallback "Unknown action"
Just ar -> do
-- MANDATORY: Log to conversation history
Memory.appendConversation chatId
[ (User, arUserMessage ar)
, (Assistant, arAssistantMessage ar)
]
-- Respond to user
sendMessage chatId (arAssistantMessage ar)
answerCallback "Done"
1. Type-enforced: Can't return () from an action, must return ActionResult with messages 2. Single dispatch point: All actions go through executeAction 3. Consistent logging: handleCallback always logs both messages 4. Easy to audit: All actions in one registry 5. Testable: Can unit test actions return proper messages
data Action (a :: ActionKind) where
StartTask :: TaskId -> Action 'Logged
StopTask :: TaskId -> Action 'Logged
-- Can't create Action 'Unlogged
data ActionKind = Logged -- no Unlogged constructor!