Communication Subsystem
The platform communication subsystem delivers asynchronous output from running apps to connected clients. The default chat UI, app widgets, and custom frontends can consume the same typed envelopes over SSE or Socket.IO. App code can emit progress, deltas, files, tool lifecycle steps, custom events, and completion without owning the browser transport.
App REST operations and app-served MCP endpoints are sibling proc surfaces that sit next to this pipeline, not inside the event envelope itself. They can still emit communicator events during a request, but the underlying HTTP request/response or MCP exchange remains a separate surface.
The same communicator can record selected post-firewall envelopes into scoped buffers and hand batches to event sinks. The firewall runs first: an allowed event may be client-visible only or both visible and recorded; a blocked event is neither delivered nor recorded.
Do not confuse outbound streaming with the two inbound work buses. The Conversation Event Bus orders context for one user/conversation/agent lane. The Data Bus delivers app-owned domain mutations to declared handlers. Payload size and JSON shape do not choose between them. See Platform Architecture for the separation and Object Ecosystem & Ontologic Contracts for provider and surface contracts.
Filter / Firewall
Apps can allow or suppress outbound envelopes before delivery and recording. The Boolean filter does not transform the envelope and is fail-open if it raises, so it is useful for disclosure policy but must not be the only authorization boundary for secrets or durable business effects.
Broadcast / P2P Channels
The relay supports session-scoped pub/sub channels. Apps can broadcast to all subscribers of a room, or send point-to-point to a specific target_sid. The same mechanism powers streaming from proc to multiple ingress instances.
Scoped Recorder / Event Sinks
Selected post-firewall envelopes can be recorded into bounded, scoped buffers. Apps open JSON-serializable scopes with comm.record(...) or async with comm.recording(...), then send batches through send_recorded_events(...). Sinks are batch callbacks, not per-event waits on the hot path.
ChatCommunicator — App Producer API
Every app entrypoint receives a ChatCommunicator instance. It wraps Redis Pub/Sub with a typed API for outbound async events. Trusted child tool runtimes can carry a portable communicator specification and return events through the supervisor or side-file handoff; generated code does not receive Redis credentials or become an independent publisher. App-defined chat UIs, widgets, and custom frontends therefore see one stream contract across in-process and governed external execution.
External Events and Named-Service Events
external_events[] lets widgets, integrations, subagents, and services enter a conversation lane without pretending to be plain chat text. The plural transport kind is uniformly external_event; semantic meaning such as follow-up, steer, consent grant, or subagent completion remains nested in payload.event.type. Reactive ingress atomically stores the accepted batch and one bodyless wake. ReAct can fold eligible events into a live turn while it owns the listener; without a live owner they are handled by a later turn. A run-to-completion adapter consumes the batch available at turn start and does not inherit ReAct's mid-turn folding merely by using the shared harness timeline.
Event-source block production is total: every consumed lane event ends either folded or acknowledged as skipped. A zero-block event advances the processed cursor without adding a timeline block or invoking a generic post-block hook. react.block_production.no_timeline is only a visibility decision; required business durability belongs to the producing service or explicit source-owned processing.
When a ReAct action, result, or protocol rejection is emitted to the UI, it travels as a lifecycle envelope on the outbound communicator. This makes the Steps view a trace of proposed calls, validation, execution, results, and errors without making the browser stream the source of truth.
Files and Links Are Delivered Out of Band
During an authenticated chat turn, a result file is emitted as chat.files with an object reference. The browser resolves that reference at click time under the user's session; the model receives a delivery note, not a signed URL that it could re-type incorrectly. Conversation links use chat.citations, including placement="chat" when the link belongs in the visible answer. Turn-less MCP clients keep their transport contract: when no chat lane exists, the original URL-bearing result passes through unchanged.
Why ReAct Uses a Channel Protocol
KDCube ReAct does not depend on a model provider's native tool-calling envelope. It listens to runtime-owned semantic channels while the model streams. That lets one response interleave reasoning, code, actions, and other event blocks, including events the agent did not itself initiate.
Code is emitted as code, not escaped inside a JSON argument string. This matters for programs containing quotes, HTML, or nested JSON and reduces avoidable formatting failures. Because the runtime validates the channel protocol itself, models that were not trained for one provider's tool-calling format can still act as the ReAct decision model.
# Standard streaming lifecycle
await communicator.start() # chat_start event
await communicator.step("Searching…") # chat_step — visible progress
await communicator.delta("answer", chunk) # chat_delta — streaming text
await communicator.event(
event_type="chat.compaction",
data={"status": "completed"},
) # chat_compaction — context compaction status
await communicator.complete() # chat_complete
# Delta markers: answer · thinking · canvas · timeline_text · subsystem
await communicator.delta("thinking", reasoning_chunk)
await communicator.delta("canvas", json_payload)
# Custom service event — broadcast to all session subscribers
await communicator.event(
event_type="chat.service",
data={"key": "status.update", "value": "processing"},
)
# P2P — deliver only to a specific connected client (target_sid)
await communicator.event(
event_type="chat.service",
data={"key": "private", "value": result},
target_sid=sid, # omit for broadcast
)
# Error
await communicator.error("Something went wrong")
Scoped Recording and Event Sinks
Recording reuses the same event vocabulary as the outbound filter but at a different boundary. The filter allows or suppresses browser and recorder disclosure; recording decides whether an allowed envelope is copied into a bounded in-memory buffer for later sink delivery.
async with communicator.recording(
selector,
scope={"owner": "workflow"},
sink=event_sink,
send_on_exit=True,
):
await run_work()
Multiple scopes are additive. If a workflow scope and a tool scope match the same envelope, the recorded item carries both scopes. Platform child tool runtimes receive portable scopes through COMM_SPEC; child-added records return through comm_recorded_events.json and are sent by the host after merge.
Outbound Event Firewall (per app)
Apps can attach an IEventFilter to the workflow to suppress or gate outbound communicator envelopes before they reach Redis, clients, or recorders. The filter sees the caller's user_type, user_id, and full event metadata. Filters are fail-open: an exception allows the event through.
from kdcube_ai_app.apps.chat.sdk.comm import IEventFilter
class MyAppEventFilter(IEventFilter):
def allow_event(self, *, user_type, user_id, event, data=None) -> bool:
# hide internal step events from non-privileged users
if user_type != "privileged":
if event.type == "chat.step" and event.broadcast:
return False
return True
# Wire at entrypoint — passed to entrypoint factory via event_filter param
workflow = MyWorkflow(..., event_filter=MyAppEventFilter())
comm_recorded_events.json, the host merges it, and the host sends through the configured sink. Sink callbacks are not serialized into the child unless the child configures its own sink.See comm-system.md and README-comm.md for full detail.
Streaming vs Request-Scoped Surfaces
The communication pipeline carries asynchronous app events. It does not mean every app interface is itself a chat stream. KDCube currently exposes several adjacent surfaces, each with a different contract.
| Surface | Route family | What it carries | How it relates to the communication pipeline |
|---|---|---|---|
| SSE | /sse/stream + /sse/chat |
Realtime chat events and turn lifecycle | Native transport for the chat event envelope. |
| Socket.IO | Socket namespace / chat_message |
Same event model as SSE with a bidirectional socket transport | Native transport for the same chat event envelope. |
| App REST interfaces | /api/integrations/bundles/.../widgets, /operations, /public, /static |
Widget HTML, main-view assets, request/response APIs, webhook-style endpoints | Not themselves chat streams, but app code can emit communicator events while handling the request. |
| App-served MCP | /api/integrations/bundles/.../mcp/{alias} or /public/mcp/{alias} |
MCP HTTP traffic into an app-provided FastMCP or ASGI app | Separate request surface. Proc resolves and dispatches the HTTP request without wrapping it in chat lifecycle events. Authentication can be app-owned or a declared Connection Hub managed MCP guard. |
| Background jobs | Redis Stream namespace, no public HTTP route | Ready background work routed to an app @on_job handler |
Separate processor-claimed work surface. A producer such as @cron or a widget operation enqueues work, proc claims it fairly, builds app runtime context, and invokes async @on_job. It is not delivered to clients unless the app emits communicator events or writes user-visible results. |
An app operation or MCP request can still produce live UI updates when the app code explicitly emits communicator events. In that case the streaming delivery reuses the same SSE or Socket.IO session identified by auth state and optional KDC-Stream-ID.
@mcp(...) returns a FastMCP application or MCP-ready ASGI app. Proc resolves the alias and dispatches the original request under the surface's declared auth contract. The exchange remains request-scoped even if app code emits separate communicator events.App-served MCP transport contract
Route family, transport, and authority are separate choices. For MCP, responsibilities divide as follows:
- Proc: resolve the app route, apply any declared managed guard, preserve request context, and dispatch into the returned FastMCP or ASGI subapp.
- App: serve the MCP contract and, when auth is app-owned, verify the app-specific token, signature, header, or cookie.
- Managed guard: when configured, resolve the bearer handle to a server-side grant/session record, enforce the matching
resource_grantsand selected operation, and project the grantor identity before the handler runs. - Client: call the explicit route and send the credential required by that declared contract.
An external MCP credential is not a browser login token. With Delegated Access, it represents a delegated actor operating through a grantor. The token is only a handle; product code must not decode it as the authority source.
Typical authenticated MCP flow:
- the app declares either an app-owned or managed credential contract
- secrets or server-side grant records stay in their owning lifecycle
- the MCP client calls
/api/integrations/bundles/{tenant}/{project}/{bundle_id}/mcp/{alias} - the declared guard authenticates the request and resolves runtime identity
- proc dispatches the request to the MCP endpoint under that context
Typical public MCP flow:
- the client calls
/api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/mcp/{alias} - proc forwards the request into the app MCP endpoint
- the app either accepts it as public or still applies its own auth logic if that is the chosen contract
route="operations" versus route="public" selects the URL family. It does not by itself choose app-owned, managed, or intentionally public access. Use the surface's explicit auth declaration.App-authenticated public API hooks
Public app APIs can use several explicit auth ownership shapes:
- Proc-owned:
public_auth="none"orpublic_auth={"mode":"header_secret", ...} - App-owned:
public_auth="bundle" - Managed delegated access: a Connection Hub REST guard resolves server-side grants and projects the runtime user before product code runs.
For public_auth="bundle", there are three separate responsibilities:
- Proc: resolve the route, parse the request, and invoke the app method
- App: accept
request: Request, read the inbound headers/body, and decide whether the hook is valid - Client / webhook provider: call the explicit public operations route and send the auth material the app contract requires
Typical app-authenticated public hook flow:
- app props define a non-secret contract such as the header name
- app secrets store the verification material such as a shared token
- the caller sends
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{alias} - proc forwards the request into the app method
- the app verifies the request and either raises
401/403or returns normally
@api(..., route="public", public_auth="bundle"), proc no longer acts as the token verifier. The app defines the header/token contract and enforces it inside the method itself.