# Konsept: Orkestrering
**Filsti:** `docs/concepts/orkestrering.md`

## 1. Konsept

Orkestreringer er noder som *gjør* ting. De er oppskrifter —
sekvenser av CLI-verktøy som utføres automatisk når en trigger
aktiveres. Tre nivåer: deklarativt script (ingen AI),
fritekst med AI-tolkning, og full drømmemodus.

## 2. Hvorfor

Synops har mange automatiseringer hardkodet i vaktmesteren:
podcast-pipeline, AI-beriking, publisering. De er usynlige,
uendrelige og umulige å tilpasse for brukere.

Orkestreringer gjør automatisering til førsteklasses graf-
primitiver — synlige, redigerbare, delbare, versjonerbare.

## 3. Nodemodell

```
node_kind: 'orchestration'
title: "Podcast: opptak → publisering"
content: <script eller fritekst>
metadata: {
  "trigger": {
    "event": "communication.ended",
    "conditions": {
      "has_trait": "podcast",
      "has_media": "audio"
    }
  },
  "executor": "script" | "bot" | "dream",
  "intelligence": 2,
  "effort": 2
}
```

### Tre typer noder som gjør

| node_kind | Ikon | Aktivering | Eksempel |
|-----------|------|------------|----------|
| `cli_tool` | 🔧 | Kalles av andre | synops-transcribe |
| `ai_preset` | 🔧 | Kalles av AI-verktøy | "Rens tekst"-prompt |
| `orchestration` | ⚡ | **Trigger-drevet, utfører seg selv** | Podcast-pipeline |

⚡ signaliserer: "denne noden gjør noe av seg selv."

## 4. To lag: menneskelig språk og teknisk script

Orkestreringer har to representasjoner av samme oppskrift —
et menneskelig lesbart språk som kompileres til tekniske
CLI-kall. Brukeren skriver og leser det ene, vaktmesteren
kjører det andre.

### Menneskelig lag (bruker skriver)

```
NÅR innspilling avsluttet
HVIS samling har podcast

1. transkriber lydfilen (stor modell)
   ved feil: transkriber lydfilen (medium modell)
2. oppsummer samtalen
3. oppdater rss-feed

ved feil: opprett oppgave "Pipeline feilet" (bug)
```

Ingen `--cas-hash`, ingen `{event.*}`, ingen CLI-syntax.
Norske verb som matcher verktøy. Argumenter i parenteser.
Lesbart for alle.

### Teknisk lag (vaktmesteren kjører)

```
NÅR innspilling.avsluttet
HVIS samling.har_trait("podcast")

1. synops-transcribe --cas-hash {event.cas_hash} --model large
   VED_FEIL: synops-transcribe --cas-hash {event.cas_hash} --model medium
2. synops-summarize --communication-id {event.communication_id}
3. synops-rss --collection-id {event.collection_id}

VED_FEIL: work_item "Podcast-pipeline feilet" --tag bug
```

Generert automatisk fra det menneskelige laget via kompilatoren.
Deterministisk, gratis, raskt.

### Kompilatoren

Vaktmesteren kompilerer menneskelig → teknisk ved lagring.
Matchingen bruker `cli_tool`-noders metadata:

```jsonc
{
  "binary": "synops-transcribe",
  "aliases": ["transkriber", "transkribering"],
  "description": "Whisper-transkribering av lydfil",
  "args_hints": {
    "lydfilen": "{event.cas_hash}",
    "stor modell": "--model large",
    "medium modell": "--model medium"
  }
}
```

"transkriber lydfilen (stor modell)" matcher:
- "transkriber" → alias for synops-transcribe
- "lydfilen" → {event.cas_hash} (fra trigger-kontekst)
- "(stor modell)" → --model large

### Kompileringsfeil (Rust-stil)

Ved feil får brukeren presise, hjelpsomme meldinger:

```
┌─ Kompilering ────────────────────────────────┐
│                                              │
│  ✓ Linje 1: transkriber lydfilen (stor)      │
│    → synops-transcribe --model large         │
│                                              │
│  ✗ Linje 2: send epost til deltakerne        │
│    Feil: "send epost" matcher ingen verktøy  │
│    Mente du: "varsle deltakerne"?            │
│    Tilgjengelig: varsle, oppsummer, publiser  │
│                                              │
│  ✓ Linje 3: oppdater rss-feed               │
│    → synops-rss                              │
│                                              │
│  1 feil. Rett opp og prøv igjen.            │
└──────────────────────────────────────────────┘
```

### Tre visninger i editoren

```
[Enkel]  [Teknisk]  [Kompilert]
```

| Visning | Hva | Hvem |
|---------|-----|------|
| **Enkel** | Norsk, lesbart | Brukeren skriver her |
| **Teknisk** | CLI-kall med variabler | For de som vil se |
| **Kompilert** | JSON/intern | Skjult som default |

Tabbar i editoren — som raw/rendered i tekst-editoren.
Kompileringsfeil vises i sanntid mens brukeren skriver.

### Grammatikk (menneskelig lag)

```
TRIGGER    NÅR <event i naturlig språk>
           HVIS <betingelse i naturlig språk>
STEP       <N>. <verb> <objekt> [(<argument>)]
FALLBACK   ved feil: <verb> <objekt> | opprett oppgave <tittel> [(<tag>)]
```

### Grammatikk (teknisk lag)

```
TRIGGER    NÅR <event> [HVIS <betingelse>]
STEP       <N>. <tool> <args...>
FALLBACK   VED_FEIL: <tool> <args...> | work_item <title> [--tag <tag>]
VARIABLE   {event.<felt>} — substitueres fra trigger-konteksten
```

**De fleste produksjonsorkestreringer skrives og vedlikeholdes
i det menneskelige laget.** Det tekniske laget er generert.

### Nivå 2: AI-assistert oppretting

Brukeren beskriver hva de vil i naturlig språk. AI genererer
et deklarativt script (nivå 1) som vaktmesteren validerer.
**AI brukes ved oppretting, ikke ved kjøring.**

```
Bruker: "Gjør episoden klar for publisering"

  → synops-ai genererer script-forslag:

    NÅR innspilling.avsluttet
    HVIS samling.har_trait("podcast")
    1. synops-transcribe --cas-hash {event.cas_hash} --model large
       VED_FEIL: synops-transcribe --cas-hash {event.cas_hash} --model medium
    2. synops-summarize --communication-id {event.communication_id}
    3. synops-rss --collection-id {event.collection_id}
    VED_FEIL: work_item "Pipeline feilet" --tag bug

  → Vaktmesteren validerer:
    ✓ Alle verktøy finnes
    ✓ Alle variabler er gyldige
    ✓ Syntaks er korrekt

  → Bruker godkjenner eller justerer
  → Lagres som orchestration-node (nivå 1)
```

Etter oppretting kjører scriptet uten AI — deterministisk,
gratis, raskt. AI-kostnaden er én gang, ved oppretting.

### Nivå 3: Drømmemodus

Brukeren skriver hva de *ønsker*, uten å vite hvilke verktøy
som finnes. AI prøver å generere et script, og mangler
som oppdages blir feature requests.

```
Bruker: "Lag en lydfil med sammendrag og send til deltakerne"

  → synops-ai genererer:
    1. synops-summarize --communication-id {event.communication_id}
    2. synops-tts ???  ← finnes ikke

  → Vaktmesteren validerer:
    ✗ synops-tts finnes ikke

  → Tilbake til bruker:
    "Scriptet refererer til synops-tts som ikke finnes.
     Jeg oppretter en forespørsel om TTS-verktøy."
    → work_item "TTS for møteoppsummeringer" --tag feature
    → Delvis script lagres med synops-tts markert som manglende
```

### Systemprompt for script-generering

AI-modellen trenger kontekst for å generere gode scripts.
`synops-orchestrate --generate-system-prompt` bygger dette
automatisk fra PG:

```
Du er en orkestreringsplanlegger for Synops.

TILGJENGELIGE VERKTØY:
- synops-transcribe: Whisper-transkribering
  Bruk: --cas-hash <hash> --model <model>
- synops-summarize: AI-oppsummering
  Bruk: --communication-id <uuid>
- synops-rss: RSS-generering
  Bruk: --collection-id <uuid>
[... hentet fra cli_tool-noder i PG]

SCRIPT-GRAMMATIKK:
  NÅR <event> [HVIS <betingelse>]
  <N>. <tool> <args...>
  VED_FEIL: <tool> <args...> | work_item <title> [--tag <tag>]
  Variabler: {event.<felt>}, {input.<felt>}

EKSEMPLER:
[... hentet fra eksisterende orchestration-noder]

Svar KUN med et gyldig orkestreringscript.
```

Systemprompt oppdateres automatisk når nye verktøy legges til.
Fungerer med alle modeller — Claude, Llama, Mixtral, Grok.

### Tre-stegs flyten

```
1. OPPRETTING (AI, én gang)
   Bruker beskriver → synops-ai genererer script
   → vaktmester validerer → bruker godkjenner → lagres

2. KJØRING (ingen AI, hver gang)
   Trigger → vaktmester parser script → utfører CLI-kall
   → logger resultat

3. FEILHÅNDTERING (AI, sjelden)
   VED_FEIL feiler → eskalér til synops-ai
   → foreslå fix eller opprett work_item
```

AI-kostnad er nesten null i drift.

### Naturlig progresjon

```
Drømmemodus  → Bruker beskriver ønsket resultat
  ↓ AI genererer script-forslag
Script       → Vaktmester validerer, bruker godkjenner
  ↓ Kjører uten AI
Produksjon   → Deterministisk, gratis, raskt
  ↓ Hvis mønster gjentas
Kode         → Eget CLI-verktøy (synops-<verb>)
```

AI ved oppretting. Ingen AI ved kjøring. Script kan alltid
redigeres manuelt.

## 5. Strukturert trigger

Triggeren er alltid strukturert — vaktmesteren evaluerer
den effektivt uten LLM, uavhengig av utførelsesnivå:

```jsonc
{
  "trigger": {
    "event": "communication.ended",
    "conditions": {
      "has_trait": "podcast",
      "has_media": "audio"
    }
  }
}
```

### Kjente trigger-events

| Event | Beskrivelse |
|-------|-------------|
| `node.created` | Ny node opprettet |
| `edge.created` | Ny edge opprettet |
| `communication.ended` | Samtale/innspilling avsluttet |
| `node.published` | Node publisert (belongs_to med slot) |
| `scheduled.due` | Planlagt tidspunkt nådd |
| `manual` | Bruker trykker "Kjør" |

### Betingelser

Filtrerer triggeren ytterligere:
- `has_trait` — noden/samlingen har denne traiten
- `has_media` — noden har media-edge av denne typen
- `has_tag` — noden har tagged-edge med denne verdien
- `node_kind` — noden er av denne typen

## 6. Utførelse

### Script-modus (nivå 1)

```
Trigger aktiveres
  → Vaktmesteren finner matchende orchestration-node
  → Parser scriptet
  → Substituerer {event.*}-variabler fra trigger-kontekst
  → Utfører steg sekvensielt via generisk dispatch
    (synops-{tool} --payload-json)
  → Ved feil: kjør VED_FEIL-steg eller opprett work_item
  → Logger i orchestration_log
```

Ingen LLM. Deterministisk. Raskt.

### Feilhåndtering ved kjøring

Alle orkestreringer kjører som script (nivå 1). Ved feil:

```
Script kjører steg 2
  → synops-summarize returnerer exit 1
  → VED_FEIL definert? → kjør alternativ
  → Alternativ feiler også?
    → Opprett work_item med feilbeskrivelse
    → Logger i orchestration_log
    → Stopp orkestreringen
```

Ingen AI-eskalering ved kjøring. Feil håndteres av scriptet
(VED_FEIL) eller blir work_items for manuell/AI-assistert
oppfølging.

### Asynkron modus: "eventually"

Brukeren kan godta at et svar eller script-generering kommer
*etterhvert* i stedet for umiddelbart:

```
Bruker: "Lag en orkestrering for podcast-pipeline"
  → [Nå] [Eventually]

"Eventually":
  → Forespørselen lagres som work_item med tag "script_request"
  → Neste Claude Code-sesjon (task runner) plukker den opp
  → Genererer script med Opus (full verktøy-tilgang, fillesing)
  → Scriptet valideres og lagres
  → Bruker varsles: "Orkestreringen din er klar"
```

Fordeler:
- **Ingen API-kostnad** — Claude Code (betalt) gjør jobben
- **Bedre kvalitet** — Opus med full kontekst, ikke en lettvekts API-modell
- **Ingen hastverk** — script-generering trenger ikke skje i sanntid

Dette gjelder også feilhåndtering: et feilet VED_FEIL-steg
blir en work_item som Claude Code løser i neste sesjon —
reparerer scriptet, legger til manglende verktøy, osv.

## 7. Koblinger mellom orkestreringer (kaskade)

Orkestreringer er noder med edges:

```
"Podcast: opptak → publisering"
  ──triggers──→ "Varsle redaksjonen om ny episode"
  ──triggers──→ "Post til sosiale medier"
```

Output fra én orkestrering kan trigge neste via `triggers`-edge.
Kaskade via edges, ikke hardkodet.

### Implementasjon (oppgave 24.8)

Etter vellykket pipeline-utførelse sjekker `handle_orchestrate()` om
orkestreringen har utgående `triggers`-edges. For hvert gyldig mål
(må være `node_kind = 'orchestration'`) legges en ny `orchestrate`-jobb
i køen med:

- `trigger_event: "cascade"` — skiller kaskade fra primær-triggere
- `trigger_context.upstream_orchestration_id` — ID til kilden
- `trigger_context.op: "CASCADE"` — for betingelsesmatching
- `cascade_chain` — liste med alle orchestration-IDer allerede utført

Nedstrøms script kan bruke `{event.upstream_orchestration_id}` for å
referere til oppstrøms orkestrering.

### Syklusdeteksjon

Kaskadekjeden (`cascade_chain`) spores som en array i jobb-payloaden.
Før enqueue av hvert mål sjekkes:

1. **Dybdegrense:** Maks 10 ledd i kjeden (konfigurerbart via `MAX_CASCADE_DEPTH`)
2. **Syklussjekk:** Target-ID finnes ikke allerede i kjeden (inkludert
   orkestreringen som nettopp fullførte)

Blokkerte kaskader logges i `orchestration_log` med `status = 'skipped'`
og `tool_binary = 'cascade'`. Kaskade-feil er ikke-fatale — den
fullførte orkestreringen rapporteres fortsatt som suksess.

## 8. Brukergrensesnitt

```
┌─ Podcastorkestrering ⚡ ──────────────────┐
│                                           │
│  Trigger: [Innspilling avsluttet ▼]       │
│  Betingelse: [Samling har podcast-trait ▼] │
│  Modus: [Script ▼]                        │
│                                           │
│  ┌─────────────────────────────────────┐  │
│  │ NÅR innspilling.avsluttet          │  │
│  │ HVIS samling.har_trait("podcast")   │  │
│  │                                    │  │
│  │ 1. synops-transcribe               │  │
│  │    --cas-hash {event.cas_hash}     │  │
│  │    --model large                   │  │
│  │    VED_FEIL: ... --model medium    │  │
│  │ 2. synops-summarize                │  │
│  │    --communication-id {event.id}   │  │
│  │ 3. synops-rss                      │  │
│  │    --collection-id {event.coll_id} │  │
│  └─────────────────────────────────────┘  │
│                                           │
│  [▶ Test kjøring]    [↑ Konverter til AI] │
│                                           │
│  Status: Aktiv  Kjørt: 47 ganger          │
│  Sist: 2026-03-18 12:30 (OK)             │
│                                           │
│  ℹ Tilgjengelig: synops-transcribe,       │
│    synops-rss, synops-render...           │
│    (12 verktøy)                           │
└───────────────────────────────────────────┘
```

Modusvelger: Script / Fritekst / Drømmemodus.
"Test kjøring" utfører med dry-run.
"Konverter til AI" løfter scriptet til fritekst-modus.

## 9. Edge-modell

| Edge | Source → Target | Betydning |
|------|----------------|-----------|
| `belongs_to` | orchestration → collection | Tilhører denne samlingen |
| `observes` | orchestration → any node | Overvåker denne noden for trigger-events |
| `triggers` | orchestration → orchestration | Kaskade-kobling |
| `uses` | orchestration → cli_tool | Bruker dette verktøyet |
| `mentions` | orchestration → any | Refererer til denne noden |

### `observes`-edge: eksplisitt kobling

Orkestreringen peker på det den observerer:

```
"Auto-clip URL-er" (orchestration)
  ──observes──→ #Redaksjonen (communication)
  ──observes──→ #Research (communication)
```

Legg til `observes`-edge → aktivert for den noden.
Fjern edge → deaktivert. Opprettet via drag-and-drop
(se `docs/retninger/interaksjonsmodell.md`).

### Implisitt vs eksplisitt

- **`observes`-edge:** Eksplisitt. "Denne orkestreringen overvåker
  denne noden."
- **Trigger-betingelser:** Implisitt. "Overvåk alt som matcher."
- `observes` overtrumfer: fjernet `observes`-edge betyr "ikke her",
  selv om betingelsene matcher. Brukeren har kontroll.

## 10. Drømmemodus: brukeren skriver hva de vil

Brukeren begrenses ikke til kjente verktøy. De skriver fritt
— boten prøver, og mangler som oppdages blir feature requests.

```
Bruker skriver: "Lag en lydfil med sammendrag og send til deltakerne"

Boten sjekker:
  ✓ synops-summarize finnes → oppsummerer
  ✗ synops-tts finnes ikke → kan ikke lage lyd

Boten svarer:
  "Jeg oppsummerte møtet, men Synops har ikke tekst-til-tale
   ennå. Jeg oppretter en forespørsel?"

  → work_item i innboks:
    title: "TTS for møteoppsummeringer"
    tagged: "feature"
    source_material → orkestreringsnoden
```

Systemet lærer hva brukerne vil ha fra det som ikke lykkes.

## 11. Prinsipp: målet er alltid script

Enhver orkestrering har som mål å bli et deklarativt script.
AI-nivåene (fritekst, drøm) er **verktøy for å komme dit**,
ikke permanente driftsmoduser.

Hvis en orkestrering ikke kan uttrykkes som script, betyr det
én av to ting:

1. **Verktøyet mangler.** Lag et nytt CLI-verktøy som dekker
   det manglende steget. Da kan scriptet uttrykke det.
2. **Logikken er for vag.** Stram opp instruksjonene til de er
   presise nok for et script. Hvis det ikke er mulig, er
   orkestreringen kanskje ikke moden nok for automatisering.

AI som permanent feilhåndtering i en orkestrering er et tegn
på at noe er galt — enten med verktøyet eller med spesifikasjonen.
Det skal fikses, ikke kompenseres.

```
Drømmemodus  → "Hva vil vi oppnå?"
Fritekst     → "Hvordan gjør vi det?"
Script       → "Nøyaktig dette."
  ↓
Hvis script ikke er mulig:
  → Mangler verktøy?  → Lag synops-<verb>
  → For vagt?         → Stram opp spec
  → Umulig?           → Ikke automatiser dette
```

## 12. Avgrensning

- Orkestreringer er **ikke** en generell workflow-engine.
  De er oppskrifter — deklarative eller AI-tolkede.
- Deklarativt script er primær for produksjon. AI er for
  oppretting, feilhåndtering og drømmemodus.
- Triggere evalueres av vaktmesteren, utførelse av
  vaktmesteren (script) eller bot (fritekst/drøm).
- Brukeren begrenses aldri til kjente verktøy. Manglende
  funksjonalitet fanges opp som feature requests.

## 13. Komponenter

| Feature | Rolle |
|---------|-------|
| Vaktmesteren | Evaluerer triggere, parser/utfører scripts, dispatcher til bot |
| @bot | Tolker fritekst-instruksjoner, utfører med function calling |
| CLI-verktøy | Gjør det faktiske arbeidet |
| Arbeidstavlen | Work items opprettes ved feil |
| Responskvalitet | Intelligence/effort per orkestrering |

## 14. Seed-orkestreringer

Fem standard-orkestreringer leveres som eksempler og utgangspunkt:

| Orkestrering | Trigger | Hva den gjør |
|---|---|---|
| **Podcast: opptak → publisering** | `communication.ended` + podcast + audio | Transkriberer, oppsummerer, genererer RSS |
| **Publisering: artikkel → render → RSS** | `node.published` + publishing | Rendrer HTML, indeks, RSS |
| **AI-beriking: foreslå koblinger** | `node.created` + content | Foreslår emne-edges via AI |
| **Planlagt publisering** | `scheduled.due` + publishing | Rendrer og genererer RSS ved planlagt tid |
| **Podcast: oppsummering → lyd (TTS)** | `cascade` (fra podcast-pipeline) | Leser oppsummering som lyd |

Podcast-pipeline → TTS er koblet med `triggers`-edge og demonstrerer
kaskade-mønsteret. Se `migrations/025_seed_orchestrations.sql`.

### Flerords-verb

Kompilatoren støtter flerords-verb (f.eks. "generer feed", "les opp",
"foreslå koblinger"). Parseren splitter alltid ved første mellomrom,
men kompilatoren prøver å sette verb + begynnelsen av objekt sammen
for å matche alias. "generer feed for samlingen" matcher alias
"generer feed" i synops-rss, med "for samlingen" som objekt.

## 15. Bygger på
- `docs/retninger/unix_filosofi.md` — CLI-verktøy som byggeklosser
- `docs/retninger/interaksjonsmodell.md` — drag-and-drop for observes-edge
- `docs/concepts/arbeidstavlen.md` — @bot, work items ved feil
- `docs/infra/robusthet.md` — function calling, fallback
- `docs/features/responskvalitet.md` — intelligence/effort-nivåer
