Vai al contenuto principale

Documentation Index

Fetch the complete documentation index at: https://docs.openclaw.ai/llms.txt

Use this file to discover all available pages before exploring further.

Il kernel dei turni di canale è la macchina a stati inbound condivisa che trasforma un evento di piattaforma normalizzato in un turno dell’agente. I plugin di canale forniscono i fatti della piattaforma e il callback di consegna. Il core possiede l’orchestrazione: ingestione, classificazione, preflight, risoluzione, autorizzazione, assemblaggio, registrazione, dispatch e finalizzazione. Usalo quando il tuo plugin si trova nel percorso critico dei messaggi inbound. Per eventi non di messaggistica (comandi slash, modali, interazioni con pulsanti, eventi del ciclo di vita, reazioni, stato vocale), mantienili locali al plugin. Il kernel possiede solo gli eventi che possono diventare un turno testuale dell’agente.
Il kernel viene raggiunto tramite il runtime del plugin iniettato come runtime.channel.turn.*. Il tipo del runtime del plugin è esportato da openclaw/plugin-sdk/core, quindi i plugin nativi di terze parti possono usare questi punti di ingresso nello stesso modo dei plugin di canale in bundle.

Perché un kernel condiviso

I plugin di canale ripetono lo stesso flusso inbound: normalizzare, instradare, applicare i gate, creare un contesto, registrare i metadati della sessione, eseguire il dispatch del turno dell’agente, finalizzare lo stato di consegna. Senza un kernel condiviso, una modifica al gate delle menzioni, alle risposte visibili solo per strumenti, ai metadati di sessione, alla cronologia in sospeso o alla finalizzazione del dispatch deve essere applicata per ogni canale. Il kernel mantiene deliberatamente separati quattro concetti:
  • ConversationFacts: da dove proviene il messaggio
  • RouteFacts: quale agente e quale sessione devono elaborarlo
  • ReplyPlanFacts: dove devono andare le risposte visibili
  • MessageFacts: quale corpo e quale contesto supplementare deve vedere l’agente
DM Slack, topic Telegram, thread Matrix e sessioni di topic Feishu li distinguono tutti nella pratica. Trattarli come un unico identificatore causa deriva nel tempo.

Ciclo di vita degli stadi

Il kernel esegue la stessa pipeline fissa indipendentemente dal canale:
  1. ingest — l’adapter converte un evento di piattaforma grezzo in NormalizedTurnInput
  2. classify — l’adapter dichiara se questo evento può avviare un turno dell’agente
  3. preflight — l’adapter esegue deduplicazione, self-echo, idratazione, debounce, decrittazione, precompilazione parziale dei fatti
  4. resolve — l’adapter restituisce un turno completamente assemblato (route, piano di risposta, messaggio, consegna)
  5. authorize — policy di DM, gruppo, menzione e comando applicata ai fatti assemblati
  6. assembleFinalizedMsgContext creato dai fatti tramite buildContext
  7. record — metadati della sessione inbound e ultima route persistiti
  8. dispatch — turno dell’agente eseguito tramite il dispatcher di blocchi bufferizzato
  9. finalizeonFinalize dell’adapter viene eseguito anche in caso di errore di dispatch
Ogni stadio emette un evento di log strutturato quando viene fornito un callback log. Vedi Osservabilità.

Tipi di ammissione

Il kernel non genera eccezioni quando un turno viene bloccato da un gate. Restituisce un ChannelTurnAdmission:
TipoQuando
dispatchIl turno viene ammesso. Il turno dell’agente viene eseguito e il percorso di risposta visibile viene esercitato.
observeOnlyIl turno viene eseguito end-to-end ma l’adapter di consegna non invia nulla di visibile. Usato per agenti osservatori broadcast e altri flussi multi-agente passivi.
handledUn evento di piattaforma è stato consumato localmente (ciclo di vita, reazione, pulsante, modale). Il kernel salta il dispatch.
dropPercorso di salto. Facoltativamente recordHistory: true mantiene il messaggio nella cronologia di gruppo in sospeso, così una menzione futura ha contesto.
L’ammissione può provenire da classify (la classe dell’evento ha indicato che non può avviare un turno), da preflight (deduplicazione, self-echo, menzione mancante con registrazione della cronologia) o da resolveTurn stesso.

Punti di ingresso

Il runtime espone tre punti di ingresso preferiti, così gli adapter possono optare per il livello che corrisponde al canale.
runtime.channel.turn.run(...)             // adapter-driven full pipeline
runtime.channel.turn.runPrepared(...)     // channel owns dispatch; kernel runs record + finalize
runtime.channel.turn.buildContext(...)    // pure facts to FinalizedMsgContext mapping
Due helper runtime precedenti restano disponibili per compatibilità con il Plugin SDK:
runtime.channel.turn.runResolved(...)      // deprecated compatibility alias; prefer run
runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer run or runPrepared

run

Usa quando il tuo canale può esprimere il proprio flusso inbound come ChannelTurnAdapter<TRaw>. L’adapter ha callback per ingest, classify facoltativo, preflight facoltativo, resolveTurn obbligatorio e onFinalize facoltativo.
await runtime.channel.turn.run({
  channel: "tlon",
  accountId,
  raw: platformEvent,
  adapter: {
    ingest(raw) {
      return {
        id: raw.messageId,
        timestamp: raw.timestamp,
        rawText: raw.body,
        textForAgent: raw.body,
      };
    },
    classify(input) {
      return { kind: "message", canStartAgentTurn: input.rawText.length > 0 };
    },
    async preflight(input, eventClass) {
      if (await isDuplicate(input.id)) {
        return { admission: { kind: "drop", reason: "dedupe" } };
      }
      return {};
    },
    resolveTurn(input) {
      return buildAssembledTurn(input);
    },
    onFinalize(result) {
      clearPendingGroupHistory(result);
    },
  },
});
run è la forma corretta quando il canale ha una logica di adapter ridotta e trae vantaggio dal possedere il ciclo di vita tramite hook.

runPrepared

Usa quando il canale ha un dispatcher locale complesso con anteprime, retry, modifiche o bootstrap di thread che deve restare di proprietà del canale. Il kernel registra comunque la sessione inbound prima del dispatch ed espone un DispatchedChannelTurnResult uniforme.
const { dispatchResult } = await runtime.channel.turn.runPrepared({
  channel: "matrix",
  accountId,
  routeSessionKey,
  storePath,
  ctxPayload,
  recordInboundSession,
  record: {
    onRecordError,
    updateLastRoute,
  },
  onPreDispatchFailure: async (err) => {
    await stopStatusReactions();
  },
  runDispatch: async () => {
    return await runMatrixOwnedDispatcher();
  },
});
I canali ricchi (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) usano runPrepared perché il loro dispatcher orchestra comportamenti specifici della piattaforma che il kernel non deve conoscere.

buildContext

Una funzione pura che mappa bundle di fatti in FinalizedMsgContext. Usala quando il tuo canale implementa manualmente parte della pipeline ma vuole una forma del contesto coerente.
const ctxPayload = runtime.channel.turn.buildContext({
  channel: "googlechat",
  accountId,
  messageId,
  timestamp,
  from,
  sender,
  conversation,
  route,
  reply,
  message,
  access,
  media,
  supplemental,
});
buildContext è utile anche dentro i callback resolveTurn quando si assembla un turno per run.
Gli helper SDK deprecati come dispatchInboundReplyWithBase passano ancora tramite un helper di turno assemblato. Il nuovo codice dei plugin dovrebbe usare run o runPrepared.

Tipi di fatti

I fatti che il kernel consuma dal tuo adapter sono agnostici rispetto alla piattaforma. Traduci gli oggetti della piattaforma in queste forme prima di passarli al kernel.

NormalizedTurnInput

CampoScopo
idID messaggio stabile usato per deduplicazione e log
timestampEpoch ms facoltativo
rawTextCorpo come ricevuto dalla piattaforma
textForAgentCorpo pulito facoltativo per l’agente (rimozione menzione, trim digitazione)
textForCommandsCorpo facoltativo usato per il parsing di /command
rawRiferimento pass-through facoltativo per callback adapter che richiedono l’originale

ChannelEventClass

CampoScopo
kindmessage, command, interaction, reaction, lifecycle, unknown
canStartAgentTurnSe false il kernel restituisce { kind: "handled" }
requiresImmediateAckSuggerimento per adapter che devono inviare ACK prima del dispatch

SenderFacts

CampoScopo
idID mittente stabile della piattaforma
nameNome visualizzato
usernameHandle se distinto da name
tagDiscriminatore in stile Discord o tag della piattaforma
rolesID dei ruoli, usati per il matching dell’allowlist dei ruoli dei membri
isBotTrue quando il mittente è un bot noto (il kernel lo usa per il drop)
isSelfTrue quando il mittente è l’agente configurato stesso
displayLabelEtichetta pre-renderizzata per il testo dell’envelope

ConversationFacts

CampoScopo
kinddirect, group o channel
idID conversazione usato per il routing
labelEtichetta leggibile per l’envelope
spaceIdIdentificatore facoltativo dello spazio esterno (workspace Slack, homeserver Matrix)
parentIdID conversazione esterna quando questa è un thread
threadIdID thread quando questo messaggio è dentro un thread
nativeChannelIdID canale nativo della piattaforma quando diverso dall’ID di routing
routePeerPeer usato per il lookup resolveAgentRoute

RouteFacts

CampoScopo
agentIdAgente che deve gestire questo turno
accountIdOverride facoltativo (canali multi-account)
routeSessionKeyChiave di sessione usata per il routing
dispatchSessionKeyChiave di sessione usata al dispatch quando diversa dalla chiave di route
persistedSessionKeyChiave di sessione scritta nei metadati di sessione persistiti
parentSessionKeyParent per sessioni ramificate/in thread
modelParentSessionKeyParent lato modello per sessioni ramificate
mainSessionKeyPin del proprietario DM principale per conversazioni dirette
createIfMissingConsenti allo step di registrazione di creare una riga di sessione mancante

ReplyPlanFacts

CampoScopo
toDestinazione logica della risposta scritta nel contesto To
originatingToDestinazione del contesto di origine (OriginatingTo)
nativeChannelIdID del canale nativo della piattaforma per la consegna
replyTargetDestinazione finale della risposta visibile se diversa da to
deliveryTargetOverride di consegna di livello inferiore
replyToIdID del messaggio citato/ancorato
replyToIdFullID citato in forma completa quando la piattaforma li ha entrambi
messageThreadIdID del thread al momento della consegna
threadParentIdID del messaggio padre del thread
sourceReplyDeliveryModethread, reply, channel, direct o none

AccessFacts

AccessFacts contiene i booleani necessari allo stadio di autorizzazione. La corrispondenza dell’identità resta nel canale: il kernel consuma solo il risultato.
CampoScopo
dmDecisione di consenso/associazione/rifiuto DM ed elenco allowFrom
groupCriterio di gruppo, consenso della rotta, consenso del mittente, allowlist, requisito di menzione
commandsAutorizzazione dei comandi tra gli autorizzatori configurati
mentionsSe il rilevamento delle menzioni è possibile e se l’agente è stato menzionato

MessageFacts

CampoScopo
bodyCorpo finale dell’envelope (formattato)
rawBodyCorpo inbound grezzo
bodyForAgentCorpo visto dall’agente
commandBodyCorpo usato per il parsing dei comandi
envelopeFromEtichetta del mittente pre-renderizzata per l’envelope
senderLabelOverride facoltativo per il mittente renderizzato
previewBreve anteprima oscurata per i log
inboundHistoryVoci recenti della cronologia inbound quando il canale mantiene un buffer

SupplementalContextFacts

Il contesto supplementare copre il contesto di citazione, inoltro e bootstrap del thread. Il kernel applica il criterio contextVisibility configurato. L’adapter del canale fornisce solo i fatti e i flag senderAllowed, così il criterio cross-channel resta coerente.

InboundMediaFacts

I media hanno la forma di fatti. Download dalla piattaforma, autenticazione, criterio SSRF, regole CDN e decrittazione restano locali al canale. Il kernel mappa i fatti in MediaPath, MediaUrl, MediaType, MediaPaths, MediaUrls, MediaTypes e MediaTranscribedIndexes.

Contratto dell’adapter

Per run completo, la forma dell’adapter è:
type ChannelTurnAdapter<TRaw> = {
  ingest(raw: TRaw): Promise<NormalizedTurnInput | null> | NormalizedTurnInput | null;
  classify?(input: NormalizedTurnInput): Promise<ChannelEventClass> | ChannelEventClass;
  preflight?(
    input: NormalizedTurnInput,
    eventClass: ChannelEventClass,
  ): Promise<PreflightFacts | ChannelTurnAdmission | null | undefined>;
  resolveTurn(
    input: NormalizedTurnInput,
    eventClass: ChannelEventClass,
    preflight: PreflightFacts,
  ): Promise<ChannelTurnResolved> | ChannelTurnResolved;
  onFinalize?(result: ChannelTurnResult): Promise<void> | void;
};
resolveTurn restituisce un ChannelTurnResolved, che è un AssembledChannelTurn con un tipo di ammissione facoltativo. Restituire { admission: { kind: "observeOnly" } } esegue il turno senza produrre output visibile. L’adapter possiede ancora la callback di consegna; semplicemente diventa una no-op per quel turno. onFinalize viene eseguito su ogni risultato, inclusi gli errori di dispatch. Usalo per svuotare la cronologia di gruppo in sospeso, rimuovere le reazioni di ack, fermare gli indicatori di stato e scaricare lo stato locale.

Adapter di consegna

Il kernel non chiama direttamente la piattaforma. Il canale passa al kernel un ChannelTurnDeliveryAdapter:
type ChannelTurnDeliveryAdapter = {
  deliver(payload: ReplyPayload, info: ChannelDeliveryInfo): Promise<ChannelDeliveryResult | void>;
  onError?(err: unknown, info: { kind: string }): void;
};

type ChannelDeliveryResult = {
  messageIds?: string[];
  threadId?: string;
  replyToId?: string;
  visibleReplySent?: boolean;
};
deliver viene chiamato una volta per ogni chunk di risposta nel buffer. Restituisci gli ID dei messaggi della piattaforma quando il canale li possiede, così il dispatcher può preservare gli ancoraggi del thread e modificare i chunk successivi. Per i turni di sola osservazione, restituisci { visibleReplySent: false } oppure usa createNoopChannelTurnDeliveryAdapter().

Opzioni di registrazione

Lo stadio di registrazione avvolge recordInboundSession. La maggior parte dei canali può usare i valori predefiniti. Esegui l’override tramite record:
record: {
  groupResolution,
  createIfMissing: true,
  updateLastRoute,
  onRecordError: (err) => log.warn("record failed", err),
  trackSessionMetaTask: (task) => pendingTasks.push(task),
}
Il dispatcher attende lo stadio di registrazione. Se la registrazione genera un’eccezione, il kernel esegue onPreDispatchFailure (quando fornito a runPrepared) e rilancia l’eccezione.

Osservabilità

Ogni stadio emette un evento strutturato quando viene fornita una callback log:
await runtime.channel.turn.run({
  channel: "twitch",
  accountId,
  raw,
  adapter,
  log: (event) => {
    runtime.log?.debug?.(`turn.${event.stage}:${event.event}`, {
      channel: event.channel,
      accountId: event.accountId,
      messageId: event.messageId,
      sessionKey: event.sessionKey,
      admission: event.admission,
      reason: event.reason,
    });
  },
});
Stadi registrati nei log: ingest, classify, preflight, resolve, authorize, assemble, record, dispatch, finalize. Evita di registrare nei log i corpi grezzi; usa MessageFacts.preview per brevi anteprime oscurate.

Cosa resta locale al canale

Il kernel possiede l’orchestrazione. Il canale possiede ancora:
  • Trasporti della piattaforma (gateway, REST, websocket, polling, webhook)
  • Risoluzione dell’identità e corrispondenza dei nomi visualizzati
  • Comandi nativi, comandi slash, completamento automatico, modali, pulsanti, stato vocale
  • Rendering di card, modali e adaptive card
  • Autenticazione dei media, regole CDN, media crittografati, trascrizione
  • API di modifica, reazione, oscuramento e presenza
  • Backfill e recupero della cronologia lato piattaforma
  • Flussi di associazione che richiedono verifica specifica della piattaforma
Se due canali iniziano ad avere bisogno dello stesso helper per uno di questi aspetti, estrai un helper SDK condiviso invece di inserirlo nel kernel.

Stabilità

runtime.channel.turn.* fa parte della superficie pubblica del runtime dei plugin. I tipi di fatto (SenderFacts, ConversationFacts, RouteFacts, ReplyPlanFacts, AccessFacts, MessageFacts, SupplementalContextFacts, InboundMediaFacts) e le forme di ammissione (ChannelTurnAdmission, ChannelEventClass) sono raggiungibili tramite PluginRuntime da openclaw/plugin-sdk/core. Si applicano le regole di compatibilità all’indietro: i nuovi campi dei fatti sono additivi, i tipi di ammissione non vengono rinominati e i nomi degli entry point restano stabili. Le nuove esigenze dei canali che richiedono una modifica non additiva devono passare attraverso il processo di migrazione dell’SDK dei plugin.

Correlati