Skip to Content
Guia do DesenvolvedorDaemonEsquema de Eventos Tipados do Daemon v1

Esquema de Eventos Tipados do Daemon v1

Visão geral

Cada quadro SSE emitido pelo daemon em GET /session/:id/events possui a forma { id, v, type, data, originatorClientId?, _meta? }. v: 1 é a versão atual de EVENT_SCHEMA_VERSION. O type provém do conjunto fechado e fixo de versão DAEMON_KNOWN_EVENT_TYPE_VALUES, definido em packages/sdk-typescript/src/daemon/events.ts; o conjunto atual possui 43 tipos de eventos conhecidos. O campo de envelope _meta é carimbado no limite de escrita SSE por formatSseFrame() em server.ts; veja Metadados no nível do envelope.

O SDK expõe asKnownDaemonEvent(evt). Ele retorna um KnownDaemonEvent discriminado para tipos de eventos conhecidos e undefined para outros tipos. Consumidores do SDK podem, portanto, lidar com compatibilidade futura sem exigir uma atualização sincronizada do SDK quando um daemon mais novo adiciona um tipo de evento; o redutor da sessão registra esses como unrecognizedKnownEventCount.

O formato de transmissão está em ../qwen-serve-protocol.md. Esta página é o contrato de payload para cada evento.

Responsabilidades

  • Fornecer a única fonte da verdade para o vocabulário de eventos (DAEMON_KNOWN_EVENT_TYPE_VALUES).
  • Fornecer um envelope tipado para cada tipo de evento (DaemonEventEnvelope<TType, TData>).
  • Fornecer redutores puros (reduceDaemonSessionEvent, reduceDaemonAuthEvent) que projetam um fluxo de eventos no estado de visualização do SDK.
  • Transmitir a tag de capacidade typed_event_schema como um sinal informativo. Se a tag estiver ausente, asKnownDaemonEvent ainda recai para unknown.

Vocabulário de eventos (43 tipos conhecidos)

Agrupados por domínio.

Sessão principal

TipoDireçãoGatilhoCampos principais do payload
session_updateS->CQualquer notificação sessionUpdate da ACP: texto do agente, pensamento, chamada de ferramenta ou planosessionUpdate: string, content?: ... (forma opaca da ACP)
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 fechamento programáticosessionId, reason: 'client_close' | string, closedBy?
session_snapshotS->C sintéticoQuadro de snapshot após anexo/replay SSEsessionId, currentModelId: string | null, currentApprovalMode: string | null

Quadros sintéticos no nível do assinante

TipoGatilhoNotas
client_evictedEstouro da fila EventBus por assinante. Sem idreason: string, droppedAfter?: number; terminal apenas para o assinante atual, enquanto a sessão permanece viva.
slow_client_warningFila >= 75%; enviado à força e não possui idqueueSize, maxQueued, lastEventId; rearmado após a fila cair abaixo de 37,5%.
stream_errorSubscriberLimitExceededError ou outro erro de rota de streamerror: string; terminal para a assinatura.
state_resync_requiredsubscribe({lastEventId}) detecta que o anel do daemon não contém mais [lastEventId+1, earliestInRing-1], ou o cursor do cliente é de uma época de barramento anterior. Enviado à força antes dos quadros de replay restantes e não possui id.reason: 'ring_evicted' | 'epoch_reset' | string, lastDeliveredId: number, earliestAvailableId: number. Este é um sinal de recuperação, não terminal: o fluxo SSE permanece aberto e os quadros de replay + ao vivo continuam. O redutor do SDK define awaitingResync = true e ignora deltas até que o chamador reinicie com loadSession.
replay_completeSentinela sem id emitida após o loop de replay Last-Event-ID terminar, tanto para replay limpo quanto para caminhos de anel expulso, mesmo quando data.replayedCount === 0. Sem idreplayedCount: number; permite que consumidores removam a UI de atualização de forma determinística sem um timeout.

Permissões (F3 + base)

TipoDireçãoGatilhoCampos principais do payload
permission_requestS->CAgente chama requestPermissionrequestId, sessionId, toolCall, options[]; o envelope carimba originatorClientId da origem do prompt.
permission_resolvedS->CMediador decidiurequestId, outcome (ACP PermissionOutcome)
permission_already_resolvedS->CVoto chega após a requisição já ter sido decididarequestId, sessionId, outcome
permission_partial_voteS->CPolítica consensus registra um voto não finalrequestId, sessionId, votesReceived, votesNeeded (>= 1), quorum, optionTallies: Record<string, number>, originatorClientId?
permission_forbiddenS->CPolítica rejeita um votorequestId, sessionId, clientId?, reason: 'designated_mismatch' | 'remote_not_allowed', originatorClientId?; eleitores anônimos omitem clientId.

Modelos

TipoDireçãoPayload
model_switchedS->CsessionId, modelId
model_switch_failedS->CsessionId, requestedModelId, error: string

Proteções MCP (PR 14b + F2)

TipoDireçãoPayload
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? para reinicializações de pool multi-entrada F2
mcp_server_restart_refusedS->CserverName, reason: 'budget_would_exceed' | 'in_flight' | 'disabled' | 'restart_failed', entryIndex?, details?. O quarto valor, restart_failed, carrega uma falha grave subjacente para reinicialização multi-entrada em modo pool. MCP_RESTART_REFUSED_REASONS rejeita razões desconhecidas; um redutor mais antigo do SDK descarta silenciosamente novos valores de razão aditivos porque parseDaemonEvent retorna undefined. Envie uma nova razão com um SDK que a conheça.

Controle de mutação (Wave 4 PR 16+17)

TipoDireçãoPayload
memory_changedS->Cscope: 'workspace' | 'global', filePath, mode: 'append' | 'replace', bytesWritten
agent_changedS->Cchange: 'created' | 'updated' | 'deleted', name, level: 'project' | 'user'
approval_mode_changedS->CsessionId, previous, next, persisted: boolean
tool_toggledS->CtoolName, enabled; afeta o próximo spawn filho do ACP e não modifica sessões já em execução.
settings_changedS->CA gravação das configurações do workspace foi concluída. O payload está aberto; os consumidores devem atualizar com leitura após gravação.
settings_reloadedS->CO serviço de workspace do daemon leu novamente as configurações. O payload está aberto.
workspace_initializedS->Cpath, action: 'created' | 'overwrote' | 'noop', originatorClientId?

Fluxo de dispositivo de autenticação (PR 21)

Estes eventos são chaveados por workspace, não por sessão. O reducer de sessão os trata como no-ops; reduceDaemonAuthEvent os projeta no estado de nível de workspace.

TipoDireçãoPayload
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

Mutação em tempo de execução do MCP

TipoDireçãoGatilhoCampos principais do payload
mcp_server_addedS->CServidor adicionado em tempo de execução através de POST /workspace/mcp/serversname, transport, replaced, shadowedSettings, toolCount, originatorClientId
mcp_server_removedS->CServidor removido em tempo de execuçãoname, wasShadowingSettings, originatorClientId

Ciclo de vida de turno / pushes do assistente

TipoDireçãoGatilhoCampos principais do payload
prompt_cancelledS->CO prompt foi cancelado através da rota explícita cancelSession ou desconexão SSE do originadorO envelope carimba originatorClientId para o cliente que está cancelando. Isso significa “cancelamento solicitado”, não “cancelamento confirmado”. Os assinantes pares aprendem que o prompt terminou.
turn_completeS->CUm turn foi concluído com sucessosessionId, stopReason, promptId?. promptId vincula-se a respostas de prompt não bloqueantes (202). O SDK combina eventos SSE ao prompt originador através dele.
turn_errorS->CUm turn falhousessionId, message, code?, promptId?; mesmo mecanismo de correlação de promptId.
session_rewoundS->CPOST /session/:id/rewind bem-sucedidosessionId, promptId, targetTurnIndex, filesChanged[], filesFailed[], originatorClientId?
session_branchedS->CPOST /session/:id/branch criou uma ramificação a partir de uma sessão existentesourceSessionId, newSessionId, displayName, originatorClientId?
followup_suggestionS->CFilho do ACP gerou sugestões de acompanhamento em texto fantasma após end_turn, encaminhadas via SSE por sessãosessionId, suggestion, promptId; o fio transporta apenas sugestões cujo getFilterReason()===null. Os clientes as renderizam como texto fantasma no placeholder de entrada e as invalidam no próximo sendPrompt.
user_shell_commandS->CO usuário iniciou um comando shell através de POST /session/:id/shell; distribuído para outros assinantes na mesma sessãosessionId, command, shellId, originatorClientId?. Ainda não há uma interface tipada DaemonXxxData; asKnownDaemonEvent retorna undefined e o normalizador da UI o analisa ad hoc.
user_shell_resultS->CResultado do comando shell acimasessionId, shellId, exitCode, output, aborted. Mesma nota de análise ad hoc que user_shell_command.

Arquitetura

PreocupaçãoFonteObservações
EVENT_SCHEMA_VERSION = 1packages/acp-bridge/src/eventBus.tsEnviado em cada frame.
DAEMON_KNOWN_EVENT_TYPE_VALUESpackages/sdk-typescript/src/daemon/events.tsLista fechada com 43 tipos.
DaemonEventEnvelope<TType, TData>events.tsEnvelope genérico.
DaemonKnownEventTypeevents.tstypeof DAEMON_KNOWN_EVENT_TYPE_VALUES[number].
Tipos de payload por eventoevents.tsA maioria dos tipos de evento tem uma interface DaemonXxxData; user_shell_* atualmente é analisado ad hoc pelo normalizador da interface.
asKnownDaemonEvent(evt)events.tsRetorna KnownDaemonEvent | undefined.
reduceDaemonSessionEvent(state, evt)events.tsProjeta em DaemonSessionViewState.
reduceDaemonAuthEvent(state, evt)events.tsProjeta em DaemonAuthState.
isWorkspaceScopedBudgetEvent(evt)events.tsDetecta F2 scope: 'workspace'.

DaemonSessionViewState

reduceDaemonSessionEvent preenche este estado de visualização. O adaptador CLI TUI, o DaemonChannelBridge e o VS Code IDE o consomem. Campos principais:

  • alive: boolean - torna-se false após um frame terminal (session_died, session_closed, client_evicted, stream_error).
  • currentModelId?: string - de model_switched.
  • displayName?: string - de session_metadata_updated.
  • pendingPermissions: Record<string, DaemonPermissionRequestData> - solicitações abertas indexadas por requestId; limpas por permission_resolved / permission_already_resolved.
  • lastSessionUpdate?: DaemonSessionUpdateData - último session_update.
  • lastModelSwitchFailure?: DaemonModelSwitchFailedData - de model_switch_failed.
  • terminalEvent? - evento terminal bruto.
  • streamError?: DaemonStreamErrorData - último payload de stream_error.
  • unrecognizedKnownEventCount, lastUnrecognizedKnownEvent? - evento foi reconhecido por asKnownDaemonEvent mas o redutor ainda não possui estado dedicado para ele.
  • droppedPermissionRequestCount, lastDroppedPermissionRequestId? - solicitação de permissão malformada não pôde entrar no mapa de pendentes.
  • unmatchedPermissionResolutionCount, lastUnmatchedPermissionResolutionId? - resolução de permissão não teve solicitação pendente correspondente.
  • slowClientWarningCount, lastSlowClientWarning? - de slow_client_warning.
  • mcpBudgetWarningCount, lastMcpBudgetWarning? - de mcp_budget_warning.
  • mcpChildRefusedBatchCount, lastMcpChildRefusedBatch? - de mcp_child_refused_batch.
  • lastWorkspaceMutation?, lastWorkspaceMutationType? - de memory_changed / agent_changed.
  • approvalMode?, approvalModeChangedCount, lastApprovalModeChange? - de approval_mode_changed.
  • toolToggleCount, lastToolToggle? - de tool_toggled.
  • workspaceInitCount, lastWorkspaceInit? - de workspace_initialized.
  • mcpRestartCount, lastMcpRestart? - de mcp_server_restarted.
  • mcpRestartRefusedCount, lastMcpRestartRefused? - de mcp_server_restart_refused.
  • settings_changed / settings_reloaded - reconhecidos por asKnownDaemonEvent; o redutor de sessão não mantém campos de estado de visualização dedicados, e as interfaces geralmente os tratam como sinais de atualização.
  • permissionVoteProgress: Record<string, DaemonPermissionPartialVoteData> - progresso de votação por consenso.
  • forbiddenVotes: DaemonPermissionForbiddenData[], forbiddenVoteCount - registros de votos rejeitados por política, limitados a 32.
  • awaitingResync: boolean - definido por state_resync_required; limpo quando o consumidor redefine o estado de visualização.
  • resyncRequiredCount, lastResyncRequired? - observabilidade de ressincronização.
  • lastFollowupSuggestion?: DaemonFollowupSuggestionData - última sugestão de acompanhamento enviada pelo daemon.
  • lastTurnComplete?: DaemonTurnCompleteData - última conclusão de turno bem-sucedida.
  • lastTurnError?: DaemonTurnErrorData - último erro de turno.
  • rewindCount, lastRewind?, lastBranch? - últimos eventos de retrocesso / ramificação.

DaemonAuthState

Uma entrada por providerId, orientada por auth_device_flow_*. Cada fluxo expõe { deviceFlowId, status, providerId, expiresAt?, lastThrottleIntervalMs?, lastError? }.

Fluxo

Lado produtor

Lado consumidor (SDK)

Metadados em nível de envelope

Além do payload data de cada evento, o daemon carimba dois campos em nível de envelope.

_meta.serverTimestamp - relógio do daemon

formatSseFrame() em packages/cli/src/serve/server.ts carimba isso no limite de escrita SSE, não dentro de EventBus.publish. O tipo BridgeEvent em memória permanece inalterado; consumidores internos do daemon não veem _meta, enquanto os quadros SSE na fio veem.

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

A mesclagem preserva quaisquer chaves _meta existentes ({...existingMeta, serverTimestamp: Date.now()}). Nenhum produtor atual do daemon escreve _meta em nível de envelope. A mesclagem de alto nível é uma válvula de escape de compatibilidade futura.

Por que é importante: UIs com vários clientes que renderizam tempo relativo ou ordenam blocos de transcrição devem usar o tempo do servidor em vez do relógio local de cada navegador/aba/telefone. O carimbo do servidor mantém a ordenação consistente entre clientes.

Acesso no SDK: prefira event._meta?.serverTimestamp. Caminhos de compatibilidade também podem sondar event.serverTimestamp ou event.data._meta.serverTimestamp. Não misture data._meta do payload ACP com _meta do envelope do daemon.

originatorClientId

Eventos acionados por uma requisição que continha um X-Qwen-Client-Id registrado podem carimbar este campo. Veja 08-session-lifecycle.md.

_meta de chamada de ferramenta (procedência / serverId)

Isso é separado do _meta do envelope: os payloads ACP session/update podem carregar seu próprio _meta em event.data._meta. ToolCallEmitter (packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts) carimba dois campos em emitStart, emitResult e emitError:

CampoTipoRegra de resolução
provenance'builtin' | 'mcp' | 'subagent'ToolCallEmitter.resolveToolProvenance: subagentMeta vence com subagent; nome da ferramenta correspondendo a mcp__<server>__<tool> mapeia para mcp; todo o resto mapeia para builtin.
serverIdstring apenas quando provenance === 'mcp'Extraído heuristicamente de mcp__<serverId>__<tool>.

O nome de exibição _meta.toolName existente é preservado. A UI usa esses campos para renderizar emblemas de ferramenta builtin / servidor MCP / subagente sem precisar reanalisar o nome da ferramenta.

Comportamento do redutor do SDK

reduceDaemonSessionEvent(state, evt) em packages/sdk-typescript/src/daemon/events.ts projeta o fluxo em DaemonSessionViewState. Os campos relacionados à ressincronização são:

  • awaitingResync: boolean - definido por state_resync_required; o chamador limpa, tipicamente após POST /session/:id/load redefinir o estado da visão.
  • resyncRequiredCount: number - contador de observabilidade.
  • lastResyncRequired?: DaemonStateResyncRequiredData - payload mais recente.

Enquanto awaitingResync = true, o redutor pula a aplicação de delta e permite apenas o conjunto fechado RESYNC_PASSTHROUGH_TYPES:

Tipo de passagemPor que ainda é aplicado durante a ressincronização
state_resync_requiredUma segunda ressincronização rara deve atualizar lastResyncRequired / resyncRequiredCount.
session_diedSinal de fluxo terminal deve permanecer visível durante a ressincronização.
session_closedO mesmo que acima.
client_evictedO mesmo que acima.
stream_errorO mesmo que acima.
session_snapshotQuadro autoritativo de estado completo; seguro aplicar durante a ressincronização.
lastEventId continua avançando monotonicamente através de advanceLastEventId(base) durante a ressincronização. Depois que o chamador faz o reset e limpa awaitingResync, os deltas subsequentes se alinham ao cursor correto.

reduceDaemonAuthEvent projeta eventos de fluxo de dispositivo em entradas de estado de autenticação no nível do workspace formatadas como {deviceFlowId, status, providerId, expiresAt?, lastThrottleIntervalMs?, lastError?} conceitualmente. No código, o redutor armazena status, errorKind, hint, intervalMs, lastSeenEventId, authorizedExpiresAt e accountAlias em DaemonDeviceFlowReducerState; os payloads dos eventos do daemon permanecem nos formatos por evento listados acima.

Estado e compatibilidade futura

  • Adicione um tipo de evento conhecido anexando a DAEMON_KNOWN_EVENT_TYPE_VALUES. SDKs antigos retornam undefined para tipos de evento não reconhecidos através do caminho de fallback e incrementam unrecognizedKnownEventCount; SDKs novos dependem da união discriminada.
  • Adicionar campos opcionais a um payload existente é seguro porque os payloads são abertos ({ [key: string]: unknown }).
  • Alterar a forma de um payload existente é uma quebra e deve incrementar EVENT_SCHEMA_VERSION além de anunciar uma tag de capacidade compatível como caps.features.typed_event_schema_v2.
  • id é monotônico por sessão. Frames sintéticos no nível do assinante (client_evicted, slow_client_warning, stream_error, state_resync_required, replay_complete, session_snapshot) intencionalmente não possuem id para que outros assinantes não vejam lacunas.
  • originatorClientId vive no envelope em vez de data. Payloads de voto parcial proibido do F3 também o mesclam em data através de mergeOriginator para que consumidores do estado da visão não precisem reter o envelope.

Dependências

Configuração

  • Sempre anunciados: typed_event_schema, mcp_guardrail_events e permission_mediation (com os modos de política suportados).
  • Nenhuma variável de ambiente ou flag controla diretamente o esquema em si. QWEN_SERVE_NO_MCP_POOL=1 altera o scope do evento MCP de 'workspace' para ausente ou 'session'.

Riscos e limitações conhecidas

  • Seis tipos de frame sintéticos intencionalmente não possuem id; o código do SDK não deve presumir que todo evento tem um id.
  • permission_partial_vote aparece apenas sob consensus. permission_forbidden aparece sob designated, consensus e local-only, mas não sob first-responder.
  • mcp_child_refused_batch aparece apenas em mode: 'enforce'; o modo warn nunca recusa.
  • Eventos auth_device_flow_* não são chaveados por sessão. Ao consumir através de DaemonSessionClient, use reduceDaemonAuthEvent para eles em vez do redutor de sessão.

Referências

  • 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)
  • Referência do protocolo: ../qwen-serve-protocol.md
Last updated on