207 lines
8.8 KiB
TypeScript
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');
|
|
}
|