Domain Projections and Custom Session Modes
Interactive sessions now run on a single shared runtime, and product/domain features are built as domain projections on top of that runtime. This keeps chat transport, streaming, and lifecycle behavior consistent while letting you ship new session experiences quickly.
What Changed
The platform now treats session behavior in two layers:
- Runtime layer (
agent-session-api) owns message turns, streaming, worker/session lifecycle, and state transitions. - Domain layer (for example brainstorming or a custom product service) owns domain metadata, curation, handoff, and domain events.
Domain services should no longer implement their own chat/SSE transport or provisioning reconciliation loop.
Deterministic Domain Marker Resolution
When a session is created with a domain marker:
sessionDomainKeyidentifies the projection kind (closed domain marker)sessionDomainRefidentifies the domain row
The runtime resolves the effective published Agent deterministically from sessionDomainKey.
Rules:
- If
sessionDomainKeyis present andagentRefis omitted, the runtime resolves the Agent from the domain marker. - If an explicit
agentRefconflicts with the domain's registered Agent, creation fails fast. - If neither
agentRefnor a resolvable domain marker is present, creation fails fast.
This removes service-level hardcoding and keeps profile selection biome-driven.
Build a New Mode (Non-Brainstorming Example)
This example creates a Design Review domain projection that reuses the same interactive session runtime.
For a complete implementation walkthrough, see Example: Design Review Projection.
1. Define the domain row in your service
1// design-review.service.ts
2type DesignReviewRow = {
3 id: string;
4 orgId: string;
5 projectId: string;
6 title: string;
7 status: 'active' | 'completed' | 'archived';
8 agentSessionId: string | null;
9};2. Create the domain row, then provision runtime session via domain marker
1// design-review.service.ts
2import { randomUUID } from 'node:crypto';
3
4const DESIGN_REVIEW_DOMAIN_KEY = 'design-review';
5
6async function createDesignReview(params: {
7 orgId: string;
8 projectId: string;
9 title: string;
10 userAccessToken: string;
11}): Promise<{ reviewId: string; agentSessionId: string }> {
12 const reviewId = randomUUID();
13
14 // 1) Create your domain row first (transaction omitted for brevity)
15 await db.designReview.create({
16 data: {
17 id: reviewId,
18 orgId: params.orgId,
19 projectId: params.projectId,
20 title: params.title,
21 status: 'active',
22 agentSessionId: null,
23 },
24 });
25
26 // 2) Ask agent-session runtime to create the backing session
27 // No hardcoded Agent ref is needed when the domain mapping owns it.
28 const sessionRes = await fetch(`${process.env.AGENT_SESSION_API_URL}/sessions`, {
29 method: 'POST',
30 headers: {
31 Authorization: `Bearer ${params.userAccessToken}`,
32 'Content-Type': 'application/json',
33 'X-Org-Id': params.orgId,
34 'X-Project-Id': params.projectId,
35 },
36 body: JSON.stringify({
37 title: `Design Review: ${params.title}`,
38 sessionDomainKey: DESIGN_REVIEW_DOMAIN_KEY,
39 sessionDomainRef: reviewId,
40 }),
41 });
42
43 if (!sessionRes.ok) {
44 throw new Error(`session create failed (${sessionRes.status})`);
45 }
46
47 const sessionEnvelope = await sessionRes.json();
48 const agentSessionId = sessionEnvelope.data.id as string;
49
50 await db.designReview.update({
51 where: { id: reviewId },
52 data: { agentSessionId },
53 });
54
55 return { reviewId, agentSessionId };
56}3. Route UI chat to the same runtime endpoints
Use the same runtime endpoints used by generic sessions:
POST /sessions/{id}/threads/{threadId}/messagesfor turnsGET /sessions/{id}/events/streamfor persistent realtime updates
Keep design-review UI additive (extra panels, badges, metadata), while transcript and stream components remain shared.
The runtime derives createdBy from the bearer token on POST /sessions.
4. Enforce one active mapping invariant
For each active design-review row, enforce exactly one active backing session.
Suggested invariant checks:
- domain row cannot enter
activewithoutagentSessionId - no second active session can be bound to the same domain row
- archive/restart flows must update both runtime and projection state deterministically
Biome-Driven Agent and Workspace Selection
For new domain modes:
- register the domain-to-Agent configuration in biome content
- resolve the published Agent and workspace from the domain marker
- do not hardcode retired session-profile constants in domain services
This keeps one runtime and one manifest model while allowing many projection modes.
Verification Checklist for New Modes
- Chat traffic goes through agent-session runtime endpoints only
- Realtime stream contract matches generic interactive sessions
- Domain service does not own runtime reconciliation loops
- Concurrent sends/restarts/archives do not drift lock/state
- Invalid or missing domain/profile mapping fails fast
Previous: ← API Reference