# Agentenkommunikation — Erfassung, Ansicht, Auswertung

> **Bauplan — noch nicht gebaut.** Die fünf Phasen sind in der [Roadmap](Roadmap.md) 3.3
> einzeln eingeordnet; die Tiefenbegrenzung (B8) ist dort Punkt 4 der Reihenfolge.
> Die Ansicht aus Phase 4 steht bereits und wartet auf den Speicher aus Phase 2.

Ziel: Die **gesamte** Kommunikation zwischen Agenten wird erfasst, ist im WhatsApp-Stil
paarweise nachlesbar und lässt sich von einem Agenten automatisiert auswerten — um zu
finden, wo die Zusammenarbeit klemmt.

Abgegrenzt davon: Rocket.Chat (siehe [RocketChat-Nextcloud-Konzept](RocketChat-Nextcloud-Konzept.md))
trägt **ausschließlich** das, was ein Mensch wissen soll. Interne Absprachen der Agenten
gehen dort nie hin.

Aufbauend auf [Audit-Konzept](Audit-Konzept.md) (A3) und [Taskboard-Konzept](Taskboard-Konzept.md) (A1).

---

## 1 — Beschlüsse (August 2026)

| # | Beschluss |
|---|---|
| 1 | **Korrelation**: `ParentRunId` + `RootRunId` in Audit-Log und Receipts. Eine Delegationskette wird damit zu einer Abfrage. |
| 2 | **Nachrichtenspeicher**: eigene Tabelle `AgentMessages`, die alle Wege gleich behandelt und beide Richtungen festhält. |
| 3 | **Rocket.Chat bleibt außen vor** — kein Spiegeln der Agentenkommunikation dorthin. Ein Gruppenchat mit allem drin wäre unlesbar. |
| 4 | **Ansicht**: Paar auswählen (Agent A / Agent B), Verlauf im Chat-Stil scrollen. |
| 5 | **Auswertung**: ein Analyse-Tool, das ein dafür vorgesehener Agent bekommt. |

---

## 2 — AgentComm behalten oder durch das Taskboard ersetzen?

Das war die offene Frage. Der Befund zuerst, die Empfehlung danach.

### 2.1 Was heute passiert

`AgentComm.send_message` ist ein **synchroner Aufruf**: A ruft, `SendMessageAsync` startet
`ChatAsync(B)`, wartet auf den vollständigen Lauf von B und gibt dessen Schlussnachricht
als Tool-Ergebnis an A zurück. Drei Eigenschaften folgen daraus:

- **Es gibt keine Tiefenbegrenzung** (B8). A→B→A→B… läuft, bis ein Timeout greift.
- **Es kann echt verklemmen.** Seit B2 serialisiert ein Gate je Agent alle Läufe. A hält
  sein Gate, während es auf B wartet. Ruft B nun `send_message(A)`, wartet B auf As Gate —
  das A hält, während es auf B wartet. Das löst nur der Timeout auf. Der Selbstaufruf
  A→A wurde damals abgefangen, der Zweierzyklus nicht.
- **Der Fehler ist teuer**: Jeder Hop ist ein vollständiger, bezahlter Lauf.

### 2.2 Was ein Task nicht kann

Trotzdem ist „einfach alles über Tasks" nicht ohne Verlust. Zwei Dinge kann der
asynchrone Weg strukturell nicht:

- **Antwort im selben Lauf.** Bei `send_message` kommt die Antwort als Tool-Ergebnis
  zurück, und A arbeitet damit sofort weiter. Über einen Task endet As Lauf; die Antwort
  kommt später als neuer Weckvorgang, und A muss seinen Gedankengang neu aufnehmen. Für
  eine Rückfrage sind das **zwei Läufe statt einem** — der asynchrone Weg ist hier also
  nicht nur langsamer, sondern *teurer*.
- **Antwortzeit.** Der Scanner tickt im Minutentakt. Eine Rückfrage „hast du die Datei
  schon abgelegt?" braucht damit im Mittel eine halbe Minute plus den Lauf des anderen.

### 2.3 Empfehlung: nach Zweck trennen, nicht beides parallel führen

Der Fehler in der jetzigen Lage ist nicht, dass es zwei Mechanismen gibt — es ist, dass
**beide dasselbe können**. Ein Agent kann Arbeit sowohl per Task delegieren als auch per
`send_message` „mal eben" abschieben, und der zweite Weg ist der gefährliche.

Vorschlag:

> **Delegation gehört ausschließlich ins Taskboard.** `AgentComm` verliert diese Rolle
> vollständig — kein „mach du mal", kein Auftrag, kein Arbeitspaket.
>
> **Die kurze Rückfrage bleibt**, aber als eigenes, eng gefasstes Werkzeug: `ask_agent`.

`ask_agent` mit harten Grenzen:

| Grenze | Begründung |
|---|---|
| **Tiefe 1** — wer gerade eine Rückfrage *beantwortet*, darf selbst keine stellen | Beendet die Rekursion an der Wurzel. Prüfbar, sobald `ParentRunId` steht (Punkt 1) — die Synergie ist der Grund, warum das jetzt fast umsonst zu haben ist |
| **Zyklusprüfung** — Ziel darf nicht im aktuellen Aufrufpfad liegen | Schließt den Deadlock aus 2.1 aus, statt auf den Timeout zu hoffen |
| **Kurzer Timeout + kleines Schrittbudget** (z. B. 60 s, 5 Schritte) | Eine Rückfrage, die fünf Schritte braucht, war keine Rückfrage, sondern ein Auftrag |
| **Eigene Beschreibung im Prompt**: „für kurze Fragen an einen Kollegen, nicht um Arbeit abzugeben" | Der häufigste Missbrauch ist der falsche Griff, nicht die böse Absicht |

Damit gibt es weiterhin zwei Wege, aber sie überschneiden sich nicht mehr: Der eine ist
ein Auftrag (dauerhaft, nachvollziehbar, mit Abnahme), der andere eine Frage
(flüchtig, sofort, begrenzt).

**Die Gegenposition, fairerweise:** Man kann `AgentComm` auch ersatzlos streichen und die
Rückfrage über einen Task mit hoher Priorität abbilden. Das wäre die konsequentere
Umsetzung des „alles ist ein Task"-Prinzips und spart ein Tool. Der Preis sind die zwei
Läufe je Rückfrage und die Minute Wartezeit. **Meine Empfehlung ist die Trennung**, weil
Rückfragen im Mehr-Agenten-Betrieb häufig sind und der Aufpreis sich dann summiert — aber
das ist eine Abwägung, keine technische Notwendigkeit.

`AgentSpawn` geht in beiden Varianten im Taskboard auf (`assignee: @new:<agent>` ist genau
das) und wird zurückgebaut.

---

## 3 — Korrelation: `ParentRunId` und `RootRunId`

Heute erzeugt jeder Lauf eine frische `runId`; der Lauf des Empfängers weiß nichts vom
Lauf des Absenders. Eine Kette A→B→C ist deshalb nur über Zeitstempel zu erraten.

Zwei Felder auf `AuditEntry` und `RunReceipt`:

- **`ParentRunId`** — der Lauf, aus dem dieser hervorging. `null` bei einem Lauf, den ein
  Mensch oder der Scanner auslöst.
- **`RootRunId`** — die Wurzel der Kette. Das ist faktisch die **Vorgangs-Id**: Alles, was
  aus einer Anweisung entstand, trägt denselben Wert.

Regeln:

- **Die Engine stempelt.** Wie beim Audit gilt: Herkunft wird nie vom Agenten behauptet.
- Ein Lauf ohne Vorgänger ist seine eigene Wurzel (`RootRunId = RunId`).
- Weitergereicht wird über die Aufrufstellen, an denen ein Lauf einen anderen auslöst:
  `ask_agent`, Task-Dispatch, Staging-Folgetask.
- **Migration**: Bestandszeilen bekommen `RootRunId = RunId` und `ParentRunId = NULL`.
  Das ist nicht rückwirkend korrekt, aber ehrlich — alte Ketten bleiben unbekannt, statt
  falsch zusammengesetzt zu werden.

Der Nutzen reicht über die Analyse hinaus: Was eine Delegationskette insgesamt gekostet
hat, ist danach ein `SUM` über `RunReceipts` gruppiert nach `RootRunId`. Damit fällt ein
Teil von C7 nebenbei ab.

---

## 4 — Der Nachrichtenspeicher

### 4.1 Tabelle

Neue Tabelle in der Instanz-DB, neben `AuditLog` und `RunReceipts`:

```sql
CREATE TABLE IF NOT EXISTS AgentMessages (
    Id          INTEGER PRIMARY KEY AUTOINCREMENT,
    PairKey     TEXT NOT NULL,   -- sortiertes Paar: "agentA|agentB"
    FromAgentId TEXT NOT NULL,
    ToAgentId   TEXT NOT NULL,
    Channel     TEXT NOT NULL,   -- ask | task | task_comment | task_result
    Direction   TEXT NOT NULL,   -- request | response
    Content     TEXT NOT NULL,   -- vollständig, nach Scrubbing
    RunId       TEXT NOT NULL,
    ParentRunId TEXT,
    RootRunId   TEXT NOT NULL,
    TaskId      TEXT,
    Status      TEXT NOT NULL,   -- delivered | failed | timeout | denied
    OccurredAt  TEXT NOT NULL
);
CREATE INDEX IX_AgentMessages_Pair ON AgentMessages(PairKey, OccurredAt);
CREATE INDEX IX_AgentMessages_Root ON AgentMessages(RootRunId, OccurredAt);
```

Dazu ein FTS5-Index auf `Content` — dieselbe Technik wie beim geplanten Historien-Umzug
(K6), damit die Suche in der Ansicht und im Analyse-Tool nicht über `LIKE` läuft.

### 4.2 Die Entscheidungen dahinter

**`PairKey` als sortiertes Paar.** Die geforderte Ansicht („A und B auswählen, scrollen")
wird damit zu `WHERE PairKey = ? ORDER BY OccurredAt` — eine Abfrage auf einem Index,
unabhängig davon, wer gerade wen anspricht.

**Beide Richtungen als eigene Zeilen.** Eine Rückfrage erzeugt zwei Zeilen (`request`
A→B, `response` B→A) mit derselben `RootRunId`. Nur so entsteht ein Verlauf, der sich wie
ein Chat liest. Das Audit-Log kann das nicht leisten: Es speichert nur `Arguments`, die
Antwort landet dort nirgends.

**Inhalt ungekappt.** Das Audit kappt bei 4.000 Zeichen — richtig, denn es dient der
Nachvollziehbarkeit. Für die Auswertung braucht es den vollen Text. Gekappt wird erst
dort, wo Text in einen LLM-Kontext zurückfließt (Abschnitt 6).

**Alle Wege in einer Tabelle.** `ask_agent`, Task-Delegation, `task_comment` und das
Ergebnis eines Tasks landen im selben Format. Sonst müsste die Auswertung drei Quellen
zusammensuchen — und genau daran scheitert sie heute.

**Fan-out statt Sammelzeile.** Eine Nachricht an mehrere Empfänger wird zu mehreren
Zeilen. Etwas redundant, dafür bleibt jede Zeile paarweise auswertbar.

### 4.3 Wer schreibt

Ein `IAgentMessageLog` im Core, aufgerufen an genau den Stellen, an denen eine Nachricht
eine Agentengrenze überschreitet:

| Aufrufstelle | Zeilen |
|---|---|
| `AgentEngine` — `ask_agent` | `request` beim Absenden, `response` beim Rückgabewert (auch bei Fehler/Timeout, mit passendem `Status`) |
| Taskboard — `task_create` mit fremdem Assignee | `request` |
| Taskboard — `task_comment` | `request` bzw. `response`, je nach Richtung |
| Taskboard — Task abgeschlossen/geblockt | `response` mit Ergebnis oder Blocker-Grund |

Drei bis vier Stellen, alle im Core. Kein Tool schreibt selbst — sonst könnte ein Agent
seine eigene Kommunikationsakte färben.

**Fehlschläge werden mitgeschrieben.** Eine nicht zugestellte Nachricht ist für die
Analyse wertvoller als eine erfolgreiche.

### 4.4 Was hier nicht hineingehört

- **Mensch↔Agent-Chat.** Das ist die Chat-Historie, ein anderer Gegenstand mit anderem
  Umzugsplan (K6). *Ausnahme mit gutem Preis-Leistungs-Verhältnis:* Tasks mit
  `assignee: @human` durchlaufen dieselben Aufrufstellen — man kann sie als Paar
  (Agent, `@human`) mitschreiben und bekommt die Ansicht dafür geschenkt. Vorschlag: ja,
  aber als Nachzügler, nicht als Teil der ersten Fassung.
- **Tool-Aufrufe.** Die stehen im Audit-Log und gehören nicht in einen Gesprächsverlauf.
- **Rocket.Chat-Nachrichten.** Anderer Gegenstand, andere Vertrauensgrenze.

### 4.5 Zwei Pflichten

- **Output-Scrubbing vor dem Schreiben.** Nachrichteninhalte enthalten Tool-Ergebnisse.
  Ohne Maskierung bekannter Geheimnisse wird dieser Speicher zur zweiten Fundstelle für
  Zugangsdaten — und über den MySQL-Spiegel (A6) verlässt er sogar die Maschine. Der
  Roadmap-Punkt „Output-Scrubbing" ist damit **Voraussetzung**, nicht Beiwerk.
- **Aufbewahrung.** Die Tabelle wächst unbegrenzt. Ein Instanz-Wert
  (`agentMessageRetentionDays`, 0 = unbegrenzt) plus ein Aufräum-Task gehören von Anfang
  an dazu, nicht erst, wenn die DB groß ist.

---

## 5 — Die Ansicht

Neuer Reiter im Hauptfenster: **Agentenkommunikation**.

**Bedienung**: zwei Auswahlfelder (Agent A, Agent B) — dazu „alle" für einen Agenten, um
zu sehen, mit wem er überhaupt spricht. Zeitraum, Kanalfilter, Freitextsuche.

**Darstellung**: Chat-Stil, A rechts, B links, Zeitstempel, Tagestrenner. Jede Blase
trägt eine kleine Kennzeichnung des Kanals (`Rückfrage` / `Auftrag` / `Kommentar` /
`Ergebnis`) und, wo vorhanden, die anklickbare Task-Id.

**Technisch**: damals über den WebView2-Unterbau von `frm_chat` gedacht. Beides gibt es
seit der Avalonia-Portierung nicht mehr — der Abschnitt ist als Entwurfsstand von damals
zu lesen; die Ansicht wäre heute eine Avalonia-Seite wie `AgentChatsPageView`
(inkl. Virtual-Host-Mapping auf einen lokalen Ordner). Der Verlauf wird als HTML
gerendert. Handgezeichnete Sprechblasen wären dort ein Vielfaches an Aufwand für
ein schlechteres Ergebnis.

**Paging**: die jüngsten ~200 Nachrichten, „ältere laden" nach oben. Ein Paar mit 50.000
Zeilen darf die Oberfläche nicht am Start blockieren.

**Der eigentliche Mehrwert** liegt über dem flachen Verlauf: Ein Klick auf eine Nachricht
zeigt die ganze Kette zu ihrer `RootRunId` — also den Vorgang von der auslösenden
Anweisung bis zum letzten Beitrag, über alle beteiligten Agenten hinweg, mit den Kosten
aus den Receipts. Das ist die Ansicht, die die Frage „warum hat das drei Stunden und
vier Dollar gekostet" tatsächlich beantwortet.

---

## 6 — Automatisierte Auswertung

Ein Tool `AgentCommAnalysis`, das **bewusst nur einem dafür vorgesehenen Agenten**
zugewiesen wird (die Zuweisung je Agent gibt es ohnehin).

| Aktion | Zweck |
|---|---|
| `list_pairs` | Wer spricht mit wem, wie oft, seit wann — der Einstieg |
| `stats` | Kennzahlen je Paar/Kanal/Zeitraum (siehe unten) |
| `read_conversation` | Verlauf eines Paares, gekappt und seitenweise |
| `chain` | Ein kompletter Vorgang über `RootRunId`, inkl. Kosten |
| `search` | Volltext über FTS5 |

**Fragen, die das beantworten soll** — sie sind der Grund für den Schnitt des Schemas:

- Wie viele Delegationen führen zu einem Ergebnis, und wie viele versanden?
- Wie tief werden Ketten, und ab welcher Tiefe steigt die Fehlerquote?
- Welche Paare stellen sich wiederholt dieselbe Rückfrage? (Ein Hinweis auf unklare
  Zuständigkeit oder einen fehlenden Skill — genau die Art Problem, die man sucht.)
- Was kostet ein Vorgang von der Anweisung bis zum Ergebnis?
- Wo häufen sich `failed`/`timeout`?

**Drei Sicherungen**, weil ein Agent hier fremde Kommunikation liest:

1. Ergebnisse werden als `<untrusted_content>` gerahmt. Der Inhalt stammt aus anderen
   Läufen und kann Anweisungen enthalten — auch ohne böse Absicht.
2. Standardmäßig liefert das Tool **Kennzahlen**, Rohtext nur auf ausdrückliche Anfrage
   und mit harter Obergrenze. Ein unbedachtes „lies mir alles vor" ist sonst ein
   Kontext-Überlauf mit Rechnung.
3. Das Tool ist **lesend**. Es gibt keine Schreibaktion.

---

## 7 — Verhältnis zu Rocket.Chat

Klargestellt, weil es die vorherige Überlegung ablöst:

- Agentenkommunikation wird **nicht** nach Rocket.Chat gespiegelt. Ein Raum, in dem jede
  interne Absprache mitläuft, ist nach einer Woche unlesbar und verdeckt genau das, was
  man sehen soll.
- Nach Rocket.Chat geht nur, was ein Mensch wissen soll oder muss — über den
  `ChannelRouter` bzw. eine bewusste Handlung des Agenten.
- Wer den internen Verlauf sehen will, nimmt die Ansicht aus Abschnitt 5. Die ist dafür
  gebaut; ein Gruppenchat ist es nicht.

---

## 8 — Schnitt

| Phase | Inhalt | Abhängigkeit |
|---|---|---|
| **1** | `ParentRunId` + `RootRunId` in `AuditLog`/`RunReceipts`, Migration, Stempelung in der Engine | — |
| **2** | `AgentMessages` + FTS5 + `IAgentMessageLog`, Schreiben an den Aufrufstellen | 1 |
| **3** | `ask_agent` (Tiefe 1, Zyklusprüfung), Rückbau von `AgentComm`/`AgentSpawn` | 1 — die Tiefe kommt aus der Kette |
| **4** | Ansicht (WebView2-Reiter) | 2 |
| **5** | `AgentCommAnalysis`-Tool | 2 |
| **—** | Output-Scrubbing | **vor** 2 |

Phase 1 zuerst, weil Phase 3 die Kette braucht und Phase 2 die Felder mitschreibt. Das
Scrubbing muss vor Phase 2 stehen — sonst legen wir einen Speicher an, der erst
nachträglich bereinigt werden müsste.

Zur Einstufung: Phasen 1, 2, 4 und 5 sind gut spezifizierbar. Phase 3 fasst das
Agent-Gate an, an dem schon einmal ein Deadlock lauerte (B2/B8) — dafür gehören die Tests
aus der [Teststrategie](Teststrategie.md) mit dazu, insbesondere der dort vorgesehene,
bis heute fehlende Fall **A13** (`A→B→A` wird begrenzt statt zu verklemmen).

---

## 9 — Offene Punkte

1. **`ask_agent` behalten oder ersatzlos streichen?** Meine Empfehlung steht in 2.3
   (behalten, eng gefasst) — die Gegenposition ist dort ebenfalls notiert.
2. **`@human`-Tasks mitschreiben?** Gibt die Paar-Ansicht auch für Mensch↔Agent, fast
   ohne Zusatzaufwand. Vorschlag: ja, aber nach Phase 4.
3. **Aufbewahrungsdauer** — Vorgabewert? (Vorschlag: unbegrenzt, bis der Spiegel aus A6
   steht; dann 180 Tage lokal.)
4. **Wer bekommt `AgentCommAnalysis`?** Ein eigener Analyse-Agent oder ein bestehender?
