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

Esquema de Eventos Tipados do Daemon v1

Visão geral

Cada frame SSE emitido pelo daemon em GET /session/:id/events tem a forma { id, v, type, data, originatorClientId?, _meta? }. v: 1 é o EVENT_SCHEMA_VERSION atual. type vem do conjunto fechado e fixado por versão DAEMON_KNOWN_EVENT_TYPE_VALUES em packages/sdk-typescript/src/daemon/events.ts; o conjunto atual possui 47 tipos de eventos conhecidos. O campo _meta do envelope é carimbado no limite de escrita do SSE por formatSseFrame() em packages/cli/src/serve/routes/sse-events.ts; consulte 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. Os consumidores do SDK podem, portanto, lidar com a compatibilidade futura sem exigir uma atualização sincronizada do SDK quando um daemon mais recente adiciona um novo tipo de evento; o reducer da sessão os registra como unrecognizedKnownEventCount.

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

Responsabilidades

  • Fornecer a fonte única 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 reducers 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 faz fallback para unknown.

Vocabulário de eventos (47 tipos conhecidos)

Agrupados por domínio.

Sessão principal

TipoDireçãoGatilhoPrincipais campos do payload
session_updateS->CQualquer notificação ACP sessionUpdate: texto do agente, pensamento, chamada de ferramenta ou planosessionUpdate: string, content?: ... (forma ACP opaca)
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éticoFrame de snapshot após anexação / replay do SSEsessionId, currentModelId: string | null, currentApprovalMode: string | null

Frames sintéticos no nível do assinante

TipoGatilhoNotas
client_evictedOverflow da fila EventBus por assinante. Sem idreason: 'queue_overflow' | 'queue_bytes_overflow' | string, droppedAfter?: number, queueSize?: number, maxQueued?: number, queuedBytes?: number, maxQueuedBytes?: number, eventBytes?: number; terminal apenas para o assinante atual, enquanto a sessão permanece ativa.
slow_client_warningBacklog de frames ao vivo ou backlog de bytes serializados ao vivo >= 75%; forçado e não possui idqueueSize, maxQueued, lastEventId, queuedBytes?, maxQueuedBytes?, threshold?: 'frames' | 'bytes' | 'frames_and_bytes'; rearmado após ambas as medições de frame e byte caírem abaixo de 37,5%.
stream_errorSubscriberLimitExceededError ou outro erro de stream de rotaerror: string; terminal para a assinatura.
state_resync_requiredsubscribe({lastEventId}) detecta que o ring do daemon não contém mais [lastEventId+1, earliestInRing-1], ou o cursor do cliente é de uma epoch anterior do bus. Forçado antes dos frames 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 stream SSE permanece aberto e os frames de replay + ao vivo continuam. O reducer do SDK define awaitingResync = true e ignora os deltas até que o chamador resete com loadSession.
replay_completeSentinel sem ID emitido após o término do loop de replay Last-Event-ID, para replay limpo e caminhos de ring-evicted, mesmo quando data.replayedCount === 0. Sem idreplayedCount: number; permite que os consumidores removam a UI de catch-up de forma determinística sem timeout.

Permissões (F3 + base)

TipoDireçãoGatilhoPrincipais campos do payload
permission_requestS->CAgente chama requestPermissionrequestId, sessionId, toolCall, options[]; o envelope carimba originatorClientId do originador do prompt.
permission_resolvedS->CMediador decidiurequestId, outcome (ACP PermissionOutcome)
permission_already_resolvedS->CVoto chega após a solicitaçã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?; votantes anônimos omitem clientId.

Modelos

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

Guardrails do 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 reinícios de pool multi-entry do 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 crítica subjacente para reinício multi-entry em modo pool. MCP_RESTART_REFUSED_REASONS rejeita motivos desconhecidos; um reducer de SDK mais antigo descarta silenciosamente novos valores de motivo aditivos porque parseDaemonEvent retorna undefined. Envie um novo motivo com um SDK que o conheça.

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

TipoDireçãoPayload
memory_changedS->CMemória de arquivo: scope: 'workspace' | 'global', filePath, mode, bytesWritten; memória gerenciada: 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; afeta o próximo spawn do filho ACP e não muta sessões já em execução.
settings_changedS->CGravação das configurações do workspace concluída. O payload é aberto; os consumidores devem atualizar com read-after-write.
settings_reloadedS->CO serviço de workspace do daemon releu as configurações. O payload é aberto.
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 também cobre tarefas de memória gerenciada sem sessão. Para esses payloads, scope é "managed", source é um de "workspace_memory_remember", "workspace_memory_forget" ou "workspace_memory_dream", taskId é o id da tarefa na fila e touchedScopes lista os escopos de memória gerenciada que foram alterados ("user" e/ou "project"). Nenhum evento é emitido quando uma tarefa remember/forget/dream é concluída sem alterar a memória gerenciada.

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

Esses eventos têm o workspace como chave, não a sessão. O reducer da sessão os trata como no-ops; reduceDaemonAuthEvent os projeta no estado no nível do 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 runtime do MCP

TipoDireçãoTriggerPrincipais campos do payload
mcp_server_addedS->CServidor adicionado em runtime através de POST /workspace/mcp/serversname, transport, replaced, shadowedSettings, toolCount, originatorClientId
mcp_server_removedS->CServidor removido em runtimename, wasShadowingSettings, originatorClientId

Ciclo de vida das extensões

TipoDireçãoTriggerPrincipais campos do payload
extensions_changedS->CTrabalho de instalação/atualização de extensão em segundo plano concluído ou alteração de statusrefreshed, failed, status?: 'installed' | 'enabled' | 'disabled' | 'updated' | 'uninstalled' | 'failed', source?, name?, version?, error?

Injeção de mensagem no meio do turno

TipoDireçãoTriggerPrincipais campos do payload
mid_turn_message_injectedS->CWeb-shell ou cliente remoto injetou mensagens em um turno em execução via POST /session/:id/injectsessionId, messages: string[], originatorClientId?; os consumidores DEVEM comparar originatorClientId com seu próprio id antes de fazer dedup.

Ciclo de vida do turno / pushes do assistente

TipoDireçãoTriggerPrincipais campos 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 cancelou. Isso significa “cancelamento solicitado”, não “cancelamento confirmado”. Assinantes pares aprendem que o prompt terminou.
turn_completeS->CUm turno foi concluído com sucessosessionId, stopReason, promptId?. promptId vincula às respostas de prompt não bloqueantes (202). O SDK corresponde os eventos SSE ao prompt de origem através dele.
turn_errorS->CUm turno falhousessionId, message, code?, promptId?; mesmo mecanismo de correlação de promptId.
session_rewoundS->CPOST /session/:id/rewind teve sucessosessionId, promptId, targetTurnIndex, filesChanged[], filesFailed[], originatorClientId?
session_branchedS->CPOST /session/:id/branch criou um branch a partir de uma sessão existentesourceSessionId, newSessionId, displayName, originatorClientId?
followup_suggestionS->CO filho ACP gerou sugestões de follow-up em ghost-text após end_turn, encaminhadas via SSE por sessãosessionId, suggestion, promptId; o wire carrega apenas sugestões cujo getFilterReason()===null. Os clientes as renderizam como ghost-text de placeholder de input 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á interface DaemonXxxData tipada; asKnownDaemonEvent retorna undefined e o normalizador de UI o analisa ad hoc.
user_shell_resultS->CResultado do comando shell acimasessionId, shellId, exitCode, output, aborted. Mesma observação de análise ad hoc que user_shell_command.

Arquitetura

AspectoOrigemNotas
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 47 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 de UI.
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, DaemonChannelBridge e a IDE VS Code 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 com chave requestId; limpo por permission_resolved / permission_already_resolved.
  • lastSessionUpdate?: DaemonSessionUpdateData - session_update mais recente.
  • lastModelSwitchFailure?: DaemonModelSwitchFailedData - de model_switch_failed.
  • terminalEvent? - evento terminal bruto.
  • streamError?: DaemonStreamErrorData - payload de stream_error mais recente.
  • unrecognizedKnownEventCount, lastUnrecognizedKnownEvent? - o evento foi reconhecido por asKnownDaemonEvent, mas o reducer ainda não tem um estado dedicado para ele.
  • droppedPermissionRequestCount, lastDroppedPermissionRequestId? - solicitação de permissão malformada não pôde entrar no mapa de pendentes.
  • unmatchedPermissionResolutionCount, lastUnmatchedPermissionResolutionId? - a resolução de permissão não tinha nenhuma 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 - reconhecido por asKnownDaemonEvent; o reducer da sessão não mantém campos de estado de visualização dedicados, e as UIs geralmente os tratam como sinais de atualização.
  • permissionVoteProgress: Record<string, DaemonPermissionPartialVoteData> - progresso da votação de 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 resync.
  • lastFollowupSuggestion?: DaemonFollowupSuggestionData - sugestão de follow-up mais recente enviada pelo daemon.
  • lastTurnComplete?: DaemonTurnCompleteData - conclusão de turno bem-sucedida mais recente.
  • lastTurnError?: DaemonTurnErrorData - erro de turno mais recente.
  • rewindCount, lastRewind?, lastBranch? - eventos de rewind / branch mais recentes.

DaemonAuthState

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

Fluxo

Lado do produtor

Lado do consumidor (SDK)

Metadados no nível do envelope

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

_meta.serverTimestamp - relógio do daemon

O EventBus.publish() em packages/acp-bridge/src/eventBus.ts adiciona o _meta.serverTimestamp quando o evento entra no bus. O tipo BridgeEvent inclui _meta?: Record<string, unknown>, então os consumidores internos do daemon veem o _meta em cada evento publicado no bus. O formatSseFrame() em packages/cli/src/serve/routes/sse-events.ts fornece um timestamp de fallback apenas para frames sintéticos (ex.: stream_error) que ignoram o EventBus.publish.

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

O merge preserva quaisquer chaves _meta existentes do evento de entrada ({...input._meta, serverTimestamp: Date.now()}). Os produtores podem anexar chaves _meta adicionais no nível do envelope; o EventBus.publish faz o merge delas com o timestamp em vez de sobrescrevê-las.

Por que isso importa: UIs multi-cliente que renderizam tempo relativo ou ordenam blocos de transcrição devem usar o horário do servidor em vez do relógio local de cada navegador/aba/celular. O timestamp do servidor mantém a ordenação consistente entre os clientes.

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

originatorClientId

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

_meta de Tool-call (provenance / serverId)

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

CampoTipoRegra de resolução
provenance'builtin' | 'mcp' | 'subagent'ToolCallEmitter.resolveToolProvenance: subagentMeta vence com subagent; nome da ferramenta correspondente 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 badges de builtin / servidor MCP / subagent sem precisar analisar o nome da ferramenta novamente.

Comportamento do reducer do SDK

O reduceDaemonSessionEvent(state, evt) em packages/sdk-typescript/src/daemon/events.ts projeta o stream no DaemonSessionViewState. Os campos relacionados ao resync são:

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

Enquanto awaitingResync = true, o reducer ignora a aplicação de deltas e permite apenas o conjunto fechado RESYNC_PASSTHROUGH_TYPES:

Tipo de passthroughPor que ainda é aplicado durante o resync
state_resync_requiredUm segundo resync raro deve atualizar lastResyncRequired / resyncRequiredCount.
session_diedO sinal de fim de stream deve permanecer visível durante o resync.
session_closedMesmo que acima.
client_evictedMesmo que acima.
stream_errorMesmo que acima.
session_snapshotFrame autoritativo de estado completo; seguro para aplicar durante o resync.

O lastEventId ainda avança monotonicamente através de advanceLastEventId(base) durante o resync. Após o chamador redefinir e limpar o awaitingResync, os deltas subsequentes se alinham ao cursor correto.

O reduceDaemonAuthEvent projeta eventos de device-flow em entradas de estado de auth no nível do workspace, com a forma de {deviceFlowId, status, providerId, expiresAt?, lastThrottleIntervalMs?, lastError?} conceitualmente. No código, o reducer armazena status, errorKind, hint, intervalMs, lastSeenEventId, authorizedExpiresAt e accountAlias no DaemonDeviceFlowReducerState; os próprios payloads de evento do daemon mantêm as formas por evento listadas acima.

Estado e compatibilidade futura

  • Adicione um tipo de evento conhecido anexando-o 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; novos SDKs 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 mudança quebrada (breaking change) e deve incrementar o EVENT_SCHEMA_VERSION, além de anunciar uma tag de capacidade compatível, como caps.features.typed_event_schema_v2.
  • O 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 têm id para que outros assinantes não vejam lacunas.
  • O originatorClientId fica no envelope em vez de data. Payloads de partial-vote / forbidden do F3 também fazem o merge dele em data através de mergeOriginator, para que os consumidores do estado da view não precisem reter o envelope.

Dependências

Configuração

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

Ressalvas e limites conhecidos

  • Seis tipos de frame sintéticos intencionalmente não têm id; o código do SDK não deve assumir que todo evento tem um id.
  • permission_partial_vote só aparece sob consensus. permission_forbidden aparece sob designated, consensus e local-only, mas não sob first-responder.
  • mcp_child_refused_batch só aparece em mode: 'enforce'; o modo warn nunca recusa.
  • Os eventos auth_device_flow_* não têm chave de sessão. Ao consumir através do DaemonSessionClient, use reduceDaemonAuthEvent para eles em vez do reducer 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 de wire: ../qwen-serve-protocol.md
Last updated on