Condor SSE / AG-UI Frame Pump — Emitter → Mapper → Wire → Consumer
Audiences: developer, internal
The pump is how agent work becomes pixels. Condor's runtime drains the Mastra / AI-SDK stream, mints typed
data-*frames at the tool edge, projects them into AG-UI events, and ships them over a dedicated raw RFC 8895 SSE transport that is deliberately not the HMS@ddx/sse-contractbus. The subsystem isddx-api/src/ai/condor/{pump,event-bus}/(19 files) plus the four top-levelagui-*files that own framing and projection.
The frame path
Two things ride the same emitter but reach the browser by different roads:
the AI-SDK UIMessage data-* part goes straight through
pipeUIMessageStreamToResponse to useChat, while the AG-UI projection above
feeds the inspector on /agui/live. data-intent is the canonical example —
the mapper forwards it verbatim precisely so neither surface loses it
(agui-orchestrator-mapper.ts:440-451).
⚠ The prefix rule — data-condor-* means two different things
This is the load-bearing contract of the whole topic. Getting it backwards produces a frame that is tsc-green, lint-green, grep-green, and renders an empty panel in the browser. The mapper handles frames through two disjoint routes, and the correct prefix depends on which route your frame takes.
Class 1 — explicitly-cased frames: data-condor-* is CORRECT
The settled tool / doc / plan frame set is hand-mapped by an explicit case
block, which strips and re-maps the type deliberately:
Emitted frame (condor-tool-stream-emitter.ts:95-126) | Mapper | Projected AG-UI name |
|---|---|---|
data-condor-tool-started | :298 | condor.tool.started (+ ToolCallStart) |
data-condor-tool-progress | :325 | condor.tool.progress |
data-condor-tool-completed | :331 | condor.tool.completed |
data-condor-tool-failed | :349 | condor.tool.failed |
data-condor-tool-card | — | ToolCard |
data-condor-doc-generation-start | :374 | condor.document.* |
data-condor-plan-revised | :419 | condor.plan-revised |
data-condor-run-suspended | :487 | HITL suspend frame |
For every one of these, data-condor- is the real, live prefix. Do not "fix"
them.
Class 2 — fallthrough frames: data-condor-* is a BUG
Any frame type without a case falls through to the generic projector:
name: `condor.${type.slice('data-'.length)}` // :433, :457, :549
That expression strips only data-. It does not strip condor-. So a new
frame emitted as data-condor-foo projects to condor.condor-foo, and a
consumer subscribing to condor.foo matches nothing and renders empty.
Live proof it already happened once: condor.condor-run is a real,
surviving, mis-prefixed channel the frontend had to accommodate rather than
rename (ddx-web/src/lib/condor/agui-artifacts.ts:443-445). The sub-agent lane
hit the same trap on 2026-07-23 and was fixed the correct way — its frames are
data-subagent-spawn / -complete / -failed, with the reasoning recorded
inline at condor-tool-stream-emitter.ts:103-115, and the web lane subscribes
to condor.subagent-* (ddx-web/src/lib/condor/agui-types.ts:240-242).
Class 3 — payload-named cased frames: data-condor-* is CORRECT (different reason)
A third route exists and is the one an author is most likely to copy when adding
a new data-condor-* frame — so it must not be mistaken for Class 2. The
capability / plan-mutation frames have an explicit case but derive their channel
name from the payload, falling back to a slice that strips the FULL data-condor-
prefix (not just data-):
case 'data-condor-capability':
case 'data-condor-plan-mutation': {
const eventName = str(d.event) ?? `condor.${type.slice('data-condor-'.length)}` // :470-473
slice('data-condor-'.length) strips data-condor-, so data-condor-capability
→ condor.capability — NO doubling. This is safe BECAUSE it has an explicit case
with a longer slice; it is NOT the generic Class-2 fallthrough. Copying this block's
data-condor-* shape into a frame that then hits the Class-2 fallthrough (no case)
is exactly how the doubling bug is introduced.
RULE. The prefix depends on the ROUTE, not the name:
- Explicit
case, re-mapped or full-prefix-sliced (Class 1 & 3) →data-condor-*is correct.- Generic fallthrough, no
case(Class 2) → must bedata-<suffix>with nocondor-, or it double-prefixes tocondor.condor-*.Before adding a frame: is there a
casefor it? If yes and it slices'data-condor-'.lengthor re-maps explicitly →data-condor-*. If it will fall through the generic projector →data-<suffix>. When unsure, trace the exact.slice(...)your type hits. Verify in a browser — neither the type-checker nor a grep can catch this class.
The two-bus model — and the inert half
Condor carries two event surfaces, and they are not peers:
- The outward AG-UI/SSE transport (live).
agui-sse.helper.tsframes raw RFC 8895 (id:/event:/data:),agui-event-store.service.tspersists a per-run monotonicseqforLast-Event-IDreplay, andGET runs/:runId/agui/liveserves it through a 3-tier resolver (durable store → live registry claim → span replay). - The internal EDA bus (degraded, observability-only).
event-bus/event-bus.bridge.tsis a module-level seam letting non-DI code (Mastra tool wrappers) publish without a NestJS dependency. As verified 2026-07-18, nothing callssetEventBusPublisher—publisherstays null for the process lifetime, sopublishBusEventSafe,publishBusEventDualSafeandEventBusFacade.publishare all silent no-ops. Callers are written to tolerate this, so it is degraded rather than broken. Note the asymmetry:EventBusFacadeitself is not dormant — itstryClaim()is live and backs plan dedupe inpump/phases/plan-phase.ts.
The practical consequence: if you need a frame to reach the UI, emit it
through the pump/emitter path, never through the event bus. Do not read the
presence of the bridge seam as evidence a publisher exists — grep for call
sites of setEventBusPublisher, not mentions of it.
Pump phases
pump/ splits the drain into phase files rather than one loop:
dispatch-phase.ts (turn entry), plan-phase.ts (plan emission + facade-backed
dedupe), suspend-resume-phase.ts + hitl-suspend.ts +
enqueue-suspend-frame.ts (the HITL gate — see
Condor HITL Approval), finalize-phase.ts and
emit-run-recap.ts (terminal frames), with zero-execution-guard.ts catching
plans that would produce no work. pump-context.ts, run-knobs.ts and
stream-readers.ts carry the shared per-run state and stream adapters.
Relationship to @ddx/sse-contract
@ddx/sse-contract (4.1.0) is the HMS global SSE bus — canonical event
registry, SseEnvelope v3, SSEChannels resolvers. The Condor AG-UI inspector
wire is a deliberately disjoint transport: /agui/live serves raw RFC 8895
frames and is not wrapped in SseEnvelope/SSEChannels, because each resolver
tier owns its own id: cursor (integer seq on the durable path, ISO-8601
startedAt on the span-replay path) and an envelope wrap would break replay.
This exemption is asserted in the helper header (agui-sse.helper.ts:10-14) and
in the controller invariants.
That exemption is scoped to this one transport. Everything else in HMS —
including any Condor code publishing to the platform bus — still MUST use
@ddx/sse-contract: no raw agent:session-${id} literals, no _meta v2
envelopes, no new v3-gating flags. See
SSE Event Engine.
Related Topics
- Condor HITL Approval — suspend/approve frames ride this same pump and event-bus wire.
- Condor Engine — the runtime, controller and 3-tier
/agui/liveresolver this pump feeds. - SSE Event Engine — the
@ddx/sse-contractglobal bus this transport is deliberately exempt from. - FlowAgent AG-UI Bridge — the sibling AG-UI projection in the voice/flow estate.