Files
AI-Rendezvous/packages/core/src/markdown.ts
T

207 lines
8.8 KiB
TypeScript

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 <your invite token>\` or \`?token=<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');
}