CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
README.md392 linesDownload Raw Back to daemon-ui
1# Daemon UI SDK — Developer Guide2 3The `@qwen-code/sdk/daemon` subpath ships shared UI primitives for daemon4clients. The current adoption target is web chat and web terminal; native local5TUI, channel, and IDE integrations keep their existing default paths while the6daemon UI contract stabilizes. This guide covers the API surface introduced by7PR #4353 (the unified follow-up to PR #4328's shared UI transcript layer).8 9## Three-layer model10 11```12Daemon SSE wire (NDJSON envelopes)13   │14   ▼15normalizeDaemonEvent(envelope) → DaemonUiEvent[]16   │17   ▼18reduceDaemonTranscriptEvents(state, events) → DaemonTranscriptState19   │                                            { blocks, currentToolCallId,20   │                                              approvalMode, toolProgress, ... }21   ▼22daemonBlockToMarkdown(block) / ToHtml / ToPlainText  ← your renderer plugs here23```24 25- **Normalizer**: takes raw daemon SSE envelopes, returns typed UI events26- **Reducer**: accumulates events into a transcript state machine27- **Render helpers**: project state blocks to renderable strings28 29## Quick start30 31```ts32import {33  DaemonSessionClient,34  createDaemonTranscriptStore,35  normalizeDaemonEvent,36  daemonBlockToMarkdown,37  selectCurrentTool,38  selectApprovalMode,39} from '@qwen-code/sdk/daemon';40 41const session = await DaemonSessionClient.createOrAttach(client, {42  workspaceCwd,43});44const store = createDaemonTranscriptStore();45 46for await (const envelope of session.events({ signal })) {47  const events = normalizeDaemonEvent(envelope, {48    clientId: session.clientId,49    suppressOwnUserEcho: true,50  });51  store.dispatch(events);52}53 54// Read state from any subscriber55store.subscribe(() => {56  const state = store.getSnapshot();57  const currentTool = selectCurrentTool(state);58  const mode = selectApprovalMode(state);59  const markdown = state.blocks.map(daemonBlockToMarkdown).join('\n\n');60  myRenderer.render({ markdown, currentTool, mode });61});62```63 64## Event taxonomy (28+ types)65 66`DaemonUiEvent` is a discriminated union of all UI-facing events:67 68### Chat-stream events69 70| Event                        | When                                                  |71| ---------------------------- | ----------------------------------------------------- |72| `user.text.delta`            | User message chunk arrives from daemon                |73| `assistant.text.delta`       | Assistant streaming chunk                             |74| `assistant.done`             | Prompt completion (from sendPrompt resolve)           |75| `thought.text.delta`         | Agent reasoning chunk                                 |76| `tool.update`                | Tool call lifecycle (running / completed / cancelled) |77| `shell.output`               | Shell tool stdout/stderr chunk                        |78| `permission.request`         | Tool needs user authorization                         |79| `permission.resolved`        | Permission decision arrived                           |80| `model.changed`              | Session model switched                                |81| `status` / `debug` / `error` | Status / debug / error blocks                         |82 83### Session-meta events (PR-A)84 85| Event                           | When                                             |86| ------------------------------- | ------------------------------------------------ |87| `session.metadata.changed`      | Session title / display name updated             |88| `session.approval_mode.changed` | Mode toggled (plan / default / yolo / auto-edit) |89| `session.available_commands`    | Slash command list refreshed                     |90 91### Workspace events (PR-A, Wave 3-4)92 93| Event                                  | When                                  |94| -------------------------------------- | ------------------------------------- |95| `workspace.memory.changed`             | QWEN.md / memory file modified        |96| `workspace.agent.changed`              | Sub-agent created / updated / deleted |97| `workspace.tool.toggled`               | Builtin tool enabled / disabled       |98| `workspace.initialized`                | `qwen init` completed                 |99| `workspace.mcp.budget_warning`         | MCP child count approaching cap       |100| `workspace.mcp.child_refused`          | MCP server refused due to budget      |101| `workspace.mcp.server_restarted`       | Manual MCP restart succeeded          |102| `workspace.mcp.server_restart_refused` | Manual restart blocked                |103 104### Auth device-flow events (PR-A, Wave 4 OAuth)105 106`auth.device_flow.{started,throttled,authorized,failed,cancelled}`107 108Each carries the daemon's `deviceFlowId`. Failed events carry a closed-enum109`errorKind` (closed enum — see `KNOWN_DEVICE_FLOW_ERROR_KINDS` exported from `@qwen-code/sdk/daemon` for the canonical list, currently: `expired_token` / `access_denied` / `invalid_grant` / `upstream_error` / `persist_failed` / `not_found_or_evicted`).110 111## Render contract (PR-D)112 113Three projection helpers, one preview helper. All discriminate on `block.kind`114or `preview.kind`:115 116```ts117daemonBlockToMarkdown(block, { sanitizeUrls?, maxFieldLength?, locale? })118daemonBlockToHtml(block, { sanitizer?, ...renderOpts })119daemonBlockToPlainText(block, renderOpts)120daemonToolPreviewToMarkdown(preview, renderOpts)121```122 123### Cookbook: render a transcript to markdown124 125```ts126const markdown = state.blocks127  .map((b) => daemonBlockToMarkdown(b, { sanitizeUrls: true }))128  .join('\n\n');129```130 131### Cookbook: render to sanitized HTML for SSR132 133```ts134import DOMPurify from 'dompurify';135import MarkdownIt from 'markdown-it';136const md = new MarkdownIt();137 138const html = state.blocks139  .map((b) => {140    // Two-stage pipeline: markdown → HTML → DOMPurify141    const rawHtml = md.render(daemonBlockToMarkdown(b));142    return DOMPurify.sanitize(rawHtml);143  })144  .join('\n');145```146 147Or use the built-in conservative HTML renderer (no markdown parsing, just148HTML escape):149 150```ts151const html = state.blocks152  .map((b) => daemonBlockToHtml(b, { sanitizer: DOMPurify.sanitize }))153  .join('\n');154```155 156### Cookbook: copy-paste plain text157 158```ts159const plain = state.blocks.map(daemonBlockToPlainText).join('\n');160navigator.clipboard.writeText(plain);161```162 163## Tool preview taxonomy (13 kinds)164 165| Kind                  | Surface                                           |166| --------------------- | ------------------------------------------------- |167| `ask_user_question`   | Multi-choice question with options                |168| `command`             | Bash-style command + cwd                          |169| `file_diff`           | File edit with oldText/newText or patch           |170| `file_read`           | Path + optional line range                        |171| `web_fetch`           | URL + HTTP method                                 |172| `mcp_invocation`      | MCP server + tool + args summary                  |173| `code_block`          | Language-tagged code snippet                      |174| `search`              | Query + result count + top results                |175| `tabular`             | Columns + rows (capped at 50, truncation flagged) |176| `image_generation`    | Prompt + optional thumbnail URL                   |177| `subagent_delegation` | Agent name + task                                 |178| `key_value`           | Generic label/value rows                          |179| `generic`             | Fallback summary                                  |180 181Each has a `daemonToolPreviewToMarkdown` projection. Custom renderers can182dispatch on `preview.kind` for rich per-type display (file diff with183syntax highlighting, MCP server badge, image thumbnail, etc.).184 185## State selectors (PR-E)186 187```ts188selectCurrentTool(state); // → DaemonToolTranscriptBlock | undefined189selectApprovalMode(state); // → 'plan' | 'default' | 'auto-edit' | 'yolo' | undefined190selectToolProgress(state, toolCallId); // → { ratio?, step? } | undefined191selectPendingPermissionBlocks(state); // → ReadonlyArray<DaemonPermissionTranscriptBlock>192selectTranscriptBlocks(state); // → ReadonlyArray<DaemonTranscriptBlock>193selectTranscriptBlocksOrderedByEventId(state); // sorted by daemon-monotonic id194 195// PR-K — sub-agent nesting196selectSubagentChildBlocks(state, parentToolCallId); // direct children only197isSubagentChildBlock(block); // type guard: was this tool invoked inside a sub-agent?198```199 200`currentToolCallId` is automatically maintained by the reducer:201 202- Set when a tool enters in-flight status (`running` / `in_progress` / `pending` / `confirming`)203- Cleared when tool enters terminal status (`completed` / `failed` / `cancelled` / etc.)204- Unknown statuses leave it untouched (forward-compat)205 206## Cancellation propagation (PR-E)207 208When `assistant.done.reason === 'cancelled'`, the reducer walks every209in-flight tool block and force-sets its status to `'cancelled'`. Daemon210does not guarantee a terminal `tool_call_update` for every in-flight211tool when the parent prompt is cancelled — this propagation prevents UI212spinners from spinning forever.213 214Sub-agent children are cancelled together with their parent because215cancellation iterates every in-flight tool block in `toolBlockByCallId`,216not just the current pointer.217 218## Sub-agent nesting (PR-K)219 220When the main agent delegates to a sub-agent (the `Task` tool, or221equivalent), the daemon stamps `parentToolCallId` and `subagentType` on222the **child** tool calls via `tool_call._meta`. The reducer reads both223and:224 225- Mirrors `parentToolCallId` + `subagentType` onto226  `DaemonToolTranscriptBlock`227- Resolves `parentBlockId` (the parent's transcript block `id`) when the228  parent block is already in state; otherwise leaves it `undefined` and229  back-fills when the parent block later appears230 231Out-of-order arrival (child before parent) is handled transparently. A232child whose parent gets trimmed by `maxBlocks` keeps `parentToolCallId`233for selector queries, but `parentBlockId` is nulled (the dangling id234would no longer resolve via `blockIndexById`).235 236```ts237import {238  selectSubagentChildBlocks,239  isSubagentChildBlock,240} from '@qwen-code/sdk/daemon';241 242// Render a parent tool block, then walk children:243function renderToolBlock(state, block) {244  if (block.kind !== 'tool') return renderOther(block);245  const children = selectSubagentChildBlocks(state, block.toolCallId);246  return (247    <ToolBlock block={block}>248      {children.length > 0 && (249        <Indent>250          {children.map((c) => renderToolBlock(state, c))}251        </Indent>252      )}253    </ToolBlock>254  );255}256 257// Or filter top-level vs. nested at render time:258const topLevel = state.blocks.filter((b) => !isSubagentChildBlock(b));259```260 261`selectSubagentChildBlocks` returns **direct** children only. Walk262recursively to render nested sub-agents (a sub-agent inside a263sub-agent). Daemon does not emit cycles, but renderers walking up via264`parentBlockId` should still detect them defensively (e.g., depth cap or265visited set).266 267Self-references (`parentToolCallId === toolCallId`) are dropped by the268normalizer before reaching the reducer.269 270## Time semantics (PR-B)271 272```ts273interface DaemonTranscriptBlockBase {274  eventId?: number; // PRIMARY sort key — daemon-monotonic275  serverTimestamp?: number; // PREFERRED display — daemon-authoritative276  clientReceivedAt: number; // FALLBACK — local clock277  createdAt: number; // @deprecated alias for clientReceivedAt278}279```280 281**Always sort by `eventId`** (use `selectTranscriptBlocksOrderedByEventId`)282when displaying long sessions. The daemon-monotonic cursor is preserved283across SSE replay-after-reconnect; client clocks are not.284 285**Always format display timestamps from `serverTimestamp`** (with286fallback to `clientReceivedAt`). Multiple clients viewing the same session287see the same "5 minutes ago" only when both read from the daemon clock.288 289```ts290import { formatBlockTimestamp } from '@qwen-code/sdk/daemon';291 292const label = formatBlockTimestamp(block, {293  locale: 'zh-CN',294  timeZone: 'Asia/Shanghai',295  timeStyle: 'short',296});297```298 299## Adapter conformance (PR-G)300 301Validate your adapter projects the SDK's reference corpus to semantically302equivalent output:303 304```ts305import { runAdapterConformanceSuite } from '@qwen-code/sdk/daemon';306 307it('my adapter conforms to daemon UI corpus', () => {308  const result = runAdapterConformanceSuite({309    reduce: (events) => myReducer(events),310    renderToText: (state) => myRenderer(state),311  });312  expect(result.failed).toEqual([]);313});314```315 316The fixture corpus (`DAEMON_UI_CONFORMANCE_FIXTURES`) covers chat, tool317lifecycle, file edits, MCP, permissions, MCP budget warning, cancellation,318malformed payload redaction, OAuth, command updates, and sub-agent319nesting. (Count is derivable at runtime — read320`DAEMON_UI_CONFORMANCE_FIXTURES.length`.)321 322**Format-agnostic** — your adapter can render to ANSI / HTML / markdown /323JSX; the framework only checks semantic content via `expectedContains` and324`expectedAbsent`.325 326## Error categorization (PR-A)327 328`DaemonUiErrorEvent.errorKind` is a closed-enum propagated from the329daemon's typed-error taxonomy (when the daemon stamps it):330 331```ts332import type { DaemonErrorKind } from '@qwen-code/sdk/daemon';333// 'missing_binary' | 'blocked_egress' | 'auth_env_error' | 'init_timeout'334// | 'protocol_error' | 'missing_file' | 'parse_error' | 'budget_exhausted'335```336 337Renderers should branch on `errorKind` for actionable affordances:338 339```ts340function errorAffordance(errorKind?: DaemonErrorKind): React.ReactNode {341  switch (errorKind) {342    case 'auth_env_error': return <button>Re-authenticate</button>;343    case 'missing_file':   return <button>Choose file</button>;344    case 'blocked_egress': return <span>Network blocked — check proxy</span>;345    default:               return null;346  }347}348```349 350## Tool provenance dispatch (PR-A)351 352`DaemonUiToolUpdateEvent.provenance` is a closed-enum (`builtin` / `mcp` /353`subagent` / `unknown`). With `serverId?: string` when `mcp`. Use it for354icon dispatch and badging:355 356```ts357function toolIcon(event: DaemonUiToolUpdateEvent): React.ReactNode {358  switch (event.provenance) {359    case 'mcp':      return <McpIcon server={event.serverId} />;360    case 'subagent': return <SubagentIcon />;361    case 'builtin':  return <BuiltinIcon name={event.toolName} />;362    default:         return <GenericIcon />;363  }364}365```366 367The SDK has a `mcp__<server>__<tool>` naming heuristic fallback — even368when daemon doesn't explicitly stamp provenance, MCP tools are detectable.369 370## Forward-compat principles371 372Every layer in the daemon UI SDK follows the **forward-compat principle**:373unknown values do NOT throw; they degrade gracefully.374 375- Unknown daemon event types → `debug` event with the raw type name376- Unknown tool status → `currentToolCallId` left untouched (no clear)377- Unknown error kind → `errorKind` undefined (renderer falls back to text)378- Missing serverTimestamp → falls back to `clientReceivedAt`379- Unrecognized preview shape → `generic` kind with `summary`380 381This means **SDK can ship ahead of daemon emission**. PR-A's tool382provenance heuristic, PR-B's three-location timestamp extraction, and383PR-E's unknown-status preservation are all examples of "ready when daemon384sends; safe when it doesn't."385 386## Cross-references387 388- [PR #4328](https://github.com/QwenLM/qwen-code/pull/4328) — base PR with the shared UI transcript layer389- [PR #4353](https://github.com/QwenLM/qwen-code/pull/4353) — this PR (unified completeness follow-up)390- [Issue #3803](https://github.com/QwenLM/qwen-code/issues/3803) — daemon mode proposal391- [Issue #4175](https://github.com/QwenLM/qwen-code/issues/4175) — Mode B v0.16 implementation tracker392 
basant307/AI_Governance_Project · CoolFace