# Kipinä Agentic Studio — Rakennuspalaset

Tämä dokumentti kuvaa projektin UI-komponentit, arkkitehtuuripatternit ja työnkulut niin, että vastaavan hajautetun AI-laskentaverkon ja agenttipohjaisen käyttöliittymän voi rakentaa alusta asti.

## Yleiskuva

```
┌─────────────────────────────────────────────────────┐
│  Selain (käyttäjä)                                  │
│  ┌──────────┐  ┌──────────┐  ┌───────────────────┐  │
│  │ Verkko-  │  │ Koodi-   │  │ Agents-näkymä     │  │
│  │ näkymä   │  │ labra    │  │ ┌───────────────┐ │  │
│  │          │  │          │  │ │ Terminaali    │ │  │
│  │ Stats    │  │ Editor   │  │ │ Tab-complete  │ │  │
│  │ Chat     │  │ Pipeline │  │ │ Dropdown      │ │  │
│  │ Tokenit  │  │ Tulokset │  │ │ Historia      │ │  │
│  └────┬─────┘  └────┬─────┘  │ └───────────────┘ │  │
│       │              │        └────────┬──────────┘  │
│       └──────────┬───┘                 │             │
│            UI WebSocket          HTTP API            │
│                  │              /api/v1/chat          │
│  ┌───────────────┴──────────────┐      │             │
│  │  Wasm Compute Node           │      │             │
│  │  (Candle + Burn)             │      │             │
│  │  ┌─────────┐ ┌────────────┐  │      │             │
│  │  │ RAM     │ │ IndexedDB  │  │      │             │
│  │  │ Cache   │ │ Cache      │  │      │             │
│  │  └─────────┘ └────────────┘  │      │             │
│  │  ┌─────────────────────────┐ │      │             │
│  │  │ Model Cache (QwenModel) │ │      │             │
│  │  └─────────────────────────┘ │      │             │
│  └──────────────┬───────────────┘      │             │
│                 │ WS                   │             │
└─────────────────┼──────────────────────┼─────────────┘
                  │                      │
         ┌────────┴──────────────────────┴──┐
         │  Hub (Axum + Tokio)              │
         │  ┌────────────┐ ┌─────────────┐  │
         │  │ Broadcast  │ │ Node        │  │
         │  │ Channel    │ │ Registry    │  │
         │  └────────────┘ └─────────────┘  │
         │  ┌────────────┐ ┌─────────────┐  │
         │  │ Busy-State │ │ Rate Limit  │  │
         │  │ Tracker    │ │ + Auth      │  │
         │  └────────────┘ └─────────────┘  │
         │  ┌─────────────────────────────┐ │
         │  │ SQLite (sessiot, tulokset)  │ │
         │  └─────────────────────────────┘ │
         └──────────────────────────────────┘
```

---

## 1. WebSocket-reaaliaikakommunikaatio

### 1.1 Hub ↔ Node broadcast-kanava

**Tarkoitus:** Jakaa tehtäviä ja vastaanottaa tuloksia kaikilta laskentasolmuilta.

**Työnkulku:**
1. Hub luo `tokio::sync::broadcast::channel(100)`
2. Jokainen solmu saa oman `rx = stats_tx.subscribe()`
3. Hub broadcastaa tehtävät: `stats_tx.send(json)`
4. Solmut suodattavat viestin tyypin ja `selected_task`:n perusteella

**Viestityupit:**

| Tyyppi | Suunta | Sisältö |
|--------|--------|---------|
| `stats` | Hub → kaikki | nodes, vram_gb, tasks |
| `pair_task` | Hub → tokenize-solmut | en, fi tekstiparit |
| `llm_prompt` | Hub → valittu solmu | prompt, model, task_id |
| `llm_chunk` | Solmu → Hub → UI | token (1 kerrallaan) |
| `llm_done` | Solmu → Hub → UI | response, tokens_generated, duration_ms |
| `llm_error` | Solmu → Hub → UI | error, task_id |
| `task_routed` | Hub → UI | status (routed/queued), node_id, message |

**Lagged-viestien käsittely:**
```rust
match rx.recv().await {
    Ok(msg) => { /* käsittele */ }
    Err(broadcast::error::RecvError::Lagged(n)) => {
        // Ohitetaan vanhat viestit, ei katkaista yhteyttä
        continue;
    }
    Err(_) => break, // Kanava suljettu
}
```

### 1.2 Kohdennettu reititys (Direct Channel)

**Tarkoitus:** Lähetä tehtävä yhdelle tietylle solmulle broadcastin sijaan.

**Työnkulku:**
1. Jokainen solmu saa `mpsc::unbounded_channel` yhdistyessään
2. Hub tallentaa `node_channels: HashMap<u64, UnboundedSender>`
3. API-pyyntö → valitaan vapaa solmu → lähetetään suoraan kanavaan
4. Broadcast-kanavaa käytetään vain tuloksen välittämiseen UI:lle

```rust
let channels = state.node_channels.read().await;
if let Some(tx) = channels.get(&target_node_id) {
    tx.send(msg.to_string());
}
```

### 1.3 Busy-state ja työjono

**Tarkoitus:** Estä tehtävien reititys varatuille solmuille.

**Rakenne:**
- `node_busy: HashSet<u64>` — solmut joilla on aktiivinen tehtävä
- Asetetaan kun tehtävä reititetään, vapautetaan `llm_done`/`llm_error`:ssa
- Jos kaikki solmut varattuja → pollaa 500ms välein, max 30s

**UI-palaute:**
```json
{"type": "task_routed", "status": "queued", "message": "Kaikki 2 solmua varattuja — odotetaan..."}
{"type": "task_routed", "status": "routed", "node_id": 3, "message": "Solmu #3 vapautui (2.5s jonossa)"}
```

---

## 2. Wasm-laskentasolmu

### 2.1 Elinkaari

```
init() → start_agent_node(ws_url, has_webgpu, device_info, task_id)
  │
  ├─ Avaa WebSocket hubiin
  ├─ Lähettää auth-viestin (laitetiedot, selected_task)
  ├─ Rekisteröityy onmessage-käsittelijä
  │    ├─ pair_task → tokenize
  │    ├─ llm_prompt → inference
  │    └─ ai_task → tensor matmul
  └─ Odottaa tehtäviä loopissa
```

**Globaali tila (atominen, lukitsematon):**
```rust
static GPU_LOAD_PERCENT: AtomicU32 = AtomicU32::new(50);
static LLM_BUSY: AtomicBool = AtomicBool::new(false);
static SELECTED_TASK: AtomicU32 = AtomicU32::new(0);
```

### 2.2 Kolmitasoinen cache

```
Pyyntö → [1] RAM-cache (thread_local HashMap)
            │ miss
            ▼
         [2] IndexedDB (selaimen pysyvä tallennus)
            │ miss
            ▼
         [3] Verkko (HuggingFace CDN, streaming + 5% progressi)
            │
            ▼
         Tallenna → IndexedDB → RAM-cache
```

| Taso | Nopeus | Koko | Pysyvyys |
|------|--------|------|----------|
| RAM | ~0ms | Rajaton | Sivulataus |
| IndexedDB | ~50ms | ~50GB | Pysyvä |
| Verkko | ~10s/100MB | ∞ | — |

**Malliinstanssin cache (neljäs taso):**
```rust
thread_local! {
    static MODEL_CACHE: RefCell<Option<CachedModel>> = RefCell::new(None);
}
// clear_kv_cache() promptien välillä — ei tarvitse rakentaa mallia uusiksi
```

### 2.3 Warmup-esilataus

**Tarkoitus:** Lataa malli valmiiksi ennen ensimmäistä oikeaa promptia.

```javascript
// Lähetetään 1 tokenin warmup heti kun WS on auki
uiSocket.send(JSON.stringify({
    type: 'user_text',
    text: '{"prompt":"warmup","max_tokens":1}',
    task_type: 'qwen-coder'
}));
```

---

## 3. LLM-inferenssipipeline

### 3.1 Prompt-formaatti (ChatML + prefill)

```
<|im_start|>system
You are a coding assistant. Respond with ONLY code.<|im_end|>
<|im_start|>user
hello world in python<|im_end|>
<|im_start|>assistant
```                          ← PREFILL: pakottaa mallin aloittamaan koodilla
```

**Prefill-tekniikka:** Lisäämällä ` ``` ` assistantin vastauksen alkuun malli jatkaa suoraan koodilla eikä tuota "Sure! Here is..." -johdantoa. Säästää 10-20 tokenia per vastaus.

### 3.2 Sampling-parametrit

| Parametri | Arvo | Tarkoitus |
|-----------|------|-----------|
| `temperature` | 0.7 | Pehmentää jakaumaa, vähentää toistoa |
| `top_k` | 40 | Rajaa valinnan 40 todennäköisimpään tokeniin |
| `repetition_penalty` | 1.15 | Rankaisee jo generoitujen tokenien uudelleenvalintaa |
| `max_tokens` | 128 | Oletusraja, JSON-promptilla konfiguroitavissa |

**Sampling-funktio (top-k + temperature + repetition penalty):**
```rust
fn sample_top_k_with_penalty(logits, k, temperature, generated_tokens, penalty) -> u32 {
    // 1. Repetition penalty: vähennä aiempien tokenien logitteja
    // 2. Temperature scaling: jaa logitit temperaturella
    // 3. Top-k: ota k suurinta
    // 4. Softmax top-k:lle
    // 5. Satunnaisvalinta kumulatiivisella todennäköisyydellä (XorShift RNG)
}
```

### 3.3 Stop-sekvenssit

Generointi katkaistaan ja teksti trimmataan kun malli alkaa selittää:

```rust
let stop_patterns = ["\n###", "\nExplanation", "\nNote:", "\nOutput:", "\n```\n\n"];
```

### 3.4 Vastauksen siivous

```
Raakavastaus:     "Sure! Here is...\n```python\n# This is a simple program\nprint('hi')\n```"
                   │
strip_markdown:    "# This is a simple program\nprint('hi')"
                   │
strip_preamble:    "print('hi')"
```

**Tunnistettavat selityskommentit:** `# This is`, `# simple`, `# program that`, `# here is`, `# the following`, `# below`

### 3.5 Streaming

Jokainen generoitu token lähetetään heti `llm_chunk`-viestinä:
```json
{"type": "llm_chunk", "token": "print", "prompt": "...", "model": "Qwen2.5-Coder", "task_id": "uuid"}
```

UI päivittää streaming-korttia reaaliaikaisesti appendaamalla tokeneita.

---

## 4. Terminaaliemulaattori

### 4.1 Rakenne

```html
<div id="agent-hub-status">     <!-- Status-palkki (Hub + Laskenta) -->
<div id="agent-terminal">       <!-- Scrollaava tulosalue, max 100 riviä -->
<div>                            <!-- Input-rivi -->
    <span>$</span>
    <input id="term-input">
    <div id="term-dropdown">    <!-- Autocompletion-valikko -->
</div>
```

### 4.2 Komentojen käsittely

```javascript
function termExec(cmd) {
    // Parsitaan: "kpn" + alikomento + argumentit
    // Tuetut: help, run, pipeline, load, status, models, hello, clear
    // Agenttinimi → malli-mapping: "coder" → "qwen-coder"
}
```

### 4.3 Tab-completion (kolmitasoinen)

```javascript
const kpnCommands = {
    'kpn': ['help', 'run', 'pipeline', 'load', ...],
    'kpn run': ['coder', 'manager', 'qwen-coder', ...],
};
const kpnExamples = {
    'kpn run coder': ['"hello world in python"', ...],
};
```

**Käyttö:**

| Näppäin | Toiminto |
|---------|----------|
| TAB | Täydennä seuraava sana tai avaa dropdown |
| Shift-TAB | Poista viimeinen sana (lainausmerkit kokonaisuutena) |
| ↑ / ↓ | Navigoi dropdownissa (tai komentohistoriassa) |
| Enter | Valitse dropdownista tai suorita komento |
| Esc | Sulje dropdown |

### 4.4 Dropdown-valikko

```javascript
function showDropdown(items, prefix) {
    // Luo div.term-dd-item per vaihtoehto
    // Positio: absolute, bottom: 100% (inputin yläpuolella)
    // Mouseenter → highlight, click → valinta
}
```

### 4.5 Komentohistoria

```javascript
const termHistory = [];      // Kaikki ajetut komennot (viimeisin ensin)
let termHistIdx = -1;        // Nykyinen positio historiassa
// ArrowUp: termHistIdx++, ArrowDown: termHistIdx--
```

---

## 5. Status-palkit ja tilaindikaattorit

### 5.1 Hub-yhteyden tila

| Tila | Väri | Teksti | Tooltip |
|------|------|--------|---------|
| Yhdistetään | 🟡 | "Yhdistetään..." | WebSocket-yhteys Kipinä Hubiin |
| Yhdistetty | 🟢 | "Yhdistetty" | Tehtävien jakelu aktiivinen |
| Katkennut | 🔴 | "Yhteys katkennut" | Tarkista verkko, lataa uudelleen |

### 5.2 Laskentasolmun tila

| Tila | Väri | Teksti | Nappi |
|------|------|--------|-------|
| Ei käynnissä | ⚫ | "—" | `[Alusta laskentasolmu]` sininen |
| Lataa | 🟡 | "Ladataan..." | `[Peruuta]` punainen |
| Valmis | 🟢 | "Qwen2.5-Coder" | `[✓ Valmis]` vihreä |

### 5.3 Pipeline-tilakone (Codelab)

```
Step 1: WebAssembly-ytimen lataus     [◯ → ◷ → ✓]
Step 2: Tokenizer (7 MB)              [◯ → ◷ → ✓]
Step 3: Mallipainot (990 MB)           [◯ → ◷ 45% → ✓ cache]
Step 4: Mallin rakentaminen            [◯ → ◷ → ✓]
Step 5: Valmis generoimaan             [◯ → ✓]
```

**Seuranta console.log-viesteistä:**
```javascript
if (msg.includes('[Coder]') && msg.includes('Malli ladattu')) {
    // Merkkaa kaikki vaiheet valmiiksi (myös cache-hitillä)
    setStep('step-wasm', 'done');
    setStep('step-tokenizer', 'done');
    setStep('step-model', 'done', 'cache');
    setStep('step-build', 'done');
    setStep('step-ready', 'done');
}
```

---

## 6. Tietoturva

### 6.1 XSS-suojaus

```javascript
function esc(str) {
    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;')
        .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
```

**Käyttöpaikat:** Kaikki `innerHTML`-insertoinnit joissa on käyttäjä- tai backend-dataa.

### 6.2 System prompt -piilotus

```javascript
function stripSystemPrompt(prompt) {
    const parts = prompt.split('\n\n');
    return parts[parts.length - 1] || prompt;
}
```

### 6.3 Viestityyppivalidointi (backend)

```rust
const ALLOWED_MSG_TYPES: &[&str] = &[
    "auth", "result", "pair_done", "llm_chunk", "llm_done",
    "llm_error", "download_progress", "user_text", "single_tokenize_done"
];

fn validate_message(text: &str) -> Result<Value, &'static str> {
    // 1. JSON-parsinta
    // 2. "type"-kenttä pakollinen
    // 3. Tyyppi sallittujen listalla
    // 4. Tyyppikohtainen validointi (esim. pair_done: token_count <= 10000)
}
```

### 6.4 Rate limiting

```rust
// Per-IP liukuva ikkuna: max 10 pyyntöä per 60s
let entry = limits.entry(addr.ip()).or_insert((now, 0));
if now.duration_since(entry.0).as_secs() >= 60 {
    *entry = (now, 1);
} else {
    entry.1 += 1;
    if entry.1 > 10 { return 429 Too Many Requests; }
}
```

### 6.5 Gamification-huijauksen esto

```rust
// Hub jakaa task_id:n → tallentaa pending_task_ids:hen
// Merkkejä jaetaan VAIN jos llm_done sisältää validin task_id:n
let valid_task = state.pending_task_ids.lock().unwrap().remove(tid);
if active_incentives && valid_task {
    *balance += 20;
}
```

---

## 7. Syntaksikorostus

### 7.1 Highlight.js-integraatio

```html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
```

```javascript
function highlightCode(code) {
    if (typeof hljs !== 'undefined') {
        return hljs.highlightAuto(code).value;  // Automaattinen kielentunnistus
    }
    return esc(code);  // Fallback
}
```

**Käyttöpaikat:** Codelab-tulokset, agents-terminaalin vastaukset, network-chat.

---

## 8. Agenttien orkestrointi

### 8.1 Multi-agent pipeline

```
┌──────────┐     ┌──────────┐     ┌──────────┐
│ Manageri │ ──→ │ Koodari  │ ──→ │ Testaaja │
│ Analysoi │     │ Koodaa   │     │ Arvioi   │
│ tehtävä  │     │ ratkaisu │     │ koodi    │
└──────────┘     └──────────┘     └──────────┘
```

```javascript
async function kpnPipeline(task) {
    const plan = await kpnRun('qwen-coder', `Analysoi: ${task}`);
    if (!plan) return;
    const code = await kpnRun('qwen-coder', `Koodaa: ${plan}`);
    if (!code) return;
    await kpnRun('smollm-135m', `Arvioi: ${code}`);
}
```

### 8.2 Agenttien promptien hallinta

```javascript
const agentPrompts = {
    manager: { model: 'qwen-coder', prompt: 'Olet projektipäällikkö...' },
    coder:   { model: 'qwen-coder', prompt: 'Olet ohjelmistokehittäjä...' },
    // ...
};
// Tallennetaan localStorage:en per agentti
localStorage.setItem('kpn-agent-prompt-coder', customPrompt);
```

### 8.3 Yhteinen promptikonteksti

```javascript
async function kpnRun(model, prompt) {
    const parts = [];
    if (sharedPrompt) parts.push(sharedPrompt);           // Kaikille yhteinen
    if (agent.prompt) parts.push(agent.prompt);            // Agenttikohtainen
    parts.push(prompt);                                     // Käyttäjän pyyntö
    const fullPrompt = parts.join('\n\n');
    // → HTTP POST /api/v1/chat/completions
}
```

---

## 9. Teknologiapino

| Kerros | Teknologia | Tarkoitus |
|--------|------------|-----------|
| Frontend | Vanilla JS + HTML + CSS | Ei build-steppiä, toimii suoraan |
| Wasm | Rust + wasm-bindgen | Inferenssi selaimessa |
| LLM | Candle (Rust) | Transformer-inferenssi CPU:lla |
| Tensorit | Burn (Rust) | GPU-tensorilaskenta (WebGPU/NdArray) |
| Backend | Axum + Tokio (Rust) | Async WebSocket + HTTP -palvelin |
| Tietokanta | SQLite (rusqlite) | Sessiot ja tulokset |
| Cache | IndexedDB | Mallipainot selaimen pysyvässä muistissa |
| Korostus | Highlight.js (CDN) | Syntaksikorostus, automaattinen kielentunnistus |
| Tokenizer | HuggingFace tokenizers | BPE-tokenisaatio Wasmissa |

---

## 10. Jatkokehitysideoita

Näiden rakennuspalasten pohjalta voi rakentaa:

- **Oma chat-UI:** WebSocket + streaming + syntaksikorostus
- **Hajautettu laskentaverkko:** Hub + node-rekisteri + busy-state + työjono
- **Selain-LLM:** Wasm + Candle + IndexedDB-cache + warmup
- **Agenttipohjainen työnkulku:** Pipeline + prompt-orkestrointi + reititys
- **Terminaaliemulasttori:** Input + historia + tab-completion + dropdown
- **Reaaliaikadashboard:** WebSocket broadcast + tilaindikaattorit + metriikat
