Example: Design Review Projection
This guide shows how to build a Design Review mode on top of the shared agent-session runtime. It uses domain markers for deterministic profile/manifest resolution and keeps chat/SSE behavior identical to generic sessions.
Goal
Create a new product mode where each design-review row maps to one backing interactive session, while reusing the same runtime endpoints for send-message and streaming.
Step 1: Create Domain Model
Your domain service owns product metadata and projection state.
1export type DesignReviewStatus = 'active' | 'completed' | 'archived';
2
3export interface DesignReviewRow {
4 id: string;
5 orgId: string;
6 projectId: string;
7 title: string;
8 summary: string | null;
9 tags: string[];
10 status: DesignReviewStatus;
11 agentSessionId: string | null;
12 createdBy: string;
13 createdAt: string;
14 updatedAt: string;
15}Enforce this invariant in your data model: one active design-review row maps to one active backing session.
Step 2: Create Domain Row and Backing Session
Use the runtime create endpoint with sessionDomainKey and sessionDomainRef.
1import { randomUUID } from 'node:crypto';
2
3const DESIGN_REVIEW_DOMAIN_KEY = 'design-review';
4
5export async function createDesignReview(input: {
6 orgId: string;
7 projectId: string;
8 actorId: string;
9 title: string;
10 userAccessToken: string;
11}): Promise<{ reviewId: string; sessionId: string }> {
12 const reviewId = randomUUID();
13
14 await db.designReview.create({
15 data: {
16 id: reviewId,
17 orgId: input.orgId,
18 projectId: input.projectId,
19 title: input.title,
20 summary: null,
21 tags: [],
22 status: 'active',
23 agentSessionId: null,
24 createdBy: input.actorId,
25 },
26 });
27
28 const res = await fetch(`${process.env.AGENT_SESSION_API_URL}/sessions`, {
29 method: 'POST',
30 headers: {
31 Authorization: `Bearer ${input.userAccessToken}`,
32 'Content-Type': 'application/json',
33 'X-Org-Id': input.orgId,
34 'X-Project-Id': input.projectId,
35 },
36 body: JSON.stringify({
37 title: `Design Review: ${input.title}`,
38 sessionDomainKey: DESIGN_REVIEW_DOMAIN_KEY,
39 sessionDomainRef: reviewId,
40 }),
41 });
42
43 if (!res.ok) {
44 throw new Error(`Failed to create backing session (${res.status})`);
45 }
46
47 const envelope = await res.json();
48 const sessionId = envelope.data.id as string;
49
50 await db.designReview.update({
51 where: { id: reviewId },
52 data: { agentSessionId: sessionId },
53 });
54
55 return { reviewId, sessionId };
56}Important behavior:
- You can omit
agentRefwhensessionDomainKeyresolves a registered domain Agent. - Runtime resolves profile deterministically from the domain marker.
- Invalid mapping fails fast.
- The actor is derived from the bearer token on
POST /sessions.
Step 3: Resolve Session ID in Your UI/API
Provide a deterministic resolver from design-review id to session id.
1export async function getDesignReviewSessionId(params: {
2 orgId: string;
3 projectId: string;
4 reviewId: string;
5}): Promise<string> {
6 const row = await db.designReview.findUnique({
7 where: { id: params.reviewId },
8 select: { orgId: true, projectId: true, agentSessionId: true, status: true },
9 });
10
11 if (!row) throw new Error('Design review not found');
12 if (row.orgId !== params.orgId || row.projectId !== params.projectId) {
13 throw new Error('Cross-tenant access denied');
14 }
15 if (row.status !== 'active') throw new Error('Design review is not active');
16 if (!row.agentSessionId) throw new Error('Backing session is missing');
17
18 return row.agentSessionId;
19}Step 4: Send Message via Shared Runtime Endpoint
1curl -X POST "https://agent-session-api.xema.dev/sessions/${SESSION_ID}/messages" \
2 -H "Authorization: Bearer $TOKEN" \
3 -H "X-Org-Id: $ORG_ID" \
4 -H "X-Project-Id: $PROJECT_ID" \
5 -H "Idempotency-Key: $(uuidgen)" \
6 -H "Content-Type: application/json" \
7 -H "Accept: text/event-stream" \
8 -d '{"content":"Review this architecture decision record and suggest risks."}'No custom design-review message endpoint is required.
Step 5: Consume Shared Session Stream Contract
Use the same stream contract your generic session console already handles.
- stream event envelope remains consistent
- ordering and replay behavior remains consistent
- domain UI remains additive (status badges, curation panels, handoff controls)
Step 6: Add Projection-Specific Behavior Only
Keep domain logic focused on projection concerns:
- summary and tags curation
- design-review-specific handoff into a pipeline
- domain events derived from runtime source-of-truth events
Do not re-implement runtime provisioning loops, turn locking, or SSE transport.
Validation Checklist
- Concurrent sends do not bypass runtime turn lock.
- Session create fails fast on invalid domain/profile mapping.
- Restart/archive updates maintain one active mapping invariant.
- Design-review mode uses exact same runtime message and stream behavior as generic sessions.
Previous: ← Examples overview