Skip to Content
Guide développeurDaemonClient Daemon du SDK TypeScript

Client Daemon du SDK TypeScript

Vue d’ensemble

packages/sdk-typescript/src/daemon/ est le client daemon du SDK TypeScript. C’est la méthode canonique pour se connecter à un daemon qwen serve en cours d’exécution depuis n’importe quel hôte TypeScript / JavaScript (l’adaptateur TUI du CLI, les backends de bots de canal, le compagnon IDE VS Code, les scripts personnalisés et les backends web côté serveur). Tous les autres adaptateurs en dépendent.

La structure du package est volontairement minimaliste :

FichierSurface
index.tsPoint d’entrée public (barrel) (DaemonClient, DaemonSessionClient, DaemonAuthFlow, parseSseStream, réducteurs d’événements, types).
DaemonClient.tsFacade HTTP/SSE de bas niveau — une méthode par route de qwen-serve-protocol.md.
DaemonSessionClient.tsWrapper limité à une session avec suivi de la relecture SSE.
DaemonAuthFlow.tsAssistant de haut niveau pour le flux d’appareil OAuth (device-flow).
sse.tsparseSseStream (analyseur de tramage NDJSON / SSE).
events.tsasKnownDaemonEvent, reduceDaemonSessionEvent, reduceDaemonAuthEvent (voir 09-event-schema.md).
types.tsDaemonCapabilities, DaemonSession, DaemonEvent, PermissionResponse, PromptResult, types MCP / agent / mémoire / auth.

L’exemple de prise en main se trouve dans ../examples/daemon-client-quickstart.md ; ce document est la référence de l’architecture et du contrat.

Responsabilités

  • Fournir une méthode TypeScript par route HTTP du daemon.
  • Appliquer correctement le bearer token et le X-Qwen-Client-Id sur chaque requête.
  • Composer les timeouts par appel avec l’AbortSignal fourni par l’appelant (sans interrompre les SSE de longue durée).
  • Streamer et analyser les trames SSE en DaemonEvent typés.
  • Suivre le lastSeenEventId par session afin que les reconnexions rejouent correctement.
  • Exposer une surface d’authentification par device-flow qui interroge à des intervalles fournis par le daemon.

Architecture

DaemonClient (DaemonClient.ts)

Constructeur :

new DaemonClient({ baseUrl: string, // default 'http://127.0.0.1:4170' token?: string, fetch?: typeof globalThis.fetch, // injectable for tests fetchTimeoutMs?: number, // 0 = disabled; default DEFAULT_FETCH_TIMEOUT_MS });

Groupes de méthodes (chaque méthode prend un clientId optionnel pour appliquer le X-Qwen-Client-Id) :

GroupeMéthodes
Plumbinghealth(), capabilities(), auth (accesseur lazy DaemonAuthFlow)
SessionscreateOrAttachSession, loadSession, resumeSession, listSessions, closeSession, setSessionMetadata, getSessionContext, getSessionSupportedCommands, setSessionApprovalMode, setSessionModel
Promptingprompt, cancel, heartbeat
ÉvénementssubscribeEvents (générateur SSE), subscribeEventsStream (réponse brute)
PermissionsrespondToPermission, respondToSessionPermission
Snapshots de workspacegetWorkspaceMcp, getWorkspaceSkills, getWorkspaceProviders, getWorkspaceEnv, getWorkspacePreflight
Mutations de workspacewriteWorkspaceMemory, readWorkspaceMemory, rememberWorkspaceMemory, getWorkspaceMemoryRememberTask, forgetWorkspaceMemory, getWorkspaceMemoryForgetTask, dreamWorkspaceMemory, getWorkspaceMemoryDreamTask, listWorkspaceAgents, getWorkspaceAgent, createWorkspaceAgent, updateWorkspaceAgent, deleteWorkspaceAgent, toggleWorkspaceTool, restartMcpServer, initializeWorkspace
FichiersreadFile, readFileBytes, writeFile, editFile, listDirectory, globPaths, statPath
AuthstartDeviceFlow, pollDeviceFlow, cancelDeviceFlow, getAuthStatus

fetchWithTimeout

Chaque requête passe par fetchWithTimeout. Détails critiques :

  • La lecture du body se trouve dans le scope du timer. Les implémentations précédentes effaçaient le timer à l’arrivée des headers ; si un proxy stagnait au milieu du body, await res.json() pouvait bloquer au-delà de fetchTimeoutMs. La forme actuelle passe le code de lecture du body en callback afin que le timer couvre à la fois l’arrivée des headers ET la consommation du body.
  • perCallTimeoutMs permet à un appel unique de surcharger le défaut global du client. L’appelant le plus visible est restartMcpServer : le SDK utilise MCP_RESTART_DEFAULT_TIMEOUT_MS = 330_000 (5 min 30s). Le MCP_RESTART_TIMEOUT_MS propre au daemon est exactement de 300s ; si le client correspondait à cette valeur, un redémarrage se terminant vers 300s pourrait perdre la course pendant que le daemon sérialise et envoie sa réponse structurée, provoquant une TimeoutError faussement positive. Les 30s supplémentaires couvrent la sérialisation, le transfert réseau et le décodage des deux côtés. Les appelants qui ont besoin d’un budget plus strict peuvent passer timeoutMs ; passer 0 désactive le timeout.
  • AbortSignal.any compose le signal fourni par l’appelant avec le signal du timer par appel, de sorte que l’annulation par l’appelant et le timeout par appel interrompent proprement l’opération.
  • AbortController + setTimeout annulable au lieu de AbortSignal.timeout(), afin que les requêtes à résolution rapide ne fuient pas de timers en attente sur la boucle d’événements. Le timer est effacé dans le finally.
  • Les endpoints de streaming (subscribeEvents) contournent le timeout — les SSE de longue durée ne doivent pas être tuées par celui-ci.

DaemonSessionClient (DaemonSessionClient.ts)

Lie une session et suit automatiquement le lastSeenEventId afin que la relecture et la reconnexion SSE fonctionnent sans état supplémentaire de la part de l’appelant.

class DaemonSessionClient { readonly client: DaemonClient; readonly session: DaemonSession; readonly state: DaemonSessionState; private lastSeenEventId: number | undefined; static createOrAttach(client, req?): Promise<DaemonSessionClient>; static load(client, sessionId, req?): Promise<DaemonSessionClient>; static resume(client, sessionId, req?): Promise<DaemonSessionClient>; events(opts?: DaemonSessionSubscribeOptions): AsyncIterable<DaemonEvent>; prompt(req: PromptRequest): Promise<PromptResult>; cancel(): Promise<void>; respondToPermission(...): Promise<PermissionResponse>; setModel(modelServiceId): Promise<SetModelResult>; heartbeat(): Promise<HeartbeatResult>; setMetadata(metadata): Promise<SessionMetadataResult>; close(): Promise<void>; }

events() proxyfie client.subscribeEvents avec resume: true par défaut — il passe le lastSeenEventId suivi afin que les reconnexions rejouent depuis l’endroit où l’abonnement précédent s’est arrêté. Chaque événement émis incrémente le lastSeenEventId.

DaemonAuthFlow (DaemonAuthFlow.ts)

class DaemonAuthFlow { start(opts: { providerId, ... }): Promise<DaemonAuthFlowHandle>; } interface DaemonAuthFlowHandle { deviceFlowId: string; providerId: string; expiresAt: string; verificationUrl: string; userCode: string; awaitCompletion(opts?): Promise<DaemonAuthDeviceFlowState>; cancel(): Promise<void>; }

awaitCompletion() interroge GET /workspace/auth/device-flow/:id à l’intervalle intervalMs fourni par le daemon jusqu’à ce que le flux devienne authorized, failed ou cancelled. Il est construit de manière paresseuse via client.auth afin que les clients qui ne touchent jamais à l’auth n’encourent aucun coût d’allocation.

parseSseStream (sse.ts)

Transforme un Response.body (ReadableStream<Uint8Array>) en AsyncIterable<DaemonEvent>. Gère :

  • Le tramage LF et CRLF.
  • La limite de débordement de buffer (16 MiB) — borne défensive contre un daemon émettant une trame absurdement grande.
  • Le câblage de l’AbortSignal — l’abort ferme le stream et l’itérateur.
  • Les trames de commentaires uniquement et les types d’événements inconnus (transmis en tant que DaemonEvent ; les consommateurs du SDK les restreignent en aval via asKnownDaemonEvent).

Types (types.ts)

Exports notables : DaemonCapabilities, DaemonSession ({ sessionId, workspaceCwd, attached, clientId?, createdAt? }), DaemonEvent, DaemonSessionState, DaemonSessionContextStatus, DaemonSessionSupportedCommandsStatus, PermissionResponse, PromptResult, HeartbeatResult, SetModelResult, SessionMetadataResult, ainsi que les types de résultats MCP / agent / mémoire / auth. Les types de tâches de mémoire de workspace gérée incluent DaemonWorkspaceMemoryRememberTask, DaemonWorkspaceMemoryForgetTask et DaemonWorkspaceMemoryDreamTask.

Assistants de tâches de mémoire gérée du workspace :

await client.rememberWorkspaceMemory('Use strict TypeScript.', { contextMode: 'workspace', }); await client.getWorkspaceMemoryRememberTask('remember-...'); await client.forgetWorkspaceMemory('old preference'); await client.getWorkspaceMemoryForgetTask('forget-...'); await client.dreamWorkspaceMemory(); await client.getWorkspaceMemoryDreamTask('dream-...');

Workflow

Création ou rattachement + premier prompt

Abonnement avec rejeu

Authentification par device-flow

qwen-oauth est l’identifiant du fournisseur hérité v1. Le niveau gratuit de Qwen OAuth a été interrompu le 2026-04-15, les nouveaux clients devraient donc préférer un fournisseur d’authentification actuellement pris en charge lorsqu’il y en a un de disponible.

État et cycle de vie

  • DaemonClient est sans connexion persistante ; rien ne se passe lors de la construction. Chaque méthode ouvre un nouveau fetch.
  • DaemonSessionClient conserve lastSeenEventId entre les appels à events() ; les reconnexions rejouent les événements depuis le dernier vu.
  • DaemonAuthFlow est paresseux (lazy) — client.auth le construit lors du premier accès.
  • L’itérateur SSE se ferme lorsque (a) le daemon termine le flux, (b) AbortSignal.abort() est déclenché, (c) le consommateur sort de la boucle for await, ou (d) la limite de débordement du tampon (16 MiB) est atteinte.

Dépendances

  • globalThis.fetch (intégré à Node 18+, navigateur, undici, etc.). Injectable par DaemonClient pour les tests.
  • AbortController / AbortSignal.any / setTimeout natifs.
  • Aucune dépendance transitive sur @qwen-code/qwen-code-core ou @qwen-code/acp-bridge — le package SDK est entièrement découplé afin que les consommateurs externes n’embarquent pas les composants internes du daemon.

Sous-package ui/* (#4328  + #4353 )

Le SDK exporte également packages/sdk-typescript/src/daemon/ui/, un ensemble de primitives indépendantes de l’hôte qui transforment les événements du daemon en blocs de transcription :

  • normalizeDaemonEvent(evt) mappe les 47 événements wire connus du daemon en 42 valeurs DaemonUiEventType adaptées à l’UI ; les événements non modélisés ou malformés sont normalisés en debug.
  • createDaemonTranscriptState() ainsi que reduceDaemonTranscriptEvents(state, events) projettent les événements UI dans DaemonTranscriptBlock[].
  • createDaemonTranscriptStore() encapsule subscribe / dispatch.
  • render.ts / terminal.ts fournissent des rendus de base pour HTML et le terminal, tandis que toolPreview.ts produit des résumés d’appels d’outils.
  • Les sélecteurs incluent selectTranscriptBlocksOrderedByEventId, selectPendingPermissionBlocks, selectCurrentTool, selectApprovalMode, selectToolProgress, selectSubagentChildBlocks, formatMissedRange et formatBlockTimestamp.
  • Les constantes publiques incluent DAEMON_PLAN_TOOL_CALL_ID.
  • conformance.ts contient la suite de tests de cohérence multi-hôtes.

Le premier consommateur en production est packages/webui/src/daemon/ via le DaemonSessionProvider de React. Consultez 14-cli-tui-adapter.md pour l’architecture détaillée, le glossaire, le tableau des sélecteurs et la relation avec l’ancien DaemonTuiAdapter.

Le sous-package est exporté depuis le sous-chemin @qwen-code/sdk/daemon. Le code existant qui fait import { DaemonClient } n’est pas affecté.

Reconnexion Last-Event-ID avec le SDK

Suivi automatique via DaemonSessionClient

DaemonSessionClient suit lastSeenEventId en interne. Chaque événement généré avec un id numérique fait avancer le curseur. Les appels suivants à events() transmettent automatiquement l’identifiant suivi en tant que Last-Event-ID, ce qui permet la reconnexion avec rejeu sans état supplémentaire de la part de l’appelant :

import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk/daemon'; const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170', token }); const session = await DaemonSessionClient.createOrAttach(client); // First subscription — starts live (or from ring start for new sessions). for await (const event of session.events()) { console.log(event.type, event.id); // session.lastEventId is bumped on each id-bearing frame. if (shouldStop(event)) break; } // Reconnect — automatically sends Last-Event-ID: <last seen id>. // The daemon replays missed events from the ring, then goes live. for await (const event of session.events()) { // Replay frames arrive first, then a synthetic `replay_complete`, // then live events. handleEvent(event); }

Reconnexion manuelle avec DaemonClient

Pour un contrôle de plus bas niveau, utilisez DaemonClient.subscribeEvents directement et gérez le curseur vous-même :

const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170', token }); let cursor: number | undefined; // undefined = live-only on first connect async function* subscribe(sessionId: string, signal: AbortSignal) { for await (const event of client.subscribeEvents(sessionId, { lastEventId: cursor, signal, })) { // Only id-bearing frames advance the cursor. if (event.id !== undefined) { cursor = event.id; } // Handle ring-eviction gap. if (event.type === 'state_resync_required') { // State is stale — reload full session state. await client.loadSession(sessionId); continue; } yield event; } }

Reconnexion avec boucle de retry

Le SDK ne tente pas de retry automatiquement en cas d’échec réseau. Implémentez une boucle de retry autour de events() :

async function resilientSubscribe(session: DaemonSessionClient) { const MAX_RETRIES = 10; const BASE_DELAY_MS = 1000; for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { // `resume: true` (default) passes the tracked lastSeenEventId. for await (const event of session.events()) { attempt = 0; // reset on successful event handleEvent(event); } break; // clean stream end } catch (err) { const delay = BASE_DELAY_MS * 2 ** Math.min(attempt, 5); await new Promise((r) => setTimeout(r, delay)); } } }

Lors de la reconnexion, le daemon rejoue les événements avec id > lastSeenEventId depuis son ring borné (par défaut 8000 événements). Si l’écart dépasse la taille du ring, une trame state_resync_required signale au client d’appeler loadSession pour une reconstruction complète de l’état.

Initialisation de lastEventId à la construction

Les appelants qui persistent le curseur entre les redémarrages de processus peuvent l’initialiser :

const session = new DaemonSessionClient({ client, session: { sessionId, workspaceCwd, attached: true }, lastEventId: persistedCursor, // resume from persisted position });

La valeur doit être un entier fini et non négatif (validé à la construction). Les valeurs invalides lèvent une erreur.

Configuration

ParamètreEffet
baseUrlConstructeur DaemonClientURL du daemon ; les slashes finaux sont supprimés.
tokenConstructeur DaemonClientInjecté en tant que Authorization: Bearer.
fetchConstructeur DaemonClientPoint d’injection pour les tests.
fetchTimeoutMsConstructeur DaemonClientTimeout par appel ; 0 = désactivé.
clientIdArgument optionnel par méthodeEn-tête X-Qwen-Client-Id (voir 08-session-lifecycle.md).
lastEventIdConstructeur DaemonSessionClientInitialise le curseur de rejeu.
maxQueuedOption par abonnement?maxQueued=N pour la route SSE ; vérifiez préalablement caps.features.slow_client_warning.
perCallTimeoutMsPar méthode (ex. restartMcpServer)Remplace le timeout global du client.

Mises en garde et limites connues

  • fetchTimeoutMs s’applique par appel, et non au niveau de la connexion. Les lectures longues du corps de la réponse partagent le même timer. Un daemon qui stream des réponses doit remplacer le timeout par appel ou définir le timeout à 0.
  • SSE contourne le fetch timeout — les connexions SSE de longue durée ne sont pas tuées par fetchTimeoutMs. Utilisez AbortSignal pour une annulation contrôlée par l’appelant.
  • La limite du tampon de parseSseStream est de 16 MiB par mesure de sécurité. Une seule trame plus grande que cela interrompt l’itérateur (le daemon n’émet jamais légitimement de telles trames).
  • asKnownDaemonEvent retourne undefined pour les types d’événements non reconnus. Les consommateurs du SDK doivent gérer cette branche plutôt que de supposer que l’union est exhaustive ; c’est le contrat de compatibilité ascendante. Les événements non reconnus incrémentent DaemonSessionViewState.unrecognizedKnownEventCount.
  • client_evicted, slow_client_warning, stream_error ne font pas partie du ring de rejeu. Se reconnecter après une expulsion reprend depuis le ring du daemon ; vous ne reverrez pas la trame d’expulsion.
  • DaemonClient ne fait pas de retry automatique. Les échecs réseau se manifestent par des rejets ; la stratégie de reconnexion / rejeu relève de la responsabilité de l’appelant (DaemonSessionClient.events() facilite le rejeu, mais la reconnexion reste à faire par appel).

Références

  • packages/sdk-typescript/src/daemon/DaemonClient.ts
  • packages/sdk-typescript/src/daemon/DaemonSessionClient.ts
  • packages/sdk-typescript/src/daemon/DaemonAuthFlow.ts
  • packages/sdk-typescript/src/daemon/sse.ts
  • packages/sdk-typescript/src/daemon/events.ts
  • packages/sdk-typescript/src/daemon/types.ts
  • Guide de bout en bout : ../examples/daemon-client-quickstart.md.
Last updated on