Skip to Content
EntwicklerhandbuchDaemon UIDaemon UI SDK – Entwicklerhandbuch

Daemon UI SDK – Entwicklerhandbuch

Der Subpfad @qwen-code/sdk/daemon liefert gemeinsame UI-Primitive für Daemon-Clients. Das aktuelle Ziel ist Web-Chat und Web-Terminal; native lokale TUI-, Kanal- und IDE-Integrationen behalten ihre bestehenden Standardpfade, während der Daemon-UI-Vertrag stabilisiert wird. Dieser Leitfaden deckt die API-Oberfläche ab, die mit PR #4353 (dem vereinheitlichten Nachfolger des gemeinsamen UI-Transkriptlayers von PR #4328) eingeführt wurde.

Drei-Schichten-Modell

Daemon SSE Wire (NDJSON Umschläge) normalizeDaemonEvent(envelope) → DaemonUiEvent[] reduceDaemonTranscriptEvents(state, events) → DaemonTranscriptState │ { blocks, currentToolCallId, │ approvalMode, toolProgress, ... } daemonBlockToMarkdown(block) / ToHtml / ToPlainText ← hier steckst du deinen Renderer an
  • Normalisierer: Nimmt rohe Daemon-SSE-Umschläge entgegen, gibt typisierte UI-Ereignisse zurück
  • Reducer: Akkumuliert Ereignisse in eine Transkriptstatusmaschine
  • Render-Helfer: Projiziert Statusblöcke in darstellbare Zeichenketten

Schnellstart

import { DaemonSessionClient, createDaemonTranscriptStore, normalizeDaemonEvent, daemonBlockToMarkdown, selectCurrentTool, selectApprovalMode, } from '@qwen-code/sdk/daemon'; const session = await DaemonSessionClient.createOrAttach(client, { workspaceCwd, }); const store = createDaemonTranscriptStore(); for await (const envelope of session.events({ signal })) { const events = normalizeDaemonEvent(envelope, { clientId: session.clientId, suppressOwnUserEcho: true, }); store.dispatch(events); } // Zustand von einem beliebigen Abonnenten auslesen store.subscribe(() => { const state = store.getSnapshot(); const currentTool = selectCurrentTool(state); const mode = selectApprovalMode(state); const markdown = state.blocks.map(daemonBlockToMarkdown).join('\n\n'); myRenderer.render({ markdown, currentTool, mode }); });

Ereignistaxonomie (28+ Typen)

DaemonUiEvent ist eine diskriminierte Vereinigung aller UI-Ereignisse:

Chat-Stream-Ereignisse

EreignisWann
user.text.deltaEin Teil einer Benutzernachricht kommt vom Daemon an
assistant.text.deltaGestreamter Teil der Antwort des Assistenten
assistant.doneAbschluss der Eingabeaufforderung (aus dem Auflösen von sendPrompt)
thought.text.deltaTeil der Überlegungen des Agenten
tool.updateLebenszyklus eines Toolaufrufs (läuft / abgeschlossen / abgebrochen)
shell.outputTeil von stderr/stdout des Shell-Tools
permission.requestTool benötigt Benutzerautorisierung
permission.resolvedEntscheidung über die Berechtigung liegt vor
model.changedSitzungsmodell gewechselt
status / debug / errorStatus-/Debug-/Fehlerblöcke

Sitzungs-Meta-Ereignisse (PR-A)

EreignisWann
session.metadata.changedSitzungstitel / Anzeigename aktualisiert
session.approval_mode.changedModus umgeschaltet (Plan / Standard / YOLO / Auto-Bearbeiten)
session.available_commandsListe der Schrägstrichbefehle aktualisiert

Arbeitsbereichsereignisse (PR-A, Wave 3-4)

EreignisWann
workspace.memory.changedQWEN.md / Speicherdatei verändert
workspace.agent.changedUnter-Agent erstellt / aktualisiert / gelöscht
workspace.tool.toggledEingebautes Tool aktiviert / deaktiviert
workspace.initializedqwen init abgeschlossen
workspace.mcp.budget_warningMCP-Kindanzahl nähert sich dem Limit
workspace.mcp.child_refusedMCP-Server aufgrund des Budgets abgelehnt
workspace.mcp.server_restartedManueller MCP-Neustart erfolgreich
workspace.mcp.server_restart_refusedManueller Neustart blockiert

Authentifizierungs-Device-Flow-Ereignisse (PR-A, Wave 4 OAuth)

auth.device_flow.{started,throttled,authorized,failed,cancelled}

Jedes trägt die deviceFlowId des Daemons. Fehlgeschlagene Ereignisse enthalten eine geschlossene Aufzählung errorKind (closed enum – siehe KNOWN_DEVICE_FLOW_ERROR_KINDS, exportiert aus @qwen-code/sdk/daemon für die kanonische Liste, derzeit: expired_token / access_denied / invalid_grant / upstream_error / persist_failed / not_found_or_evicted).

Render-Vertrag (PR-D)

Drei Projektionshelfer, ein Vorschauhelfer. Alle unterscheiden nach block.kind oder preview.kind:

daemonBlockToMarkdown(block, { sanitizeUrls?, maxFieldLength?, locale? }) daemonBlockToHtml(block, { sanitizer?, ...renderOpts }) daemonBlockToPlainText(block, renderOpts) daemonToolPreviewToMarkdown(preview, renderOpts)

Kochbuch: Ein Transkript als Markdown rendern

const markdown = state.blocks .map((b) => daemonBlockToMarkdown(b, { sanitizeUrls: true })) .join('\n\n');

Kochbuch: In bereinigtes HTML für SSR rendern

import DOMPurify from 'dompurify'; import MarkdownIt from 'markdown-it'; const md = new MarkdownIt(); const html = state.blocks .map((b) => { // Zwei-Stufen-Pipeline: Markdown → HTML → DOMPurify const rawHtml = md.render(daemonBlockToMarkdown(b)); return DOMPurify.sanitize(rawHtml); }) .join('\n');

Oder verwenden Sie den eingebauten konservativen HTML-Renderer (kein Markdown-Parsing, nur HTML-Escape):

const html = state.blocks .map((b) => daemonBlockToHtml(b, { sanitizer: DOMPurify.sanitize })) .join('\n');

Kochbuch: Klartext kopieren und einfügen

const plain = state.blocks.map(daemonBlockToPlainText).join('\n'); navigator.clipboard.writeText(plain);

Tool-Vorschau-Taxonomie (13 Arten)

ArtDarstellung
ask_user_questionMultiple-Choice-Frage mit Optionen
commandBash-ähnlicher Befehl + Arbeitsverzeichnis
file_diffDateibearbeitung mit oldText/newText oder Patch
file_readPfad + optionaler Zeilenbereich
web_fetchURL + HTTP-Methode
mcp_invocationMCP-Server + Tool + Argumente-Zusammenfassung
code_blockSprachgetaggtes Code-Snippet
searchAbfrage + Ergebnisanzahl + Top-Ergebnisse
tabularSpalten + Zeilen (begrenzt auf 50, Kürzung markiert)
image_generationPrompt + optionale Miniaturbild-URL
subagent_delegationAgentenname + Aufgabe
key_valueGenerische Label/Wert-Zeilen
genericFallback-Zusammenfassung

Jede hat eine daemonToolPreviewToMarkdown-Projektion. Benutzerdefinierte Renderer können auf preview.kind verzweigen, um eine umfangreiche typabhängige Darstellung zu ermöglichen (Dateidiff mit Syntax-Highlighting, MCP-Server-Badge, Bild-Miniaturansicht usw.).

Zustandsselektoren (PR-E)

selectCurrentTool(state); // → DaemonToolTranscriptBlock | undefined selectApprovalMode(state); // → 'plan' | 'default' | 'auto-edit' | 'yolo' | undefined selectToolProgress(state, toolCallId); // → { ratio?, step? } | undefined selectPendingPermissionBlocks(state); // → ReadonlyArray<DaemonPermissionTranscriptBlock> selectTranscriptBlocks(state); // → ReadonlyArray<DaemonTranscriptBlock> selectTranscriptBlocksOrderedByEventId(state); // sorted by daemon-monotonic id // PR-K — Sub-Agenten-Verschachtelung selectSubagentChildBlocks(state, parentToolCallId); // direct children only isSubagentChildBlock(block); // type guard: was this tool invoked inside a sub-agent?

currentToolCallId wird automatisch vom Reducer verwaltet:

  • Wird gesetzt, wenn ein Tool den Status „in Bearbeitung“ erreicht (running / in_progress / pending / confirming)
  • Wird gelöscht, wenn das Tool einen Endstatus erreicht (completed / failed / cancelled / usw.)
  • Unbekannte Status lassen es unverändert (vorwärtskompatibel)

Abbruchweitergabe (PR-E)

Wenn assistant.done.reason === 'cancelled', durchläuft der Reducer jeden in Bearbeitung befindlichen Tool-Block und setzt dessen Status erzwingend auf 'cancelled'. Der Daemon garantiert keine abschließende tool_call_update für jedes laufende Tool, wenn die übergeordnete Eingabeaufforderung abgebrochen wird – diese Weitergabe verhindert, dass UI-Ladeanzeigen endlos drehen.

Untergeordnete Sub-Agenten werden zusammen mit ihrem übergeordneten Agenten abgebrochen, da die Abbruchlogik jeden in Bearbeitung befindlichen Tool-Block in toolBlockByCallId durchläuft, nicht nur den aktuellen Zeiger.

Sub-Agenten-Verschachtelung (PR-K)

Wenn der Hauptagent an einen Sub-Agenten delegiert (das Task-Tool oder ein Äquivalent), markiert der Daemon parentToolCallId und subagentType auf den untergeordneten Tool-Aufrufen über tool_call._meta. Der Reducer liest beide und:

  • Spiegelt parentToolCallId + subagentType auf DaemonToolTranscriptBlock
  • Löst parentBlockId (die id des übergeordneten Transkript-Blocks) auf, wenn der übergeordnete Block bereits im Zustand ist; andernfalls bleibt es undefined und wird nachgefüllt, wenn der übergeordnete Block später erscheint

Auftreten in falscher Reihenfolge (Kind vor Eltern) wird transparent behandelt. Ein Kind, dessen Elternteil durch maxBlocks abgeschnitten wird, behält parentToolCallId für Selektorabfragen, aber parentBlockId wird auf null gesetzt (die verwaiste ID würde nicht mehr über blockIndexById aufgelöst werden).

import { selectSubagentChildBlocks, isSubagentChildBlock, } from '@qwen-code/sdk/daemon'; // Ein übergeordnetes Tool-Block rendern, dann Kinder durchlaufen: function renderToolBlock(state, block) { if (block.kind !== 'tool') return renderOther(block); const children = selectSubagentChildBlocks(state, block.toolCallId); return ( <ToolBlock block={block}> {children.length > 0 && ( <Indent> {children.map((c) => renderToolBlock(state, c))} </Indent> )} </ToolBlock> ); } // Oder auf oberster Ebene vs. verschachtelt zur Renderzeit filtern: const topLevel = state.blocks.filter((b) => !isSubagentChildBlock(b));

selectSubagentChildBlocks gibt nur direkte Kinder zurück. Durchlaufen Sie rekursiv, um verschachtelte Sub-Agents darzustellen (ein Sub-Agent in einem Sub-Agent). Der Daemon erzeugt keine Zyklen, aber Renderer, die über parentBlockId nach oben gehen, sollten sie dennoch defensiv erkennen (z. B. Tiefenbegrenzung oder besuchte Menge).

Selbstreferenzen (parentToolCallId === toolCallId) werden vom Normalisierer verworfen, bevor sie den Reducer erreichen.

Zeit-Semantik (PR-B)

interface DaemonTranscriptBlockBase { eventId?: number; // PRIMARY sort key — daemon-monotonic serverTimestamp?: number; // PREFERRED display — daemon-authoritative clientReceivedAt: number; // FALLBACK — local clock createdAt: number; // @deprecated alias for clientReceivedAt }

Sortieren Sie immer nach eventId (verwenden Sie selectTranscriptBlocksOrderedByEventId), wenn Sie lange Sitzungen anzeigen. Der daemon-monotonische Cursor bleibt über SSE-Wiederholung nach Wiederverbindung erhalten; Client-Uhren nicht.

Formatieren Sie Anzeigezeitstempel immer von serverTimestamp (mit Fallback auf clientReceivedAt). Mehrere Clients, die dieselbe Sitzung anzeigen, sehen dieselbe „vor 5 Minuten”-Angabe nur, wenn beide von der Daemon-Uhr lesen.

import { formatBlockTimestamp } from '@qwen-code/sdk/daemon'; const label = formatBlockTimestamp(block, { locale: 'zh-CN', timeZone: 'Asia/Shanghai', timeStyle: 'short', });

Adapter-Konformität (PR-G)

Validieren Sie, dass Ihr Adapter das Referenzkorpus des SDKs semantisch äquivalent ausgibt:

import { runAdapterConformanceSuite } from '@qwen-code/sdk/daemon'; it('my adapter conforms to daemon UI corpus', () => { const result = runAdapterConformanceSuite({ reduce: (events) => myReducer(events), renderToText: (state) => myRenderer(state), }); expect(result.failed).toEqual([]); });

Das Fixture-Korpus (DAEMON_UI_CONFORMANCE_FIXTURES) deckt Chat, Tool-Lebenszyklus, Dateibearbeitungen, MCP, Berechtigungen, MCP-Budgetwarnung, Stornierung, Schwärzung fehlerhafter Nutzdaten, OAuth, Befehlsaktualisierungen und Sub-Agent-Verschachtelung ab. (Die Anzahl ist zur Laufzeit ableitbar – lesen Sie DAEMON_UI_CONFORMANCE_FIXTURES.length.)

Formatsagnostisch – Ihr Adapter kann nach ANSI / HTML / Markdown / JSX rendern; das Framework prüft nur den semantischen Inhalt über expectedContains und expectedAbsent.

Fehlerkategorisierung (PR-A)

DaemonUiErrorEvent.errorKind ist ein geschlossenes Enum, das aus der typisierten Fehlertaxonomie des Daemons weitergegeben wird (wenn der Daemon es stempelt):

import type { DaemonErrorKind } from '@qwen-code/sdk/daemon'; // 'missing_binary' | 'blocked_egress' | 'auth_env_error' | 'init_timeout' // | 'protocol_error' | 'missing_file' | 'parse_error' | 'budget_exhausted'

Renderer sollten auf errorKind verzweigen, um umsetzbare Hilfsmittel bereitzustellen:

function errorAffordance(errorKind?: DaemonErrorKind): React.ReactNode { switch (errorKind) { case 'auth_env_error': return <button>Re-authenticate</button>; case 'missing_file': return <button>Choose file</button>; case 'blocked_egress': return <span>Network blocked — check proxy</span>; default: return null; } }

Tool-Herkunfts-Dispatch (PR-A)

DaemonUiToolUpdateEvent.provenance ist ein geschlossenes Enum (builtin / mcp / subagent / unknown). Mit serverId?: string bei mcp. Verwenden Sie es für die Icon-Dispatch und Badging:

function toolIcon(event: DaemonUiToolUpdateEvent): React.ReactNode { switch (event.provenance) { case 'mcp': return <McpIcon server={event.serverId} />; case 'subagent': return <SubagentIcon />; case 'builtin': return <BuiltinIcon name={event.toolName} />; default: return <GenericIcon />; } }

Das SDK hat einen mcp__<server>__<tool>-Namensheuristik-Fallback – selbst wenn der Daemon keine Herkunft explizit stempelt, sind MCP-Tools erkennbar.

Forward-Compat-Prinzipien

Jede Schicht im Daemon-UI-SDK folgt dem Forward-Compat-Prinzip: unbekannte Werte werfen KEINE Fehler; sie degradieren elegant.

  • Unbekannte Daemon-Ereignistypen → debug-Ereignis mit dem rohen Typnamen
  • Unbekannter Tool-Status → currentToolCallId bleibt unberührt (kein Löschen)
  • Unbekannter Fehlertyp → errorKind undefiniert (Renderer fällt auf Text zurück)
  • Fehlender serverTimestamp → fällt auf clientReceivedAt zurück
  • Nicht erkannte Vorschauform → generic-Art mit summary

Das bedeutet, dass das SDK vor der Daemon-Emission ausgeliefert werden kann. PR-As Tool-Herkunfts-Heuristik, PR-Bs Drei-Stellen-Zeitstempel-Extraktion und PR-Es Erhaltung unbekannter Status sind alles Beispiele für „bereit, wenn der Daemon sendet; sicher, wenn nicht”.

Querverweise

  • PR #4328  — Basis-PR mit der gemeinsamen UI-Transkript-Schicht
  • PR #4353  — dieser PR (vereinheitlichtes Vollständigkeits-Follow-up)
  • Issue #3803  — Daemon-Mode-Vorschlag
  • Issue #4175  — Mode B v0.16-Implementierungs-Tracker
Last updated on