# Haskell Frontend Guide

A comprehensive guide for developers unfamiliar with Haskell working with the `frontend-hs/` codebase.

## Overview

The Haskell frontend (`frontend-hs/`) is a WebSocket client that communicates with the Cognition hub using JSON-RPC. It provides:

1. **Low-level RPC primitives** for JSON-RPC over WebSocket
2. **Typed plugin APIs** that wrap RPC calls (Bash, Health)
3. **Streaming interfaces** using the `streaming` library

## Project Structure

```
frontend-hs/
├── src/
│   └── Cognition/
│       ├── Rpc.hs           # Main entry point & docs
│       ├── Rpc/
│       │   ├── Types.hs     # JSON-RPC protocol types
│       │   └── Client.hs    # WebSocket client implementation
│       ├── Bash.hs          # Typed Bash plugin API
│       └── Health.hs        # Typed Health plugin API
└── app/
    └── Main.hs              # CLI application
```

## Core Concepts for Non-Haskell Developers

### 1. Type Signatures

In Haskell, functions have explicit type signatures that read as: `functionName :: InputType -> OutputType`

Example from `Cognition.Bash:36`:
```haskell
execute :: HubConnection -> Text -> Stream (Of BashEvent) IO ()
```

**Translation**: "`execute` is a function that takes:"
- A `HubConnection` AND
- A `Text` (string)
- And returns a `Stream` of `BashEvent`s that performs `IO` effects

The `->` separates each parameter and the final return type.

### 2. Newtypes vs Data

**`newtype`** (like `RequestId` at `Cognition.Rpc.Types:30`):
```haskell
newtype RequestId = RequestId { unRequestId :: Int }
```
- A zero-cost wrapper around a single value
- Provides type safety (can't accidentally use an `Int` where `RequestId` is expected)
- `unRequestId` is an accessor function: `unRequestId (RequestId 5) = 5`

**`data`** (like `BashEvent` at `Cognition.Bash:19-23`):
```haskell
data BashEvent
  = Stdout { line :: Text }
  | Stderr { line :: Text }
  | Exit { code :: Int }
```
- A sum type (like a Rust enum or TypeScript union)
- Can be one of several variants
- Each variant can hold different data

### 3. Deriving Strategies

Lines like `deriving stock (Show, Eq, Generic)` (e.g., `Cognition.Rpc.Types:52`):

```haskell
deriving stock (Show, Eq, Generic)
deriving newtype (ToJSON, FromJSON)
```

**What this means:**
- `stock`: Use compiler-provided implementations
  - `Show`: Convert to string for debugging
  - `Eq`: Check equality
  - `Generic`: Enable automatic JSON serialization
- `newtype`: Reuse the wrapped type's implementation
  - `ToJSON/FromJSON`: Serialize as the underlying type

### 4. Pattern Matching

Example from `Cognition.Rpc.Client:220-235`:
```haskell
case resp of
  RpcError _ err -> do
    liftIO $ putStrLn $ "Subscription error: " <> show err
    pure ()

  RpcSuccess _ result -> do
    -- handle success
```

**Similar to:**
- TypeScript: `if (resp.type === 'error') { ... } else if (resp.type === 'success') { ... }`
- Rust: `match resp { RpcError(_, err) => ..., RpcSuccess(_, result) => ... }`

### 5. The `do` Notation

`do` blocks are syntax sugar for sequencing operations (like async/await):

```haskell
do
  conn <- connect defaultConfig
  result <- someOperation conn
  disconnect conn
```

**Reads as**: "First connect, then use that connection for an operation, then disconnect"

The `<-` extracts values from monadic contexts (like `await` in JavaScript).

### 6. Streaming

The `streaming` library provides efficient, constant-memory streams:

```haskell
Stream (Of BashEvent) IO ()
```

**Translation**: "A stream that yields `BashEvent` values and performs `IO` effects"

**Common operations:**
- `S.yield item` - emit an item into the stream
- `S.mapMaybe f` - filter and transform stream items
- `S.print` - print each item
- `S.foldM_` - consume stream with side effects

Think of it like Node.js streams or Rust iterators, but with better memory guarantees.

## Architecture

### Low-Level RPC (`Cognition.Rpc.Client`)

The core primitive is **`hubRpc`** at `Cognition.Rpc.Client:193-255`:

```haskell
hubRpc :: HubConnection -> Text -> Value -> Stream (Of HubStreamItem) IO ()
```

**What it does:**
1. Generates a unique request ID
2. Creates a queue for receiving subscription notifications
3. Sends a JSON-RPC subscription request
4. Waits for subscription confirmation (gets back a subscription ID)
5. Returns a stream that yields items as they arrive from the hub
6. Automatically cleans up when the stream completes

**Key implementation details:**

#### Connection Management (`Cognition.Rpc.Client:73-107`)

```haskell
connect :: HubConfig -> IO HubConnection
```

- Establishes WebSocket connection in a background thread
- Starts a reader loop to dispatch incoming messages
- Uses STM (Software Transactional Memory) for thread-safe state management

#### Reader Loop (`Cognition.Rpc.Client:116-176`)

The reader loop continuously receives WebSocket messages and dispatches them:

- **Subscription notifications** (lines 142-152): messages with `params.subscription` but no `id`
  - Routed to the appropriate subscription queue
- **RPC responses** (lines 154-176): messages with an `id` field
  - Matched to pending requests
  - Subscription IDs are registered atomically before signaling completion (prevents race conditions)

### Concurrency Model

The client uses several Haskell concurrency primitives:

1. **`TVar`** - Transactional variable (thread-safe mutable state)
   - Used for: subscription map, pending requests map

2. **`TQueue`** - Transactional queue (thread-safe FIFO)
   - Used for: routing notifications to subscription handlers

3. **`TMVar`** - Transactional MVar (single-value channel)
   - Used for: signaling connection ready, waiting for RPC responses

4. **`Async`** - Lightweight thread handle
   - Used for: background reader thread

**Why STM?** Software Transactional Memory provides composable, deadlock-free concurrency. Operations like "check if subscription exists, then insert" happen atomically.

### Typed Plugin APIs

Plugins like `Cognition.Bash` and `Cognition.Health` provide typed wrappers:

**Pattern** (from `Cognition.Bash:36-37`):
```haskell
execute :: HubConnection -> Text -> Stream (Of BashEvent) IO ()
execute conn cmd = S.mapMaybe extractBashEvent $ hubRpc conn "bash_execute" (toJSON [cmd])
```

**What's happening:**
1. Call `hubRpc` with method name and JSON-encoded params
2. Get back a stream of generic `HubStreamItem`s
3. Use `S.mapMaybe extractBashEvent` to:
   - Filter for items with the right content type (`bash.event`)
   - Parse JSON into typed `BashEvent` values
   - Discard malformed items

This provides type-safe access to plugin functionality while the underlying RPC remains flexible.

### JSON Serialization

The codebase uses `aeson` for JSON handling with two approaches:

1. **Automatic derivation** (most types):
```haskell
deriving stock (Generic)
deriving anyclass (ToJSON, FromJSON)
```

2. **Manual instances** (custom formats):

From `Cognition.Rpc.Types:39-43`:
```haskell
instance FromJSON SubscriptionId where
  parseJSON (String s) = pure $ SubscriptionId s
  parseJSON v = pure $ SubscriptionId $ T.decodeUtf8 $ LBS.toStrict $ encode v
```
**Why manual?** Subscription IDs can be strings or numbers, so this normalizes them to strings.

From `Cognition.Rpc.Types:157-184` - the `HubStreamItem` parser:
```haskell
instance FromJSON HubStreamItem where
  parseJSON = withObject "HubStreamItem" $ \o -> do
    event <- o .: "event" :: Parser Text
    dataObj <- o .: "data"
    case event of
      "progress" -> withObject "Progress" parseProgress dataObj
      "data"     -> withObject "Data" parseData dataObj
      ...
```
**Why manual?** The stream item format uses a discriminated union with an `event` field that determines structure.

## Advanced Haskell Features Used

### 1. Language Extensions

From `cognition-client.cabal:35-42`:

- **`OverloadedStrings`**: String literals are polymorphic (can be `Text`, `ByteString`, etc.)
- **`DeriveGeneric`**: Auto-generate `Generic` instances for types
- **`DeriveAnyClass`**: Auto-derive typeclasses using `Generic`
- **`DerivingStrategies`**: Explicitly specify how to derive instances
- **`GeneralizedNewtypeDeriving`**: Newtypes inherit wrapped type's instances
- **`LambdaCase`**: Shorthand for `\x -> case x of ...`
- **`RecordWildCards`**: Pattern match all record fields at once

### 2. Record Wildcards

Example from `Cognition.Rpc.Types:54-60`:
```haskell
instance ToJSON RpcRequest where
  toJSON RpcRequest{..} = object
    [ "jsonrpc" .= rpcReqJsonrpc
    , "method"  .= rpcReqMethod
    , "params"  .= rpcReqParams
    , "id"      .= rpcReqId
    ]
```

`RpcRequest{..}` brings all fields (`rpcReqJsonrpc`, `rpcReqMethod`, etc.) into scope automatically.

### 3. Monadic Composition

From `Main.hs:31`:
```haskell
exitCode <- S.foldM_ handleBashEvent (pure ExitSuccess) pure $ Bash.execute conn cmd
```

**Reading right to left:**
1. `Bash.execute conn cmd` - create a stream of events
2. `S.foldM_` - fold over the stream with effects
3. `handleBashEvent` - process each event, accumulating exit code
4. Result is the final exit code

### 4. Operator Sections

- `<$>` - `fmap` (map over a context)
- `<*>` - `ap` (apply a function in a context)
- `.:` - JSON field accessor from aeson
- `.=` - JSON field constructor from aeson
- `<>` - Append/concatenate (works on Text, lists, etc.)

## Common Patterns

### Creating a New Plugin API

To add a typed API for a new plugin (e.g., "FooPlugin"):

1. **Create `src/Cognition/Foo.hs`**:

```haskell
module Cognition.Foo (FooEvent(..), operation) where

import Data.Aeson
import Data.Text (Text)
import GHC.Generics (Generic)
import Streaming (Stream, Of)
import qualified Streaming.Prelude as S

import Cognition.Rpc.Client (HubConnection, hubRpc)
import Cognition.Rpc.Types (HubStreamItem(..))

-- Define your event type
data FooEvent
  = FooResult { value :: Int }
  | FooError { message :: Text }
  deriving stock (Show, Eq, Generic)

-- Parse from JSON
instance FromJSON FooEvent where
  parseJSON = withObject "FooEvent" $ \o -> do
    typ <- o .: "type"
    dat <- o .: "data"
    case typ :: Text of
      "result" -> FooResult <$> dat .: "value"
      "error"  -> FooError <$> dat .: "message"
      _        -> fail $ "Unknown foo event type: " <> show typ

-- Public API function
operation :: HubConnection -> Text -> Stream (Of FooEvent) IO ()
operation conn param = S.mapMaybe extractFooEvent $
  hubRpc conn "foo_operation" (toJSON [param])

-- Extract and parse events
extractFooEvent :: HubStreamItem -> Maybe FooEvent
extractFooEvent (StreamData _ contentType dat)
  | contentType == "foo.event" =
      case fromJSON dat of
        Success evt -> Just evt
        Error _     -> Nothing
  | otherwise = Nothing
extractFooEvent _ = Nothing
```

2. **Add to `cognition-client.cabal`** (line 18):
```cabal
Cognition.Foo
```

3. **Use it**:
```haskell
import qualified Cognition.Foo as Foo

main = do
  conn <- connect defaultConfig
  S.print $ Foo.operation conn "test"
  disconnect conn
```

### Error Handling

The codebase uses Haskell's built-in error handling:

1. **`Maybe`** - represents absence of value:
```haskell
extractBashEvent :: HubStreamItem -> Maybe BashEvent
```
Returns `Nothing` if item doesn't match expected type.

2. **`Either`** - represents success or failure:
```haskell
case eitherDecode msg of
  Left err -> handleError err
  Right val -> processValue val
```

3. **Exceptions** (sparingly):
```haskell
`catch` \(_ :: SomeException) -> pure ()
```
Used mainly for cleanup and connection handling.

## Building and Running

### Build the project:
```bash
cd frontend-hs
cabal build
```

### Run the CLI:
```bash
cabal run cognition-cli -- health
cabal run cognition-cli -- bash execute echo hello
```

### Run in GHCi (REPL):
```bash
cabal repl
```

Then:
```haskell
import Cognition.Rpc
import qualified Cognition.Bash as Bash
import qualified Streaming.Prelude as S

conn <- connect defaultConfig
S.print $ Bash.execute conn "echo hello"
disconnect conn
```

## Understanding The Flow

Let's trace a bash execution (`Main.hs:28-33`):

```haskell
runBash :: Text -> IO ()
runBash cmd = do
  conn <- connect defaultConfig                            -- 1
  exitCode <- S.foldM_ handleBashEvent                     -- 2
                       (pure ExitSuccess)
                       pure
                       $ Bash.execute conn cmd              -- 3
  disconnect conn                                          -- 4
  exitWith exitCode                                        -- 5
```

**Step by step:**

1. **Connect** (`Cognition.Rpc.Client:74`):
   - Opens WebSocket connection
   - Starts reader thread
   - Returns `HubConnection` handle

2. **Execute** (`Cognition.Bash:36`):
   ```haskell
   Bash.execute conn cmd = S.mapMaybe extractBashEvent $ hubRpc conn "bash_execute" (toJSON [cmd])
   ```
   - Calls `hubRpc` with method name and command
   - Filters/parses stream items into `BashEvent`s

3. **Inside `hubRpc`** (`Cognition.Rpc.Client:193`):
   - Generates request ID
   - Creates notification queue
   - Sends subscription request over WebSocket
   - Waits for subscription confirmation
   - Yields stream items as they arrive
   - The reader thread (running in background) routes incoming messages to the queue

4. **Reader Thread** (`Cognition.Rpc.Client:121`):
   - Continuously receives WebSocket messages
   - Parses JSON
   - Routes notifications to subscription queues
   - Routes responses to pending request handlers

5. **Stream Processing** (`Main.hs:31`):
   ```haskell
   S.foldM_ handleBashEvent (pure ExitSuccess) pure
   ```
   - Consumes each `BashEvent`
   - `handleBashEvent` prints output and tracks exit code
   - Returns final exit code

6. **Cleanup**:
   - Stream completes when `StreamDone` received
   - `hubRpc` removes subscription from map
   - `disconnect` cancels reader thread and closes WebSocket

## Key Takeaways

1. **Type Safety**: The type system prevents entire classes of errors (wrong JSON structure, missing fields, etc.)

2. **Composability**: Small pieces (`hubRpc`, `extractBashEvent`, stream operators) combine to create complex functionality

3. **Resource Safety**: Automatic cleanup via stream completion, no manual subscription management needed

4. **Concurrency**: STM provides safe, composable concurrent operations without locks

5. **Streaming**: Constant memory usage regardless of output size

## Common Questions

**Q: What's `IO`?**
A: The `IO` type represents side effects (file access, network, printing, etc.). Pure functions have no `IO` in their signature.

**Q: What's `liftIO`?**
A: Lifts an `IO` action into a more complex monad (like `Stream ... IO`). Think of it as "run this IO action here".

**Q: What are those weird operators (`<$>`, `<*>`, etc.)?**
A: Infix functions. `a <$> b` is the same as `fmap a b`. They're used for ergonomics and composability.

**Q: Why so many type parameters?**
A: Haskell's type system is very expressive. `Stream (Of BashEvent) IO ()` precisely describes a streaming computation.

**Q: How do I debug?**
A: Add `trace` calls (from `Debug.Trace`):
```haskell
import Debug.Trace
trace ("Debug: " <> show value) $ ...
```

Or use GHCi to inspect values interactively.

## Resources

- [Haskell Documentation](https://www.haskell.org/documentation/)
- [streaming library](https://hackage.haskell.org/package/streaming)
- [aeson JSON library](https://hackage.haskell.org/package/aeson)
- [STM tutorial](https://www.schoolofhaskell.com/school/advanced-haskell/beautiful-concurrency)

## Next Steps

1. Read `Cognition.Rpc` module documentation (`src/Cognition/Rpc.hs:1-74`)
2. Explore one plugin implementation (`Cognition.Bash` is simpler than `Health`)
3. Try adding a new plugin API using the pattern above
4. Experiment in GHCi to understand the types interactively
