Skip to Content
Guide développeurDaemonSchéma d'événements typés du démon v1

Schéma d’événements typés du démon v1

Vue d’ensemble

Chaque trame SSE émise par le démon sur GET /session/:id/events a la forme { id, v, type, data, originatorClientId?, _meta? }. v: 1 est la EVENT_SCHEMA_VERSION actuelle. type provient de l’ensemble fermé et épinglé par version DAEMON_KNOWN_EVENT_TYPE_VALUES dans packages/sdk-typescript/src/daemon/events.ts ; l’ensemble actuel compte 47 types d’événements connus. Le champ d’enveloppe _meta est estampillé à la limite d’écriture SSE par formatSseFrame() dans packages/cli/src/serve/routes/sse-events.ts ; voir Métadonnées au niveau de l’enveloppe.

Le SDK expose asKnownDaemonEvent(evt). Il retourne un KnownDaemonEvent discriminé pour les types d’événements connus et undefined pour les autres types. Les consommateurs du SDK peuvent ainsi gérer la compatibilité ascendante sans nécessiter une mise à jour du SDK au même rythme lorsqu’un démon plus récent ajoute un type d’événement ; le réducteur de session les enregistre sous unrecognizedKnownEventCount.

Le format de transmission se trouve dans ../qwen-serve-protocol.md. Cette page définit le contrat de charge utile pour chaque événement.

Responsabilités

  • Fournir la source unique de vérité pour le vocabulaire des événements (DAEMON_KNOWN_EVENT_TYPE_VALUES).
  • Fournir une enveloppe typée pour chaque type d’événement (DaemonEventEnvelope<TType, TData>).
  • Fournir des réducteurs purs (reduceDaemonSessionEvent, reduceDaemonAuthEvent) qui projettent un flux d’événements dans l’état de vue du SDK.
  • Diffuser le tag de capacité typed_event_schema comme signal informatif. Si le tag est absent, asKnownDaemonEvent retombe tout de même sur unknown.

Vocabulaire des événements (47 types connus)

Regroupés par domaine.

Session principale

TypeDirectionDéclencheurChamps clés de la charge utile
session_updateS->CToute notification ACP sessionUpdate : texte de l’agent, réflexion, appel d’outil ou plansessionUpdate: string, content?: ... (forme ACP opaque)
session_metadata_updatedS->CPATCH /session/:id/metadatasessionId, displayName?
session_diedS->C terminalchannel.exitedsessionId, reason, exitCode? | null, signalCode? | null
session_closedS->C terminalDELETE /session/:id ou fermeture programmatiquesessionId, reason: 'client_close' | string, closedBy?
session_snapshotS->C syntheticTrame de snapshot après attachement / relecture SSEsessionId, currentModelId: string | null, currentApprovalMode: string | null

Trames synthétiques au niveau de l’abonné

TypeDéclencheurNotes
client_evictedDébordement de la file d’attente EventBus par abonné. Pas d’idreason: 'queue_overflow' | 'queue_bytes_overflow' | string, droppedAfter?: number, queueSize?: number, maxQueued?: number, queuedBytes?: number, maxQueuedBytes?: number, eventBytes?: number ; terminal uniquement pour l’abonné actuel, tandis que la session reste active.
slow_client_warningArriéré de trames en direct ou arriéré d’octets sérialisés en direct >= 75 % ; poussé de force et n’a pas d’idqueueSize, maxQueued, lastEventId, queuedBytes?, maxQueuedBytes?, threshold?: 'frames' | 'bytes' | 'frames_and_bytes' ; réarmé après que les mesures de trames et d’octets passent toutes deux sous les 37,5 %.
stream_errorSubscriberLimitExceededError ou une autre erreur de flux de routeerror: string ; terminal pour l’abonnement.
state_resync_requiredsubscribe({lastEventId}) détecte que l’anneau du démon ne contient plus [lastEventId+1, earliestInRing-1], ou que le curseur du client provient d’une époque de bus précédente. Poussé de force avant les trames de relecture restantes et n’a pas d’id.reason: 'ring_evicted' | 'epoch_reset' | string, lastDeliveredId: number, earliestAvailableId: number. Il s’agit d’un signal de récupération, non terminal : le flux SSE reste ouvert et la relecture + les trames en direct continuent. Le réducteur du SDK définit awaitingResync = true et ignore les deltas jusqu’à ce que l’appelant réinitialise avec loadSession.
replay_completeSentinelle sans id émise après la fin de la boucle de relecture Last-Event-ID, pour les chemins de relecture propre et d’éviction d’anneau, même lorsque data.replayedCount === 0. Pas d’idreplayedCount: number ; permet aux consommateurs de supprimer l’UI de rattrapage de manière déterministe sans délai d’attente.

Permissions (F3 + base)

TypeDirectionDéclencheurChamps clés de la charge utile
permission_requestS->CL’agent appelle requestPermissionrequestId, sessionId, toolCall, options[] ; l’enveloppe estampille originatorClientId depuis l’origine du prompt.
permission_resolvedS->CLe médiateur a décidérequestId, outcome (ACP PermissionOutcome)
permission_already_resolvedS->CLe vote arrive après que la demande a déjà été décidéerequestId, sessionId, outcome
permission_partial_voteS->CLa politique consensus enregistre un vote non finalrequestId, sessionId, votesReceived, votesNeeded (>= 1), quorum, optionTallies: Record<string, number>, originatorClientId?
permission_forbiddenS->CLa politique rejette un voterequestId, sessionId, clientId?, reason: 'designated_mismatch' | 'remote_not_allowed', originatorClientId? ; les votants anonymes omettent clientId.

Modèles

TypeDirectionCharge utile
model_switchedS->CsessionId, modelId
model_switch_failedS->CsessionId, requestedModelId, error: string

Garde-fous MCP (PR 14b + F2)

TypeDirectionCharge utile
mcp_budget_warningS->CliveCount, reservedCount, budget, thresholdRatio: 0.75, mode: 'warn' | 'enforce', scope?: 'workspace' | 'session'
mcp_child_refused_batchS->CrefusedServers: [{ name, transport, reason: 'budget_exhausted' }], budget, liveCount, reservedCount, mode: 'enforce', scope?: 'workspace' | 'session'
mcp_server_restartedS->CserverName, durationMs, entryIndex? pour les redémarrages de pool multi-entrées F2
mcp_server_restart_refusedS->CserverName, reason: 'budget_would_exceed' | 'in_flight' | 'disabled' | 'restart_failed', entryIndex?, details?. La quatrième valeur, restart_failed, indique une panne matérielle sous-jacente pour le redémarrage multi-entrées en mode pool. MCP_RESTART_REFUSED_REASONS rejette les raisons inconnues ; un réducteur SDK plus ancien ignore silencieusement les nouvelles valeurs de raisons additives car parseDaemonEvent retourne undefined. Expédiez une nouvelle raison avec un SDK qui la connaît.

Contrôle des mutations (Wave 4 PR 16+17)

TypeDirectionPayload
memory_changedS->CMémoire fichier : scope: 'workspace' | 'global', filePath, mode, bytesWritten ; mémoire managée : scope: 'managed', source, taskId, touchedScopes
agent_changedS->Cchange: 'created' | 'updated' | 'deleted', name, level: 'project' | 'user'
approval_mode_changedS->CsessionId, previous, next, persisted: boolean
tool_toggledS->CtoolName, enabled ; affecte le prochain spawn d’enfant ACP et ne modifie pas les sessions déjà en cours d’exécution.
settings_changedS->CL’écriture des paramètres du workspace est terminée. Le payload est ouvert ; les consommateurs doivent actualiser avec un read-after-write.
settings_reloadedS->CLe service workspace du daemon relit les paramètres. Le payload est ouvert.
trust_change_requestedS->CworkspaceCwd, desiredState: 'trusted' | 'untrusted', reason?
workspace_initializedS->Cpath, action: 'created' | 'overwrote' | 'noop', originatorClientId?
github_setup_completedS->CreleaseTag, readmeUrl, secretsUrl?, workflows: [{path, status, sizeBytes?, error?}], gitignore: {path, status, added?, error?}

memory_changed couvre également les tâches de mémoire managée sans session. Pour ces payloads, scope est "managed", source est l’un des éléments suivants : "workspace_memory_remember", "workspace_memory_forget" ou "workspace_memory_dream", taskId est l’identifiant de la tâche en file d’attente, et touchedScopes liste les scopes de mémoire managée qui ont changé ("user" et/ou "project"). Aucun événement n’est émis lorsqu’une tâche remember/forget/dream se termine sans toucher la mémoire managée.

Flux d’authentification par appareil (PR 21)

Ces événements sont indexés par workspace, et non par session. Le reducer de session les traite comme des no-ops ; reduceDaemonAuthEvent les projette dans l’état au niveau du workspace.

TypeDirectionPayload
auth_device_flow_startedS->CdeviceFlowId, providerId, expiresAt
auth_device_flow_throttledS->CdeviceFlowId, intervalMs
auth_device_flow_authorizedS->CdeviceFlowId, providerId, expiresAt?, accountAlias?
auth_device_flow_failedS->CdeviceFlowId, errorKind, hint?
auth_device_flow_cancelledS->CdeviceFlowId

Mutation runtime MCP

TypeDirectionDéclencheurChamps de payload clés
mcp_server_addedS->CServeur ajouté à l’exécution via POST /workspace/mcp/serversname, transport, replaced, shadowedSettings, toolCount, originatorClientId
mcp_server_removedS->CServeur supprimé à l’exécutionname, wasShadowingSettings, originatorClientId

Cycle de vie des extensions

TypeDirectionDéclencheurChamps de payload clés
extensions_changedS->CTravail d’installation/actualisation d’extension en arrière-plan terminé ou changement de statutrefreshed, failed, status?: 'installed' | 'enabled' | 'disabled' | 'updated' | 'uninstalled' | 'failed', source?, name?, version?, error?

Injection de messages en cours de tour

TypeDirectionDéclencheurChamps de payload clés
mid_turn_message_injectedS->CLe web-shell ou un client distant injecte des messages dans un tour en cours via POST /session/:id/injectsessionId, messages: string[], originatorClientId? ; les consommateurs DOIVENT comparer originatorClientId à leur propre identifiant avant la déduplication.

Cycle de vie du tour / pushes de l’assistant

TypeDirectionDéclencheurChamps de payload clés
prompt_cancelledS->CLe prompt est annulé via la route explicite cancelSession ou déconnexion SSE de l’origineL’enveloppe ajoute originatorClientId pour le client qui annule. Cela signifie “annulation demandée”, et non “annulation confirmée”. Les abonnés pairs apprennent que le prompt est terminé.
turn_completeS->CUn tour se termine avec succèssessionId, stopReason, promptId?. promptId fait le lien avec les réponses de prompt non bloquantes (202). Le SDK fait correspondre les événements SSE au prompt d’origine grâce à celui-ci.
turn_errorS->CUn tour échouesessionId, message, code?, promptId? ; même mécanisme de corrélation promptId.
session_rewoundS->CPOST /session/:id/rewind réussitsessionId, promptId, targetTurnIndex, filesChanged[], filesFailed[], originatorClientId?
session_branchedS->CPOST /session/:id/branch crée une branche à partir d’une session existantesourceSessionId, newSessionId, displayName, originatorClientId?
followup_suggestionS->CL’enfant ACP génère des suggestions de suivi en ghost-text après end_turn, transmises via le SSE par sessionsessionId, suggestion, promptId ; le wire ne transporte que les suggestions dont getFilterReason()===null. Les clients les affichent sous forme de ghost-text en placeholder d’input et les invalident lors du prochain sendPrompt.
user_shell_commandS->CL’utilisateur démarre une commande shell via POST /session/:id/shell ; diffusé aux autres abonnés de la même sessionsessionId, command, shellId, originatorClientId?. Il n’y a pas encore d’interface typée DaemonXxxData ; asKnownDaemonEvent retourne undefined et le normalisateur d’UI l’analyse de manière ad hoc.
user_shell_resultS->CRésultat de la commande shell ci-dessussessionId, shellId, exitCode, output, aborted. Même note d’analyse ad hoc que pour user_shell_command.

Architecture

SujetSourceNotes
EVENT_SCHEMA_VERSION = 1packages/acp-bridge/src/eventBus.tsEnvoyé sur chaque frame.
DAEMON_KNOWN_EVENT_TYPE_VALUESpackages/sdk-typescript/src/daemon/events.tsListe fermée avec 47 types.
DaemonEventEnvelope<TType, TData>events.tsEnveloppe générique.
DaemonKnownEventTypeevents.tstypeof DAEMON_KNOWN_EVENT_TYPE_VALUES[number].
Types de payload par événementevents.tsLa plupart des types d’événements ont une interface DaemonXxxData ; user_shell_* est actuellement analysé de manière ad hoc par le normalisateur d’UI.
asKnownDaemonEvent(evt)events.tsRetourne KnownDaemonEvent | undefined.
reduceDaemonSessionEvent(state, evt)events.tsProjette dans DaemonSessionViewState.
reduceDaemonAuthEvent(state, evt)events.tsProjette dans DaemonAuthState.
isWorkspaceScopedBudgetEvent(evt)events.tsDétecte F2 scope: 'workspace'.

DaemonSessionViewState

reduceDaemonSessionEvent remplit cet état de vue. L’adaptateur CLI TUI, DaemonChannelBridge et l’IDE VS Code le consomment. Champs clés :

  • alive: boolean - devient false après une frame terminale (session_died, session_closed, client_evicted, stream_error).
  • currentModelId?: string - provenant de model_switched.
  • displayName?: string - provenant de session_metadata_updated.
  • pendingPermissions: Record<string, DaemonPermissionRequestData> - requêtes ouvertes indexées par requestId ; vidées par permission_resolved / permission_already_resolved.
  • lastSessionUpdate?: DaemonSessionUpdateData - dernier session_update.
  • lastModelSwitchFailure?: DaemonModelSwitchFailedData - provenant de model_switch_failed.
  • terminalEvent? - événement terminal brut.
  • streamError?: DaemonStreamErrorData - dernier payload stream_error.
  • unrecognizedKnownEventCount, lastUnrecognizedKnownEvent? - l’événement a été reconnu par asKnownDaemonEvent mais le reducer n’a pas encore d’état dédié pour celui-ci.
  • droppedPermissionRequestCount, lastDroppedPermissionRequestId? - une requête de permission malformée n’a pas pu entrer dans la map pending.
  • unmatchedPermissionResolutionCount, lastUnmatchedPermissionResolutionId? - la résolution de permission n’avait aucune requête pending correspondante.
  • slowClientWarningCount, lastSlowClientWarning? - provenant de slow_client_warning.
  • mcpBudgetWarningCount, lastMcpBudgetWarning? - provenant de mcp_budget_warning.
  • mcpChildRefusedBatchCount, lastMcpChildRefusedBatch? - provenant de mcp_child_refused_batch.
  • lastWorkspaceMutation?, lastWorkspaceMutationType? - provenant de memory_changed / agent_changed.
  • approvalMode?, approvalModeChangedCount, lastApprovalModeChange? - provenant de approval_mode_changed.
  • toolToggleCount, lastToolToggle? - provenant de tool_toggled.
  • workspaceInitCount, lastWorkspaceInit? - provenant de workspace_initialized.
  • mcpRestartCount, lastMcpRestart? - provenant de mcp_server_restarted.
  • mcpRestartRefusedCount, lastMcpRestartRefused? - provenant de mcp_server_restart_refused.
  • settings_changed / settings_reloaded - reconnus par asKnownDaemonEvent ; le reducer de session ne maintient pas de champs d’état de vue dédiés, et les UI les traitent généralement comme des signaux d’actualisation.
  • permissionVoteProgress: Record<string, DaemonPermissionPartialVoteData> - progression du vote par consensus.
  • forbiddenVotes: DaemonPermissionForbiddenData[], forbiddenVoteCount - enregistrements de votes rejetés par la politique, plafonnés à 32.
  • awaitingResync: boolean - défini par state_resync_required ; vidé lorsque le consommateur réinitialise l’état de vue.
  • resyncRequiredCount, lastResyncRequired? - observabilité de la resynchronisation.
  • lastFollowupSuggestion?: DaemonFollowupSuggestionData - dernière suggestion de suivi poussée par le daemon.
  • lastTurnComplete?: DaemonTurnCompleteData - dernière complétion de tour réussie.
  • lastTurnError?: DaemonTurnErrorData - dernière erreur de tour.
  • rewindCount, lastRewind?, lastBranch? - derniers événements rewind / branch.

DaemonAuthState

Une entrée par providerId, pilotée par auth_device_flow_*. Chaque flux expose { deviceFlowId, status, providerId, expiresAt?, lastThrottleIntervalMs?, lastError? }.

Flux

Côté producteur

Côté consommateur (SDK)

Métadonnées au niveau de l’enveloppe

Au-delà de la charge utile data de chaque événement, le démon ajoute deux champs au niveau de l’enveloppe.

_meta.serverTimestamp - horloge du démon

EventBus.publish() dans packages/acp-bridge/src/eventBus.ts ajoute _meta.serverTimestamp lorsque l’événement entre dans le bus. Le type BridgeEvent inclut _meta?: Record<string, unknown>, ainsi les consommateurs internes du démon voient bien _meta sur chaque événement publié dans le bus. formatSseFrame() dans packages/cli/src/serve/routes/sse-events.ts fournit un timestamp de secours uniquement pour les trames synthétiques (par ex. stream_error) qui contournent EventBus.publish.

{ "id": 47, "v": 1, "type": "session_update", "data": { ... }, "_meta": { "serverTimestamp": 1716287345123 } }

La fusion préserve toutes les clés _meta existantes de l’événement d’entrée ({...input._meta, serverTimestamp: Date.now()}). Les producteurs peuvent attacher des clés _meta supplémentaires au niveau de l’enveloppe ; EventBus.publish les fusionne avec le timestamp au lieu de les écraser.

Pourquoi c’est important : les interfaces multi-clients qui affichent un temps relatif ou trient les blocs de transcription doivent utiliser l’heure du serveur au lieu de l’horloge locale de chaque navigateur/onglet/téléphone. L’horodatage par le serveur maintient un ordre cohérent entre les clients.

Accès SDK : préférez event._meta?.serverTimestamp. Les chemins de compatibilité peuvent aussi sonder event.serverTimestamp ou event.data._meta.serverTimestamp. Ne mélangez pas le _meta de la charge utile ACP data._meta avec le _meta de l’enveloppe du démon.

originatorClientId

Les événements déclenchés par une requête portant un X-Qwen-Client-Id enregistré peuvent renseigner ce champ. Voir 08-session-lifecycle.md.

_meta des appels d’outils (provenance / serverId)

Ceci est distinct du _meta de l’enveloppe : les charges utiles ACP session/update peuvent porter leur propre _meta dans event.data._meta. ToolCallEmitter (packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts) ajoute deux champs lors de emitStart, emitResult et emitError :

ChampTypeRègle de résolution
provenance'builtin' | 'mcp' | 'subagent'ToolCallEmitter.resolveToolProvenance : subagentMeta l’emporte avec subagent ; le nom de l’outil correspondant à mcp__<server>__<tool> est mappé à mcp ; tout le reste est mappé à builtin.
serverIdstring uniquement quand provenance === 'mcp'Extrait de manière heuristique depuis mcp__<serverId>__<tool>.

Le nom d’affichage existant _meta.toolName est conservé. L’interface utilisateur utilise ces champs pour afficher les badges builtin / serveur MCP / subagent sans avoir à reparser le nom de l’outil.

Comportement du reducer SDK

reduceDaemonSessionEvent(state, evt) dans packages/sdk-typescript/src/daemon/events.ts projette le flux dans DaemonSessionViewState. Les champs liés à la resynchronisation sont :

  • awaitingResync: boolean - défini par state_resync_required ; l’appelant le réinitialise, généralement après que POST /session/:id/load a réinitialisé l’état de la vue.
  • resyncRequiredCount: number - compteur d’observabilité.
  • lastResyncRequired?: DaemonStateResyncRequiredData - dernière charge utile.

Tant que awaitingResync = true, le reducer ignore l’application des deltas et n’autorise que l’ensemble fermé RESYNC_PASSTHROUGH_TYPES :

Type transitantPourquoi il est toujours appliqué pendant la resynchronisation
state_resync_requiredUne seconde resynchronisation rare doit mettre à jour lastResyncRequired / resyncRequiredCount.
session_diedLe signal de fin de flux doit rester visible pendant la resynchronisation.
session_closedIdem ci-dessus.
client_evictedIdem ci-dessus.
stream_errorIdem ci-dessus.
session_snapshotTrame faisant autorité pour l’état complet ; sûre à appliquer pendant la resynchronisation.

lastEventId continue d’avancer de manière monotone via advanceLastEventId(base) pendant la resynchronisation. Après que l’appelant a réinitialisé et effacé awaitingResync, les deltas suivants s’alignent sur le bon curseur.

reduceDaemonAuthEvent projette les événements de flux d’appareil (device-flow) dans des entrées d’état d’authentification au niveau du workspace, ayant conceptuellement la forme {deviceFlowId, status, providerId, expiresAt?, lastThrottleIntervalMs?, lastError?}. Dans le code, le reducer stocke status, errorKind, hint, intervalMs, lastSeenEventId, authorizedExpiresAt et accountAlias sur DaemonDeviceFlowReducerState ; les charges utiles des événements du démon restent quant à elles conformes aux formes par événement listées ci-dessus.

État et compatibilité ascendante

  • Ajoutez un type d’événement connu en l’ajoutant à DAEMON_KNOWN_EVENT_TYPE_VALUES. Les anciens SDKs retournent undefined pour les types d’événements non reconnus via le chemin de secours et incrémentent unrecognizedKnownEventCount ; les nouveaux SDKs s’appuient sur l’union discriminée.
  • L’ajout de champs optionnels à une charge utile existante est sûr car les charges utiles sont ouvertes ({ [key: string]: unknown }).
  • Modifier la forme d’une charge utile existante est un changement cassant (breaking) et doit incrémenter EVENT_SCHEMA_VERSION ainsi qu’annoncer un tag de capacité compatible tel que caps.features.typed_event_schema_v2.
  • id est monotone par session. Les trames synthétiques au niveau de l’abonné (client_evicted, slow_client_warning, stream_error, state_resync_required, replay_complete, session_snapshot) n’ont volontairement pas d’id afin que les autres abonnés ne voient pas de trous.
  • originatorClientId se trouve sur l’enveloppe plutôt que dans data. Les charges utiles F3 partial-vote / forbidden le fusionnent également dans data via mergeOriginator afin que les consommateurs de l’état de la vue n’aient pas besoin de conserver l’enveloppe.

Dépendances

Configuration

  • Toujours annoncées : typed_event_schema, mcp_guardrail_events et permission_mediation (avec les modes de politique supportés).
  • Aucune variable d’environnement ou flag ne contrôle directement le schéma lui-même. QWEN_SERVE_NO_MCP_POOL=1 change le scope des événements MCP de 'workspace' à absent ou 'session'.

Mises en garde et limites connues

  • Six types de trames synthétiques n’ont volontairement pas d’id ; le code du SDK ne doit pas supposer que chaque événement a un id.
  • permission_partial_vote n’apparaît que sous consensus. permission_forbidden apparaît sous designated, consensus et local-only, mais pas sous first-responder.
  • mcp_child_refused_batch n’apparaît qu’en mode: 'enforce' ; le mode warn ne refuse jamais.
  • Les événements auth_device_flow_* ne sont pas liés à une session. Lors de la consommation via DaemonSessionClient, utilisez reduceDaemonAuthEvent pour ceux-ci plutôt que le reducer de session.

Références

  • packages/sdk-typescript/src/daemon/events.ts
  • packages/acp-bridge/src/eventBus.ts (EVENT_SCHEMA_VERSION)
  • packages/cli/src/serve/capabilities.ts (typed_event_schema, mcp_guardrail_events, permission_mediation)
  • Référence wire : ../qwen-serve-protocol.md
Last updated on