Design type-safe action system with mandatory context logging

t-280.2.7·WorkTask·
·
·
·Omni/Agent/Telegram.hs
Parent:t-280.2·Created1 month ago·Updated1 month ago

Dependencies

Description

Edit

Create a type-level enforcement that all button/command actions must produce conversation context.

Goal

Make it impossible to add an action that bypasses the message flow.

Proposed Design

Core Types

-- 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

Registry

-- 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

Callback Handler (replaces current ad-hoc dispatch)

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"

Benefits

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

Alternative: GADT approach

data Action (a :: ActionKind) where
  StartTask :: TaskId -> Action 'Logged
  StopTask :: TaskId -> Action 'Logged
  -- Can't create Action 'Unlogged

data ActionKind = Logged  -- no Unlogged constructor!

Files

  • Omni/Agent/Telegram/Actions.hs (new)
  • Omni/Agent/Telegram.hs (refactor callbacks to use Actions)

Timeline (1)

🔄[human]Open → Done1 month ago