DeepSeek-Harness: Ereignissystem: emit / bail / serial / waterfall

Wie kommunizieren Plugins auf lose gekoppelte Weise? Events sind der zentrale Kommunikationsmechanismus von Cordis.

Harness nutzt Events umfangreich, um erweiterbare Erweiterungspunkte zu implementieren. Dieser Artikel erklärt die fünf Dispatch-Modi klar.


Grundlegende Verwendung

Events sind in zwei Enden aufgeteilt: Zuhören und Auslösen.

Beispiel

// Auf Events hören: Callback registrieren
ctx.on('event-name', (payload) => {
  // Events behandeln
})

// Ereignis auslösen: an alle Listener senden
ctx.emit('event-name', payload)

Listener, die über `ctx.on` registriert wurden, werden automatisch entfernt, wenn das Plugin entladen wird.


Fünf Verteilungsmodi

Cordis bietet mehrere Dispatch-Modi, geeignet für unterschiedliche Interaktionsverträge.

五种分发模式行为示意图

ModeVerteilungsmethodeOb gewartet werden sollSequenzOb ein Rückgabewert vorhanden istTypische Szenarien
emitRundrufctx.emitneinIn RegistrierungsreihenfolgeneinBenachrichtigungstyp: alle Listener beobachten
bailKurzschlussctx.bailneinIn RegistrierungsreihenfolgeJaEntscheidungstyp: der erste gültige Rückgabewert gewinnt
serialSequenzctx.serialJaIn RegistrierungsreihenfolgeJaPhasenweise Initialisierung: in Reihenfolge ausführen und warten
waterfallPipelinectx.waterfallneinIn RegistrierungsreihenfolgeJaVerarbeitungskette: Rückgabewerte nachgelagerter Ebenen Schicht für Schicht umhüllen
parallelParallelitätctx.parallelJaAlle parallelneinFan-out: Mehrere Listener verarbeiten parallel

Jedes Ereignis hat genau einen Dispatch-Modus und kann nur über die entsprechende Methode dispatcht werden.

Die offizielle Einstiegsdokumentation kennzeichnet `waterfall` als „not await“; da Listener jedoch üblicherweise async-Funktionen sind, schreiben Tutorial-Beispiele häufig `await ctx.waterfall(...)`, um auf das Endergebnis der gesamten Verarbeitungskette zu warten. Merken Sie sich einfach diesen Unterschied – beide Stile sind akzeptabel.


emit: Broadcast

Alle Listener werden synchron ausgeführt und Rückgabewerte werden ignoriert.

Beispiel

// Auslöserseite: eine 'ready'-Nachricht senden
ctx.emit('my-plugin/ready', { id: 'example-worker-1' })

// Listener: empfängt Ready-Nachricht, gibt Log aus
ctx.on('my-plugin/ready', ({ id }) => {
  console.log(`${id} ist bereit`)
})

bail: Kurzschluss

Listener werden in Reihenfolge ausgeführt; der erste Rückgabewert, der nicht `null`, `false` oder `undefined` ist, wird zum Endergebnis.

Beispiel

// Dispatcher: eine Prüfung durchführen, das erste Ergebnis mit einem Einwand übernehmen
const result = ctx.bail('some-check', input)

// Listener: wenn die Abfangbedingung erfüllt ist, 'blocked' zurückgeben, andernfalls fortfahren
ctx.on('some-check', (input) => {
  if (shouldBlock(input)) return 'blocked'
  // null / false / undefined zurückgeben, um 'Ich habe keinen Einwand' anzuzeigen, damit nachfolgende Listener fortfahren können.
})

serial: sequenziell ausführen

Listener werden einer nach dem anderen in der Registrierungsreihenfolge ausgeführt und warten auf asynchrone Ergebnisse.

Der erste Rückgabewert, der nicht null, false oder undefined ist, beendet die nachfolgende Ausführung.

Beispiel

// Führt eine phasenweise Initialisierung nacheinander aus und wartet, bis jede Phase abgeschlossen ist
await ctx.serial('setup-phase', context)

waterfall: Pipeline

Jeder Listener kann den Rückgabewert der nachgelagerten Listener umhüllen und so eine Verarbeitungskette bilden.

Listener erhalten (...args, next); der Aufruf von next() führt nachgelagerte Listener aus, und der Rückgabewert der nachgelagerten Listener wird über next() an die aktuelle umhüllende Schicht zurückgegeben.

Beispiel

// Dispatcher: Der Anfangswert ist input und wird an den ersten Listener übergeben
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)

// Listener-Seite: next() muss aufgerufen werden; nach Erhalt des nachgelagerten Ergebnisses dieses einmal verarbeiten und dann zurückgeben.
ctx.on('my-plugin/transform', async (_input, next) => {
  const downstream = await next()
  return downstream.trim()
})

Waterfall-Listener müssen next() aufrufen. Wird next() nicht aufgerufen, bricht dies die gesamte Pipeline ab; dies ist ein absichtliches Design zur Implementierung von Interception- oder Gateway-Logik.

Strategy-Listener können, wenn sie Entscheidungsbefugnis besitzen, direkt zurückkehren, ohne next() aufzurufen, und blockieren dadurch die gesamte Kette.

Listener, die nur annotieren oder beobachten, müssen an den Downstream delegieren.


Typsichere Events

Verwenden Sie TypeScript-Declaration-Merging, um Typsicherheit für Events bereitzustellen.

Beispiel

// Dateipfad: scratch-plugin/src/events.ts
import '@deepseek-ai/cordis'

// Declaration Merging: Eventnamen und Signatur registrieren
declare module '@deepseek-ai/cordis' {
  interface Events {
    'my-plugin/ready': (payload: { id: string }) => void
    'my-plugin/check': (input: string) => boolean | undefined
    'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
  }
}

// Danach: ctx.on('my-plugin/ready', ...) und ctx.emit('my-plugin/ready', ...)
// Argumente werden automatisch abgeleitet; falsch geschriebene Ereignisnamen oder Parametertypen führen zu Kompilierzeitfehlern.

Cordis-Ereignisse und Sitzungsdatensätze

Die Cordis-Ereignisse von Harness folgen der Namespace/Action-Benennung, z. B. agent/step, agent/request, agent/request-error, tools/result und session/event.

Beachte den Unterschied zwischen zwei Arten von Ereignissen:

EreignisseWas ist esWie man es beobachtet
agent/step, tools/result, usw.Cordis-Ereignisse, in Echtzeit verteiltDirekt ctx.on('tools/result', ...)
turn/*, step/*, tool/call, tool/result, compaction/*Persistierte Session-Ereignistypensession/event abhören, event.type prüfen

turn/*, step/*, tool/call, tool/result und compaction/* sind persistente Session-Ereignistypen, nicht die gleichnamigen Cordis-Ereignisse.

Um sie zu beobachten, session/event abhören und event.type prüfen.


Praxisbeispiel: Logging-Plugin

Die offizielle Dokumentation verwendet das folgende Plugin, um Tool-Aufrufe und Tool-Ergebnisse zu protokollieren, das auf das Cordis-Ereignis `tools/result` hört.

Beispiel

// Dateipfad: scratch-plugin/src/tool-logger.ts
import type { Context } from '@deepseek-ai/cordis'
import '@deepseek-ai/dsh-tools'

export const name = 'tool-logger'

export function apply(ctx: Context) {
  // Auf das tools/result-Ereignis hören: wird jedes Mal ausgelöst, wenn eine Tool-Ausführung abgeschlossen ist.
  ctx.on('tools/result', (exec, result) => {
    // Tool-Namen und Argumente ausgeben
    console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
    // Die Textblöcke im Ergebnisinhalt verketten und nur die ersten 100 Zeichen ausgeben.
    const text = result.content
      .map(block => block.type === 'text' ? block.text : '')
      .join('')
    console.log(`[tool result] ${text.slice(0, 100)}`)
  })
}

Dieses Plugin gibt nach jedem Tool-Aufruf durch den Agenten den Tool-Namen, die Parameter und eine Ergebniszusammenfassung im Terminal aus.

Listener werden über `ctx.on` registriert und automatisch entfernt, wenn das Plugin entladen wird.


Zusammenfassung Selbsttest

Die fünf Dispatch-Modi decken fünf Arten von Verträgen ab: Broadcast, Decision, Sequential, Pipeline und Parallel; Waterfall muss next() aufrufen; declare-module-Zusammenführung bietet Typgarantien für Ereignisnamen und Parameter.

Testen Sie sich selbst:

  1. Welche Methode löst jeden der fünf Dispatch-Modi aus? Welche haben Rückgabewerte?
  2. Was passiert, wenn ein Waterfall-Listener next() nicht aufruft? Ist das ein Fehler oder Absicht?
  3. Bei persistenten Sitzungsereignissen wie turn/start, welches Ereignis sollte abgehört werden und welches Feld sollte geprüft werden?
Weitere Erweiterungen