The Invariant Check

A standing review criterion applied to every pull request against Beach core packages.

The check

Does this change introduce any reference to a protocol-specific concept into the event router, the manifest registry, any participant interface, or any event or result type?

If yes: the change does not pass, regardless of other merits. This is called the Invariant Check.

The packages to which this check applies are: @cool-ai/beach-core, @cool-ai/beach-session, the adapter and plugin interfaces in @cool-ai/beach-transport, and any future package that forms part of the Beach interior. It does not apply to specific adapter or plugin implementations.

How to apply it in a review

Read the diff with one question in mind: after this change, can a participant, the router, or the manifest registry distinguish where an event came from or where a result is going?

If the answer is yes — even via an optional field, a metadata attachment, an abstract flag, or a naming convention — the check fails. The test is not whether a specific protocol name appears; it is whether upstream channel identity has entered the interior in any form.

If the answer is no, the check passes and the rest of the review proceeds normally.

Citing it

When closing a PR or issue on Invariant Check grounds, use this form:

This change does not pass the Invariant Check (invariant-review-criterion.md). It would cause [specific thing — e.g. "the event router to be aware of the transport that delivered the event"], which introduces a protocol-specific concept into the interior. See the contribution policy for the reasoning and for examples of how to achieve the same goal at the edge rather than the interior.

The citation should always name the specific thing that fails, not just assert that the check fails. A reviewer who cannot name the specific violation should ask before refusing.

Worked examples

Fails: transport metadata on events

// Proposed addition to InternalEvent
interface InternalEvent {
  type: string;
  payload: unknown;
  transport?: 'mcp' | 'a2a' | 'http' | 'sse';  // FAILS
}

The router and any participant can now distinguish how the event arrived. The interior has seen a protocol concept. Correct solution: strip transport metadata in the inbound adapter before emitting the event.


Fails: protocol-conditional routing

// Proposed routing rule
router.on('user:message', (event) => {
  if (event.meta?.protocol === 'mcp') {
    return mcpHandler(event);  // FAILS
  }
  return defaultHandler(event);
});

Routing on protocol rather than event type. Correct solution: if MCP and non-MCP messages genuinely need different handling, they are different event types and should be emitted as such by their respective adapters.


Fails: protocol field on a result type

// Proposed addition to AssistantReply
interface AssistantReply {
  parts: EnvelopePart[];
  httpStatusCode?: number;  // FAILS
}

A result type now carries HTTP-specific semantics. The outbound plugin that serialises to HTTP should derive the status code from the result's content, not receive it as a field.


Fails: abstract channel metadata

// Proposed addition to InternalEvent
interface InternalEvent {
  type: string;
  payload: unknown;
  channelMeta?: {
    realTime: boolean;   // FAILS
    streaming: boolean;  // FAILS
  };
}

No protocol name appears, but this is still a leak. A participant that reads channelMeta.realTime is conditioning its behaviour on where the event came from. Once this field exists, participants will use it — and once participants use it, the interior is no longer channel-agnostic. The correct location for this information is inside the adapter that produced the event; it should shape how the adapter emits events, not travel with the event into the interior.


Passes: new event type with protocol-agnostic fields

// New event type for file uploads
interface FileUploadEvent {
  type: 'file:uploaded';
  filename: string;
  mimeType: string;
  sizeBytes: number;
  contentRef: string;  // opaque reference resolved by a plugin
}

No protocol concept in sight. Any adapter (MCP file attachment, HTTP multipart, A2A binary part) can produce this event type. Passes.


Passes: new inbound adapter

// New adapter in @cool-ai/beach-grpc
class GrpcInboundAdapter implements InboundAdapter {
  // gRPC-specific code lives entirely here
  // emits standard InternalEvent types
  // the interior never knows this exists
}

Protocol-specific code at the edge, not the interior. Passes.

Relationship to the contribution policy

The Invariant Check is the operational form of the contribution policy. The policy explains the reasoning; the check is what gets cited in a review. Both documents should be read together.