Skip to content

feat: add EventThrottleMiddleware for stream notification throttling - #1423

Merged
mxmzb merged 14 commits into
mainfrom
feat/notification-throttle-ms-v2
Apr 2, 2026
Merged

feat: add EventThrottleMiddleware for stream notification throttling#1423
mxmzb merged 14 commits into
mainfrom
feat/notification-throttle-ms-v2

Conversation

@mxmzb

@mxmzb mxmzb commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds EventThrottleMiddleware — a new middleware that coalesces rapid subscriber notifications during streaming
  • intervalMs: time-based throttle cap (e.g. 16 = ~60fps)
  • minChunkSize: character-based cap (e.g. 20 = batch single-char tokens until 20 chars accumulate)
  • When both are set, notification fires when either threshold is hit first
  • Bufferable events (text chunks, tool call chunks, state snapshots, etc.) are batched and coalesced; lifecycle events (RUN_STARTED, TOOL_CALL_START, etc.) flush the buffer and pass through immediately
  • Coalesces consecutive chunk events with the same ID (messageId/toolCallId) into a single event with merged deltas
  • intervalMs: 0 with no minChunkSize is a no-op passthrough (zero overhead)

Replaces #1420 — clean branch without #1385 contamination.

Test plan

  • Default behavior: every mutation fires through 1:1 (no middleware)
  • intervalMs: fewer notifications than chunks, final delta content complete
  • minChunkSize: holds notifications until N chars accumulate, final delta content complete
  • Combined thresholds: fires when either threshold is hit first
  • Leading edge: first buffered event fires immediately
  • Immediate events (TOOL_CALL_START) flush pending buffer before passing through
  • Trailing timer: pending events flush after the time window
  • Stream completion flushes remaining buffer
  • Stream error discards buffer and propagates error
  • Validation: rejects negative, NaN, Infinity for both config options
  • Coalescing: merges consecutive same-ID chunks (text, tool call, reasoning)
  • Does not coalesce chunks with different IDs
  • Multiple runs on same middleware get fresh throttle state
  • Unsubscribe and error both clear pending trailing timers

🤖 Generated with Claude Code

…ractAgent

Add configurable notification throttling to AbstractAgent that batches
subscriber notifications during high-frequency event streams. Includes
notifySubscribers helper for safe, deduplicated subscriber dispatch,
throttle config types, and comprehensive tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mxmzb
mxmzb requested a review from a team as a code owner April 2, 2026 16:48
@vercel

vercel Bot commented Apr 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ag-ui-dojo Ready Ready Preview, Comment Apr 2, 2026 8:14pm

Request Review

@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1775160689 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1775160689' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1775160689' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1775160689' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1775160689' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1775160689' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1775160689' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1775160689

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 98b5377

@pkg-pr-new

pkg-pr-new Bot commented Apr 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@1423

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@1423

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@1423

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@1423

@ag-ui/middleware-starter

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/middleware-starter@1423

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@1423

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@1423

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@1423

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@1423

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@1423

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@1423

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@1423

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@1423

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@1423

@ag-ui/langroid

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langroid@1423

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@1423

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@1423

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@1423

@ag-ui/server-starter

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/server-starter@1423

@ag-ui/server-starter-all-features

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/server-starter-all-features@1423

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@1423

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@1423

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@1423

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@1423

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@1423

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@1423

commit: b606488

mxmzb and others added 3 commits April 2, 2026 18:59
…er snapshots

- Apply notifySubscribers to onInitialize/onError/onFinalize (6 bare
  forEach calls → isolated per-subscriber error handling)
- Replace streamErrored flag with streamCompleted (track success, not
  failure — fixes edge case where error inside tap.next bypasses tap.error)
- Remove broad catch from notify() — bookkeeping errors are programming
  bugs that should propagate; notifySubscribers already isolates subscriber errors
- Add .catch() to void this.onFinalize() to prevent unhandled rejections
- Snapshot subscriber list in addMessage/addMessages/setMessages/setState
  to prevent mid-iteration mutation
- Add .catch() to all fire-and-forget async IIFEs
- Wrap scheduleTrailing timer callback defensively
- Add debug logs for disposed guard, stream error discard, and trailing timer failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ix JSDoc

- Add ResolvedNotificationThrottleConfig with required minChunkSize
  (mirrors AgentDebugConfig/ResolvedAgentDebugConfig pattern)
- Extract resolveNotificationThrottleConfig() to types.ts, co-located
  with the type (replaces inline validation in constructor)
- Add readonly modifiers to interface fields
- Fix intervalMs JSDoc: "only the latest state" → "single coalesced
  notification reflecting all accumulated mutations"
- Fix minChunkSize JSDoc: "active assistant message" → "trailing
  assistant message (last in array, with string content)"
- Add finalize flush clarification to interface JSDoc
- Re-export ResolvedNotificationThrottleConfig and resolver from index.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Combined messages + state mutations under single throttle window
- intervalMs: 0 with minChunkSize streaming behavior
- Multiple sequential runAgent on same throttled agent
- Non-string content fallback with minChunkSize

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Updated agent message addition to use 'as const' for role type safety.
- Introduced utility functions to extract assistant message content, reducing duplication in notification throttle tests.
- Refactored message handling in tests to utilize new helper functions for clarity and maintainability.
…rottle tests

Eliminate ~50 `as BaseEvent` casts via centralized event factories and
extract repeated setup/lifecycle code into startRun/runToCompletion helpers.
700 lines → 439 lines with identical test semantics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rove docs

- notifySubscribers now handles MaybePromise<void> rejections (critical:
  async subscribers previously caused unhandled promise rejections)
- Wrap throttle finalize in try/finally so disposed=true is always set
- Add disposed guard in trailing timer callback
- Add debug logger to fire-and-forget .catch() handlers for consistency
- Fix non-string content test to use MESSAGES_SNAPSHOT with tool-call-only msg
- Add tests: disposed suppression, timer cleanup on error, async rejections
- Improve JSDoc: leading-edge semantics, intervalMs:0 behavior, undefined content

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mxmzb and others added 5 commits April 2, 2026 20:37
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-based tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The throttle logic now lives in @ag-ui/event-throttle-middleware instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t, extend coalescing, add type guards

- Invert IMMEDIATE_EVENT_TYPES → BUFFERABLE_EVENT_TYPES allowlist so new
  protocol event types default to immediate passthrough (safer for lifecycle events)
- Extend coalescing to TOOL_CALL_CHUNK and REASONING_MESSAGE_CHUNK (previously
  buffered but not coalesced, defeating the middleware's purpose)
- Replace `as any` casts with typed guards (isTextChunk, isToolCallChunk,
  isReasoningChunk) and prevent coalescing chunks with undefined IDs
- Fix incorrect comment: "leading-edge treatment" → "throttle window restarts"
- Wrap timer-triggered flush() in try-catch to prevent unhandled global exceptions
- Add teardown tests: unsubscribe with pending timer, error with pending timer
- Fix error test: assert error propagation instead of swallowing with catch()
- Relax rxjs peer dep from exact "7.8.1" to "^7.0.0"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mxmzb mxmzb changed the title feat: add notificationThrottleMs and notificationMinChunkSize to AbstractAgent feat: add EventThrottleMiddleware for stream notification throttling Apr 2, 2026
@mxmzb
mxmzb merged commit 72b35da into main Apr 2, 2026
29 checks passed
@mxmzb
mxmzb deleted the feat/notification-throttle-ms-v2 branch April 2, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants