Skip to main content

Chat protocol

The wire protocol between the browser and the Worker is defined once, as zod schemas, in packages/shared/src/chat.ts and packages/shared/src/feedback.ts. Both sides import the same definitions.

ChatRequest​

The body of POST /api/chat.

const ChatRequest = z.object({
claim: Claim.optional(),
claimId: z.string().optional(),
messages: z.array(ChatMessage).min(1).max(60),
capability: z.string().optional(),
promptOverrides: z.record(z.string(), z.string()).optional(),
locale: Locale.optional(), // 'en-GB' | 'en-US'
brand: Brand.optional(), // 'claimpilot' | 'clonepilot'
screenActions: z.boolean().optional(),
}).refine((r) => Boolean(r.claim ?? r.claimId), { message: 'Provide either claim or claimId' });

const ChatMessage = z.object({
role: z.enum(['user', 'assistant']),
content: z.string().max(20_000),
});
  • claim or claimId, at least one. claim is how the demo and every host work: the page sends the document it is showing. claimId loads from the store; the Prompt Lab uses it.
  • messages is the visible conversation, text only. Tool round-trips never appear here; they live inside one request.
  • capability makes the last user message a quick action: the system prompt carries that capability's playbook.
  • promptOverrides are the browser's active Prompt Lab versions, plus any per-run edits. The server drops unknown ids and over-long values.
  • screenActions says the caller can carry out client_action events. Without it, the model is never offered highlight_panel.

Events​

ChatEvent is a discriminated union on type. The Worker writes one per SSE data: line, in the order they happen.

typeFieldsMeaning
text_deltatextA chunk of the answer, to append
activitytool, labelA tool started. label is the present-tense chip: Reading the policy…
tool_resulttool, summaryA tool finished. summary is its result, truncated to 400 characters, for the lookups trace
client_actionactionSomething for the browser to do on the screen
doneusage?, model?, turnId?, prompts?The turn is complete
errormessageThe turn failed; no done follows

done​

{
type: 'done',
usage?: { inputTokens: number; outputTokens: number; cacheReadTokens: number }, // summed over the turn's rounds
model?: string,
turnId?: string, // identifies this answer for feedback
prompts?: { fingerprint: string; overridden: string[] }, // 'default', or a 12-hex hash of the overrides that applied
}

The fields are optional in the schema so an older Worker remains readable; the current Worker always sends all four. An answer without a turnId cannot be rated.

Ordering​

A turn with one tool round looks like this:

activity → tool_result → (client_action…) → [next round] → text_delta… → done

Activity for every call in a round comes before the next round's text. The browser inserts each activity chip before the streaming answer, so the answer reads as the conclusion.

ClientAction​

const ClientAction = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('highlight_panel'), panel: z.string() }),
z.object({ kind: z.literal('navigate'), path: z.string() }),
]);
  • highlight_panel scrolls to, pulses and outlines [data-panel="<panel>"] in the page. panel is one of MATTER_PANELS: home, schedule, documents, finance, involvements, proceedings, strategy, finance_resolution, mi, system_checks, key_facts, schedule_table, proceedings_table, notes.
  • navigate is defined for later use. The demo passes its router's navigate; an embedded host passes nothing, so it is ignored there. No tool emits it today.

FeedbackRequest​

The body of POST /api/feedback.

FieldTypeNotes
turnIdstring, 8 to 64From the done event; minted by the browser for voice answers
ratingup | down | nullnull withdraws
reasonsFeedbackReason[]Default []. Kept only for down
notestring ≤ 2,000Kept only for down
claimIdstring ≤ 100
capabilitystring ≤ 64
channeltext | voicerequired
surfaceapp | embedrequired
locale, brandas in ChatRequest
modelstring ≤ 100
promptFingerprintstring ≤ 32
promptOverriddenstring[] ≤ 50The prompt ids behind the fingerprint
questionstring ≤ 20,000Omitted for embed votes without feedback-content
answerstring ≤ 40,000Likewise
tools{ tool, summary }[] ≤ 40Likewise

FeedbackReason is one of incorrect, missed, off_target, unclear, unsafe, other. See Answer feedback.

The embed's DOM event​

<claimpilot-assistant> dispatches claimpilot-feedback (bubbling, composed) with the full FeedbackRequest as event.detail, including the question and answer whatever is stored.