$dshubuser@dshub:~/plugins$
  • Plugins
  • Submit
  • Blog
waterfall: Intercept, Wrap, and Short-Circuit with next()
2026/08/26

waterfall: Intercept, Wrap, and Short-Circuit with next()

Follow a Cordis waterfall chain to see listeners delegate downstream, wrap results, or short-circuit when they own the decision.

Remember one rule: an observer must call next(); only a listener that owns the decision should omit it and return directly.

waterfall is not a broadcast. Each listener receives the arguments and a continuation. It can delegate to the default behavior or wrap the result that comes back from downstream.

01 / The chain is nested functions

Cordis waterfall listeners wrapping a downstream default function from outer to inner
import type { Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Events {
    'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
  }
}

export async function apply(ctx: Context) {
  ctx.on('demo/transform', async (input, next) => {
    const downstream = await next()
    return downstream.toUpperCase()
  })

  ctx.on('demo/transform', async (input, next) => {
    if (input.includes('blocked')) return '** blocked **'
    return next()
  })

  const input = 'hello'
  const result = await ctx.waterfall('demo/transform', input, async () => input)
  console.log(result)
}

The innermost function is the default behavior. Listeners wrap around it in registration order, and next() is what moves execution downstream.

02 / Wrapping and short-circuiting are different intents

Cordis waterfall decision diagram showing delegate, wrap, and short-circuit outcomes
  • Delegate: the listener does not own the result, so it calls next().
  • Wrap: it awaits next() and transforms the result, such as uppercasing or adding diagnostics.
  • Short-circuit: it owns the decision, skips next(), and returns a replacement.

In the example, hello reaches the default behavior and becomes HELLO; blocked words returns ** blocked ** at the policy listener, so the default function never runs.

03 / Forgetting next() silently consumes the default

ctx.on('agent/request', async (request, next) => {
  console.log('observed request')
  return next()
})

If an observer omits next(), its return value ends the entire waterfall. A downstream model request, tool execution, or default policy may never happen without looking like an exception.

Calling or not calling next is therefore a responsibility declaration: omission means veto or replacement; calling it means delegation.

04 / Two DSH decision points

DeepSeek Harness uses waterfall for replaceable policy:

  • agent/request lets a plugin replace model-request configuration or wrap it before the provider.
  • approval/request lets a policy answer for the user, while ordinary observers can delegate downstream.

The owning service may define its own error boundary: for example, approval/request normalizes an answerer exception into an unavailable result. That behavior is not universal to every waterfall event.

The policy does not need to be embedded in the agent loop. A plugin attaches to the event, and waterfall semantics make result ownership explicit.

05 / Ask three questions before using it

  1. What is the default behavior? Write it as the innermost next function.
  2. Is this listener observing, or does it own the replacement decision? Observers must delegate.
  3. Should the result be wrapped? If yes, transform it after await next() instead of bypassing downstream.

Answering these questions turns waterfall into a readable middleware chain instead of an opaque callback list.

Where to go next

  • Events: Let Plugins Cooperate Without Knowing Who Listens
  • Configuration: Options with Validation
  • Cordis Inside DeepSeek Harness

Sources: events tutorial, waterfall semantics in the primer, and Events implementation.

All Posts

Author

avatar for DSH Research
DSH Research

Categories

  • Agent Infrastructure
01 / The chain is nested functions02 / Wrapping and short-circuiting are different intents03 / Forgetting next() silently consumes the default04 / Two DSH decision points05 / Ask three questions before using itWhere to go next

More Posts

Services and Context: Share Capabilities Without Binding Implementations
Agent Infrastructure

Services and Context: Share Capabilities Without Binding Implementations

Use Service, Context, and declaration merging to understand how providers, consumers, and scopes connect DeepSeek Harness capabilities.

avatar for DSH Research
DSH Research
2026/08/22
Cordis: Turning a Running Application into a Reversible Plugin Tree
Agent Infrastructure

Cordis: Turning a Running Application into a Reversible Plugin Tree

Follow cordis.yml, Loader, and Context to see how Cordis composes, replaces, and safely tears down runtime capabilities in DeepSeek Harness.

avatar for DSH Research
DSH Research
2026/08/18
Configuration: Give Plugins Validated Options and Defaults
Agent Infrastructure

Configuration: Give Plugins Validated Options and Defaults

Define Cordis plugin configuration with Schemastery and see how defaults, path-aware errors, and FAILED Fibers prevent a sick start.

avatar for DSH Research
DSH Research
2026/08/27

Newsletter

Join the community

Subscribe to our newsletter for the latest news and updates

[dshub] [zsh]$ dshub ls$ dshub submit$ dshub blog
-- NORMAL -- ✓ 2026 dshub