@cool-ai/beach-starter
v1.5.0
Published
Canonical pipeline handlers and routing template — the working shape for a Beach application.
Downloads
757
Readme
@cool-ai/beach-starter
Canonical pipeline handlers and routing template for Beach applications. Owns the reference wiring that turns the router and its session primitives into a working application — channel-blind interior, reply delivery at the router layer.
Home: cool-ai.org · Documentation: cool-ai.org/docs
Three consumer projects rediscovered the same canonical-handlers pattern independently. This package extracts it so new consumers inherit the working shape rather than rediscovering it.
Install
npm install @cool-ai/beach-starterPeer dependencies: @cool-ai/beach-core, @cool-ai/beach-llm.
A configured starting point — including a sample concierge handler, an example-lookup tool, and an sse streaming channel pre-wired for this pipeline — comes from @cool-ai/beach-config:
npm install @cool-ai/beach-config
npx beach-config init --with-starterThe flag lays down config/handlers/concierge.yaml, config/tools/example-lookup.yaml, and config/channels/sse.yaml alongside the regular config scaffold. Replace the placeholders with the application's real concierge and tools.
The canonical pipeline
channel inbound
→ channel:message_received (channel-blind first-layer event;
inbound adapter has resolved threadId,
personId, destinations on the session)
→ [channel-router resolves the inbound to session:request:
sessionId from threadId, mints the correlation eventId]
→ session:request ({ sessionId, eventId, parts })
→ YOUR_HANDLER (router.register; deterministic or LLM-backed,
runs its work, replies by routing
session:reply — channel-blind)
→ [router delivers the reply to each session.destination]
→ delivery:ui_streaming OR delivery:formatter
→ YOUR delivery handler (wired in your routing config; a thin route
to your streaming transport, or a MissiveStore
write for a batched Composer)Multi-channel replies (a client emailed and texted; reply via both) work without channel-branching code anywhere — the inbound adapter populates two destinations on the session and the router delivers the reply to each after the handler settles.
Quick start — channel-blind application
import { EventRouter } from '@cool-ai/beach-core';
import { createLLMHandler } from '@cool-ai/beach-llm';
import { InMemoryMissiveStore } from '@cool-ai/beach-missives/stores';
import routingConfig from '@cool-ai/beach-starter/templates/routing.json' with { type: 'json' };
const router = new EventRouter();
const store = new InMemoryMissiveStore();
// 1. Build the orchestrator handler. createLLMHandler returns an EventHandler;
// a deterministic handler is the same type.
const concierge = createLLMHandler({ config: handlerConfig, provider, registry: tools });
// 2. Register your orchestrator. Inbound resolution to session:request is the
// channel-router's job — the starter ships no inbound handler.
router.register('concierge', concierge);
// Wire each delivery event to your own outbound path. The starter ships no
// reference delivery handlers — they are thin wrappers over existing components:
// delivery:ui_streaming → your streaming transport (e.g. SSE/Redis publish)
// delivery:formatter → a handler that writes a draft to the MissiveStore
router.register('ui-stream', async (event) => {
const { sessionId, parts } = event.data as { sessionId: string; parts: unknown };
await redis.publish(`reply:${sessionId}`, JSON.stringify(parts));
});
// 3. Load the routing config (point the orchestrator placeholder at 'concierge' first).
router.loadRoutingConfig(routingConfig);
// 4. The inbound adapter opens each session with its destination set.
// A chat session opens with a ui-streaming destination:
router.openSession({
id: sessionId,
destinations: [{ kind: 'ui-streaming' }, { kind: 'audit' }],
});
// An email session opens with a formatter destination:
// destinations: [
// { kind: 'formatter:email-html', formatterChannel: 'email-html' },
// { kind: 'audit' },
// ]
//
// A multi-channel session populates both — the router delivers the reply to
// each without any channel-branching code.Handlers
The starter ships no reference handlers. Your orchestrator (deterministic or built with createLLMHandler) registers with router.register(name, handler) and consumes session:request.
Inbound resolution — channel:message_received → session:request — is the channel-router's job: it resolves the session from the inbound threadId, mints the correlation eventId, and emits the channel-blind session:request. Delivery is a thin route to existing components (a transport adapter; MissiveStore.write), so the starter ships no reference collectors for it either.
The canonical event tuples — including the delivery:* events you wire your own delivery handlers to — are exported as EVENTS.
Session resolution strategies
The inbound adapter populates threadId on the first-layer event before routing it; the channel-router resolves the session from it. Per-channel computation lives in the channel adapters:
| Channel adapter | Computes threadId from |
|---|---|
| @cool-ai/beach-channel-email | RFC 5322 In-Reply-To / References chain |
| @cool-ai/beach-channel-whatsapp | quotedMessageId |
| SSE / chat | session-cookie / session id from the HTTP layer |
Routing template
@cool-ai/beach-starter/templates/routing.json ships as the reference routing config. Import it with a JSON module assertion and pass it to router.loadRoutingConfig(). Replace the orchestrator placeholder with your registered handler id before loading.
Architectural discipline
This package is the concrete answer to design-principle 2.1 (the router is the boundary) and the channel-blind discipline. Consumers that bypass the canonical pipeline — calling an LLM directly from an HTTP route, for instance — lose every architectural guarantee Beach exists to provide: audit, observability, approval interception, replay, distributed tracing. A handler reads context.session (a SessionView with no destinations) and cannot name a channel; channel-aware logic belongs at the channel adaptor (@cool-ai/beach-channel-*), wired with a renderer (@cool-ai/beach-a2ui-renderer-*) and an optional composer (@cool-ai/beach-channel-composer).
The canonical pipeline is the working shape. Wire your handler into it — but do not skip it.
Related
@cool-ai/beach-core— the event router, manifest handler, session lifecycle, and the unified event router primitive.@cool-ai/beach-channel-composer— the optional small-LLM composer; renderers live in@cool-ai/beach-a2ui-renderer-*.- https://cool-ai.org/docs/design-principles — Part 1 (the architectural invariant), principle 2.1 (router is the boundary), principle 2.7 (channel-blind interior).
- https://cool-ai.org/docs/contribution-policy — what "protocol-leaking" means; why the canonical pipeline is not optional.
