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
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.
| Mode | Verteilungsmethode | Ob gewartet werden soll | Sequenz | Ob ein Rückgabewert vorhanden ist | Typische Szenarien |
|---|---|---|---|---|---|
| emitRundruf | ctx.emit | nein | In Registrierungsreihenfolge | nein | Benachrichtigungstyp: alle Listener beobachten |
| bailKurzschluss | ctx.bail | nein | In Registrierungsreihenfolge | Ja | Entscheidungstyp: der erste gültige Rückgabewert gewinnt |
| serialSequenz | ctx.serial | Ja | In Registrierungsreihenfolge | Ja | Phasenweise Initialisierung: in Reihenfolge ausführen und warten |
| waterfallPipeline | ctx.waterfall | nein | In Registrierungsreihenfolge | Ja | Verarbeitungskette: Rückgabewerte nachgelagerter Ebenen Schicht für Schicht umhüllen |
| parallelParallelität | ctx.parallel | Ja | Alle parallel | nein | Fan-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
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
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
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
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
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:
| Ereignisse | Was ist es | Wie man es beobachtet |
|---|---|---|
| agent/step, tools/result, usw. | Cordis-Ereignisse, in Echtzeit verteilt | Direkt ctx.on('tools/result', ...) |
| turn/*, step/*, tool/call, tool/result, compaction/* | Persistierte Session-Ereignistypen | session/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
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:
- Welche Methode löst jeden der fünf Dispatch-Modi aus? Welche haben Rückgabewerte?
- Was passiert, wenn ein Waterfall-Listener next() nicht aufruft? Ist das ein Fehler oder Absicht?
- Bei persistenten Sitzungsereignissen wie turn/start, welches Ereignis sollte abgehört werden und welches Feld sollte geprüft werden?