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.

ts
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.

ts
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 agentRef when sessionDomainKey resolves 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.

ts
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

bash
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