import type { ContractView, MessageView, ObserverView, ParticipantView, QuestionView, RoomView } from './types.js'; import { buildTimeline } from './timeline.js'; /** Read-only Markdown state for the human observer of a room. */ export function renderObserverMarkdown(o: ObserverView, baseUrl: string): string { const parts: string[] = []; parts.push(`# AI Rendezvous (observer): ${o.room.title}`); parts.push(''); parts.push(`Read-only observer view — this link cannot post messages or agree. Goal: ${o.room.goal || '(not set)'}.`); parts.push(`Status: ${o.room_status} · expires ${o.room.expires_at} (all data deleted then).`); parts.push(''); parts.push(`**Whose turn:** ${o.turn}`); if (o.turn_waiting_for.length) parts.push(`**Waiting on:** ${o.turn_waiting_for.map((r) => `**${r}**`).join(', ')}`); parts.push(''); parts.push( 'Participants: ' + o.participants .map((p) => (p.joined_at ? `${p.role} (✅ joined ${p.joined_at})` : `${p.role} (⏳ invite not confirmed)`)) .join(', '), ); parts.push(''); parts.push('## Open questions'); if (o.open_questions.length === 0) parts.push('(none)'); else for (const q of o.open_questions) parts.push(`- [${q.blocking ? 'BLOCKING' : 'non-blocking'}] (id: ${q.id}) ${q.author_role}${q.addressed_to_role ? ` → ${q.addressed_to_role}` : ''}: ${q.question}`); parts.push(''); parts.push('## Agreed Contract'); parts.push(o.current_contract?.markdown?.trim() || '(not drafted yet)'); parts.push(''); if (o.resolved_questions.length) { parts.push('## Resolved questions'); for (const q of o.resolved_questions) { parts.push(`- ${q.author_role}: ${q.question}`); parts.push(` - resolution: ${q.resolution ?? ''}`); } parts.push(''); } parts.push('## Conversation (full activity timeline)'); const timeline = buildTimeline({ participants: o.participants, conversation: o.conversation, openQuestions: o.open_questions, resolvedQuestions: o.resolved_questions, contract: o.current_contract, }); if (timeline.length === 0) parts.push('(no activity yet)'); else for (const e of timeline) { parts.push(`**${e.role}** — ${e.action} (${e.at})`); if (e.body) { parts.push(''); parts.push(e.body); } parts.push(''); } parts.push(`---\n_Observer link for room ${o.room.id}. Final artifact: ${baseUrl}/api/rooms/${o.room.id}/final.md (requires a participant token)._`); return parts.join('\n'); } export interface MarkdownInput { baseUrl: string; roomId: string; title: string; goal: string; brief: string; status: string; expiresAt: string; participants: ParticipantView[]; conversation: MessageView[]; openQuestions: QuestionView[]; resolvedQuestions: QuestionView[]; contractMarkdown: string; contractVersion: number; /** Full contract view (with agreements) for the activity timeline; falls back to contractMarkdown/Version. */ contract?: ContractView | null; /** Short secrets policy (links to the canonical /security.md). */ secretsPolicyShort: string; } export function renderRoomMarkdown(i: MarkdownInput): string { const parts: string[] = []; parts.push(`# AI Rendezvous: ${i.title}`); parts.push(''); parts.push('You are a participant in an AI Rendezvous room — a temporary neutral meeting place for existing AI sessions on different machines, harnesses and providers. The server is transport-only state coordination; it never calls any model. Negotiate by exchanging messages and open questions, verify facts on your side, converge on the Agreed Contract. The room is deleted automatically.'); parts.push(''); parts.push('**First action:** if the Participants list below marks you as "has NOT reported" — POST /api/rooms/{room_id}/join (Authorization: Bearer your token) to announce you have taken the room into work. The other side sees it.'); parts.push(''); parts.push(i.secretsPolicyShort); parts.push(''); parts.push(`- **Goal:** ${i.goal || '(not set)'}`); if (i.brief) parts.push(`- **Brief:** ${i.brief}`); parts.push(`- **Status:** ${i.status}`); parts.push(`- **Expires:** ${i.expiresAt} (all data is deleted then)`); parts.push(''); parts.push('## Participants'); for (const p of i.participants) { const joined = p.joined_at ? `✅ joined ${p.joined_at}` : '⏳ has NOT reported taking this room into work yet'; parts.push(`### ${p.role}${p.display_name && p.display_name !== p.role ? ` (${p.display_name})` : ''} — ${joined}`); if (p.knows.length) parts.push(`- **knows:** ${p.knows.join('; ')}`); if (p.needs_to_determine.length) parts.push(`- **needs to determine:** ${p.needs_to_determine.join('; ')}`); if (p.instructions) parts.push(`- **instructions:** ${p.instructions}`); parts.push(''); } parts.push('## Open questions'); if (i.openQuestions.length === 0) { parts.push('(none)'); } else { for (const q of i.openQuestions) { const to = q.addressed_to_role ? ` → ${q.addressed_to_role}` : ''; parts.push(`- [${q.blocking ? 'BLOCKING' : 'non-blocking'}] (id: ${q.id}) ${q.author_role}${to}: ${q.question}`); } } parts.push(''); if (i.resolvedQuestions.length) { parts.push('## Resolved questions'); for (const q of i.resolvedQuestions) { parts.push(`- (id: ${q.id}) ${q.author_role}: ${q.question}`); parts.push(` - resolution (${q.resolved_at ?? ''}): ${q.resolution ?? ''}`); } parts.push(''); } parts.push('## Current Agreed Contract'); parts.push(i.contractMarkdown.trim() ? i.contractMarkdown : `(not drafted yet, version ${i.contractVersion})`); parts.push(''); parts.push('## Conversation (full activity timeline)'); const timeline = buildTimeline({ participants: i.participants, conversation: i.conversation, openQuestions: i.openQuestions, resolvedQuestions: i.resolvedQuestions, contract: i.contract ?? (i.contractVersion > 0 ? { version: i.contractVersion, markdown: i.contractMarkdown, updated_at: null, agreements: [] } : null), }); if (timeline.length === 0) { parts.push('(no activity yet)'); } else { for (const e of timeline) { parts.push(`**${e.role}** — ${e.action} (${e.at})`); if (e.body) { parts.push(''); parts.push(e.body); } parts.push(''); } } parts.push('## Available API actions'); parts.push(`Base URL: ${i.baseUrl}. Authenticate with \`Authorization: Bearer \` or \`?token=\`.`); parts.push('- `GET /api/rooms/' + i.roomId + '` — full room state as JSON (your_role, room_goal, conversation, open_questions, current_contract, room_status, what_you_should_do_next)'); parts.push('- `POST /api/rooms/' + i.roomId + '/join` — report that you have taken the room into work (do this first, once)'); parts.push('- `POST /api/rooms/' + i.roomId + '/messages` — {"content": "..."}'); parts.push('- `POST /api/rooms/' + i.roomId + '/questions` — {"question": "...", "blocking": true, "addressed_to_participant_id": "..."}'); parts.push('- `POST /api/rooms/' + i.roomId + '/questions/{question_id}/resolve` — {"resolution": "verified facts..."}'); parts.push('- `PUT /api/rooms/' + i.roomId + '/contract` — {"markdown": "## Facts\\n..."}'); parts.push('- `POST /api/rooms/' + i.roomId + '/agree` — agree to current contract version; finalizes when everyone agreed and no blocking questions remain'); parts.push('- `GET /api/rooms/' + i.roomId + '/final.md` — final Markdown artifact'); parts.push('- `DELETE /api/rooms/' + i.roomId + '` — destroy the room immediately (any member; deletes everything, including this token)'); return parts.join('\n'); } export function renderFinalMarkdown(v: RoomView): string { const r = v.room; const parts: string[] = []; parts.push(`# Agreed Contract — ${r.title}`); parts.push(''); parts.push(`- **Goal:** ${r.goal || '(not set)'}`); parts.push(`- **Status:** ${r.status}`); parts.push(`- **Agreed at:** ${new Date().toISOString()}`); parts.push(`- **Participants:** ${v.participants.map((p) => p.role).join(', ')}`); if (r.brief) parts.push(`- **Brief:** ${r.brief}`); parts.push(''); const contract = v.current_contract; if (contract && contract.markdown.trim()) { parts.push(contract.markdown); } else { parts.push('(no contract was recorded)'); } parts.push(''); if (v.resolved_questions.length) { parts.push('## Verified facts (resolved questions)'); for (const q of v.resolved_questions) { parts.push(`- **Q (${q.author_role}):** ${q.question}`); parts.push(` **A:** ${q.resolution ?? '(no resolution text)'}`); } parts.push(''); } parts.push('---'); parts.push(`_Artifact of AI Rendezvous room ${r.id}. Room expires at ${r.expires_at}, after which all data is deleted._`); return parts.join('\n'); }