@cool-ai/beach-format
v1.1.0
Published
Channel Formatter and Content Renderer primitives for batched-edge delivery in Beach applications.
Readme
@cool-ai/beach-format
Composer / Response Formatter primitive for Beach. Renders authoritative-data + narrative into a channel-shaped artifact for batched channels (email, WhatsApp, SMS, voice, push, PDF).
Home: cool-ai.org · Documentation: cool-ai.org/docs
Install
npm install @cool-ai/beach-formatConcern
Channels without a UI need to compose a standalone message from two inputs:
- Authoritative data — tool results that flowed through filter-and-distribute, annotated with composition tiers per field (
anchored | validated | narrative). - Narrative — the orchestrating LLM's
respond()output, providing framing, tone, and context.
The composition rule is absolute: facts come from data; framing comes from narrative. Dates, statuses, amounts, names, email subjects, flight numbers — all from authoritative data. Connective prose comes from the narrative.
The Composer is where this rule lands at the rendering boundary. Anchored fields are token-substituted (LLM cannot edit them). Validated fields are post-checked against the data; failures fall back to anchored rendering. Narrative fields are LLM-authored end-to-end. The trust mechanism is structural, not prompted.
The parametric Composer<E, A>
import {
EmailHtmlComposer,
type EmailEnvelope,
} from '@cool-ai/beach-format';
import { annotateRecord } from '@cool-ai/beach-core';
const composer = new EmailHtmlComposer({
llmRender: async ({ section, narrative, envelope }) => {
// your AI SDK call — returns the prose-rendered HTML for the section
},
});
const flight = annotateRecord(
{ flightNumber: 'BA572', departTime: '11:15', advisoryNote: 'Check in 60 min before' },
{
flightNumber: { composition: 'anchored' },
departTime: { composition: 'anchored' },
advisoryNote: { composition: 'narrative' },
},
);
const result = await composer.compose({
sections: [{ sectionId: 'flight', sectionTitle: 'Flight', data: [flight] }],
narrative: 'Two direct flights to Rome.',
envelope: {
channelClass: 'email-html',
from: '[email protected]',
to: ['[email protected]'],
inboundSubject: 'Trip to Rome',
},
});
if (result.status === 'rendered') {
await emailChannel.send(result.artifact);
}Composer<E extends ChannelEnvelope, A> is parametric on the channel envelope type. An EmailHtmlComposer extends Composer<EmailEnvelope, EmailHtmlArtifact> cannot read WhatsAppEnvelope fields — the type system rejects it at compile time. Cross-channel-read prohibition is enforced at the type boundary, not in prose.
Seven canonical channel formats
| Subclass | channelClass | Envelope | Artifact |
|---|---|---|---|
| EmailHtmlComposer | email-html | EmailEnvelope | { from, to, cc?, bcc?, subject, inReplyTo?, references?, html, plainText } |
| EmailPlainComposer | email-plain | EmailEnvelope | { from, to, cc?, bcc?, subject, inReplyTo?, references?, body } |
| WhatsAppComposer | whatsapp | WhatsAppEnvelope | { from, to, body, quotedMessageId?, interactiveOptions? } |
| SmsComposer | sms | SmsEnvelope | { from, to, body } |
| PushMobileComposer | push-mobile | PushMobileEnvelope | { to, title, body, deepLink?, category?, interruptionLevel?, badgeCount?, sound?, data? } |
| VoiceComposer | voice | VoiceEnvelope | { plainText, ssml? } |
| PdfPrintComposer | pdf-print | PdfPrintEnvelope | { html } |
Bespoke channels register via class MyComposer extends Composer<MyEnvelope, MyArtifact>. The seven canonical formats are capped — see architectural-constraints.md.
Three-tier composition annotation
| Tier | Behaviour |
|---|---|
| anchored | Channel-aware structural rendering, never LLM-generated. Primitives substitute character-for-character; structured objects render via the channel's structural rules (or per-data-part-type registrations). |
| validated | LLM-rendered, then post-checked against the data. Validation failure → fall back to anchored rendering for that field; the rest of the message renders normally. |
| narrative | LLM-rendered freely; no validation. |
Anchored fields are the trust mechanism — the LLM has no path to alter, paraphrase, round, or convert them. Anchored rendering is always structural.
Five canonical matrix rules
canonicalRules<E>() returns: pure narrative, empty everything, count-zero only, complex data no narrative, multi-lingual mismatch. Consumer-registered rules are evaluated first (in registration order); canonical defaults are evaluated last as fallbacks; the first non-null decision wins.
const composer = new EmailHtmlComposer({
matrixRules: [
(sections) => sections.length > 5 ? { mode: 'template' } : null,
],
});Per-data-part-type rendering and validation overrides
Anchored object rendering and validator string-containment checks both support per-(partType, channelClass) overrides:
import { InMemoryAnchoredRendererRegistry, InMemoryValidatorOverrideRegistry } from '@cool-ai/beach-format';
const renderers = new InMemoryAnchoredRendererRegistry();
renderers.register('flight-leg', 'voice', (value) => {
const v = value as { flightNumber: string; departTime: string };
return `${v.flightNumber}, departing at ${v.departTime}`;
});
const validators = new InMemoryValidatorOverrideRegistry();
validators.register('place-name', { stringContainment: 'case-insensitive' });
const composer = new VoiceComposer({
anchoredRenderers: renderers,
validatorOverrides: validators,
});Suppression + budget
Empty messages are suppressed by default — { status: 'suppressed', reason: 'no content' } is returned when both sections and narrative are empty. Heartbeat channels (where the user wants confirmation that the system ran successfully) override suppressEmptyMessage: false.
characterBudget triggers the deterministic shrink procedure: within-budget → narrative-truncated → template-fallback → over-budget. Anchored fields are preserved character-for-character; only narrative portions can be cut.
Architectural constraints
packages/format/src/composer/architectural-constraints.md records the invariants this primitive enforces — channel-format awareness scope, the canonical-format cap, the matrix-rule cap, tier-immutability, render-order guarantees, and the type-level cross-channel-read prohibition.
Related
beach-coreREADME § Filter-and-distribute — CR-156's per-destination dispatch primitive; the Composer's input is filter-and-distribute's output.beach-format-emailREADME — re-exportsEmailHtmlComposerandEmailPlainComposerfor the email-specific import path.- Migration: CR-157 — full migration walkthrough from the removed
EmailFormatterandComposerActorto the parametric Composer.
