import { estimateTokens } from "../context/envelope.js"; import type { JsonSchema } from "pi-agents/src/model/json-schema.js"; export const BOARD_TYPES = ["hypothesis", "evidence", "challenge", "confirmation", "request", "decision", "blocker"] as const; /** * The round members. Three distinct lenses, not three seats — a fourth would need a fourth lens, * which is why the count is a constant here rather than a config knob. Extra perspectives arrive as * named specialists, requested from the board when a member hits something it cannot resolve. */ export const WAR_ROOM_MEMBERS = ["lead", "flow", "invariants"] as const; export type BoardType = (typeof BOARD_TYPES)[number]; export type SignalType = "evidence" | "challenge" | "request" | "blocker"; export type BoardPolicy = "balanced" | "quality"; type BoardPayload = { readonly author: string; readonly claim: string; readonly target?: string; readonly targetCursor?: number; readonly evidence?: readonly string[]; readonly verified?: boolean; }; export type BoardEvent = { [Type in BoardType]: BoardPayload & { readonly type: Type } }[BoardType]; export type BoardRecord = BoardEvent & { readonly cursor: number; readonly round: number; readonly estimatedTokens: number }; export interface BoardPage { readonly events: readonly BoardRecord[]; readonly cursor: number } export interface RoundMetrics { openHypotheses: number; confirmedHypotheses: number; rejectedHypotheses: number; conflicts: number; uniqueEvidence: number; } export interface RoundResult extends RoundMetrics { readonly round: number; readonly openHypothesesReduction: number; readonly uncertaintyReduction: number; readonly communicationTokens: number; readonly collapse: boolean; readonly status: "continue" | "converged" | "collapse"; } export type CoordinationAction = | { readonly type: "steer"; readonly target: string; readonly cursor: number; readonly message: string } | { readonly type: "specialist-request"; readonly requester: string; readonly gap: string; readonly events: readonly BoardRecord[] }; export interface WarRoomSignal { readonly credential: string; readonly signalId: string; readonly type: SignalType; readonly target?: string; readonly targetCursor?: number; readonly claim: string; readonly evidence?: readonly string[]; } export interface WarRoomWorkerBinding { readonly signalPath: string; readonly credential: string; } const EVENT_FIELDS = new Set(["type", "author", "claim", "target", "targetCursor", "evidence", "verified"]); const METRIC_FIELDS = ["openHypotheses", "confirmedHypotheses", "rejectedHypotheses", "conflicts", "uniqueEvidence"] as const; const TOKEN_CAP: Record = { balanced: 6000, quality: 10_000 }; const MEMBER_BOARD_TYPES = BOARD_TYPES.filter((type) => type !== "decision"); function assertEvent(event: BoardEvent): number { if (!event || typeof event !== "object") throw new Error("War Room event must be an object"); if (!BOARD_TYPES.includes(event.type)) throw new Error("Unknown War Room event type"); if (Object.keys(event).some((field) => !EVENT_FIELDS.has(field))) throw new Error("Unknown War Room event field"); if (typeof event.author !== "string" || !event.author.trim()) throw new Error("War Room event author is required"); if (typeof event.claim !== "string" || !event.claim.trim()) throw new Error("War Room event claim is required"); if (event.target !== undefined && (typeof event.target !== "string" || !event.target.trim())) throw new Error("War Room event target must be a non-empty member"); if (event.targetCursor !== undefined && (!Number.isSafeInteger(event.targetCursor) || event.targetCursor < 1)) throw new Error("War Room targetCursor must be a positive integer"); if (event.evidence !== undefined && (!Array.isArray(event.evidence) || event.evidence.some((item) => typeof item !== "string" || !item.trim()))) throw new Error("War Room event evidence must contain non-empty strings"); if (event.verified !== undefined && typeof event.verified !== "boolean") throw new Error("War Room event verified must be boolean"); const chars = JSON.stringify(event).length; if (chars > 1200) throw new Error("War Room event exceeds 1200 characters"); return estimateTokens(event); } export function boardBatchSchema(author: string): JsonSchema { const types = author === "controller" ? BOARD_TYPES : MEMBER_BOARD_TYPES; return { type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, properties: { type: { enum: [...types] }, author: { const: author }, claim: { type: "string", minLength: 1, maxLength: 1000 }, target: { type: "string", minLength: 1, maxLength: 256 }, targetCursor: { type: "integer", minimum: 1 }, evidence: { type: "array", maxItems: 8, items: { type: "string", minLength: 1, maxLength: 256 } }, verified: { type: "boolean" }, }, required: ["type", "author", "claim"], }, }; } export function parseBoardBatch(value: unknown, author: string): BoardEvent[] { const parsed = typeof value === "string" ? JSON.parse(value.trim().replace(/^```(?:json)?\s*|\s*```$/g, "")) as unknown : value; if (!Array.isArray(parsed) || parsed.length !== 1) throw new Error("War Room result must contain exactly one event"); return parsed.map((entry) => { if (!entry || typeof entry !== "object" || Array.isArray(entry) || (entry as { author?: unknown }).author !== author) throw new Error("War Room event author does not match its member"); const event = entry as BoardEvent; assertEvent(event); if (author !== "controller" && event.type === "decision") throw new Error("Only the controller may emit a War Room decision"); return structuredClone(event); }); } function assertMetrics(metrics: RoundMetrics): void { if (!metrics || typeof metrics !== "object") throw new Error("War Room round metrics are required"); if (Object.keys(metrics).length !== METRIC_FIELDS.length || Object.keys(metrics).some((field) => !METRIC_FIELDS.includes(field as typeof METRIC_FIELDS[number]))) throw new Error("War Room round metrics are malformed"); for (const name of METRIC_FIELDS) { const value = metrics[name]; if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`War Room ${name} must be a non-negative number`); } } function requestKey(event: BoardEvent): string { return event.claim.trim().replaceAll(/\s+/g, " ").toLocaleLowerCase(); } export class WarRoom { private readonly board: BoardRecord[] = []; private readonly messages = new Map(); private readonly subscriptions = new Map; cursor: number }>(); private readonly requests = new Set(); private readonly rounds: RoundResult[] = []; private readonly tokenCap: number; private usedTokens = 0; private closed = false; constructor(private readonly maxRounds = 2, private readonly maxMessagesPerMember = 4, policy: BoardPolicy = "balanced") { if (!Number.isInteger(maxRounds) || maxRounds < 1 || maxRounds > 3) throw new Error("War Room round limit must be between 1 and 3"); if (!Number.isInteger(maxMessagesPerMember) || maxMessagesPerMember < 1 || maxMessagesPerMember > 4) throw new Error("War Room member message limit must be between 1 and 4"); if (policy !== "balanced" && policy !== "quality") throw new Error("Unknown War Room board policy"); this.tokenCap = TOKEN_CAP[policy]; } get events(): readonly BoardRecord[] { return this.board.slice(); } private assertReference(event: BoardEvent): void { if (["challenge", "confirmation", "decision"].includes(event.type) && event.targetCursor === undefined) throw new Error(`War Room ${event.type} must refer to targetCursor`); if (event.targetCursor === undefined) return; const target = this.board[event.targetCursor - 1]; if (!target || target.cursor !== event.targetCursor || target.type !== "hypothesis") throw new Error("War Room targetCursor must refer to an existing hypothesis"); if (event.type === "confirmation" && target.author === event.author) throw new Error("War Room agents cannot confirm their own hypothesis"); if (event.type === "confirmation" && !(event.evidence?.length ?? 0)) throw new Error("War Room confirmation requires at least one evidence item"); if (event.type === "decision" && (event.author !== "controller" || event.verified !== true)) throw new Error("Only the verified controller may emit a War Room decision"); } private append(event: BoardEvent): BoardRecord { const estimatedTokens = assertEvent(event); if ((this.messages.get(event.author) ?? 0) >= this.maxMessagesPerMember) throw new Error("War Room member message limit reached"); if (this.usedTokens + estimatedTokens > this.tokenCap) throw new Error("War Room board token limit reached"); const record = Object.freeze({ ...event, evidence: event.evidence ? Object.freeze([...event.evidence]) : undefined, cursor: this.board.length + 1, round: this.rounds.length + 1, estimatedTokens }) as BoardRecord; this.board.push(record); this.messages.set(event.author, (this.messages.get(event.author) ?? 0) + 1); this.usedTokens += estimatedTokens; return record; } post(event: BoardEvent): BoardRecord { if (this.closed) throw new Error("War Room is closed"); this.assertReference(event); return this.append(event); } accept(event: BoardEvent): CoordinationAction[] { if (this.closed && !(event.type === "decision" && event.author === "controller" && event.verified === true)) throw new Error("War Room is closed"); this.assertReference(event); const record = this.append(event); const actions: CoordinationAction[] = []; if (event.target && event.target !== event.author) { actions.push({ type: "steer", target: event.target, cursor: record.cursor, message: `War Room cursor ${record.cursor} from ${event.author}: ${event.claim}${event.evidence?.length ? ` Evidence: ${event.evidence.join("; ")}` : ""}. Cite cursor ${record.cursor} in your result.` }); } if (event.type === "request" || event.type === "blocker") { const key = requestKey(event); if (!this.requests.has(key)) { this.requests.add(key); actions.push({ type: "specialist-request", requester: event.author, gap: event.claim, events: this.board.slice(-8) }); } } return actions; } subscribe(subscriber: string, types: readonly BoardType[], cursor = 0): void { if (typeof subscriber !== "string" || !subscriber.trim()) throw new Error("War Room subscriber is required"); if (!Array.isArray(types) || !types.length || types.some((type) => !BOARD_TYPES.includes(type))) throw new Error("War Room subscription requires valid event types"); if (!Number.isInteger(cursor) || cursor < 0 || cursor > this.board.length) throw new Error("War Room subscription cursor is invalid"); const previous = this.subscriptions.get(subscriber); if (previous && cursor < previous.cursor) throw new Error("War Room subscription cursor cannot move backwards"); this.subscriptions.set(subscriber, { types: new Set(types), cursor }); } poll(subscriber: string): BoardPage { const subscription = this.subscriptions.get(subscriber); if (!subscription) throw new Error("War Room subscriber is not registered"); const cursor = this.board.length; const events = this.board.filter((event) => event.cursor > subscription.cursor && subscription.types.has(event.type)); subscription.cursor = cursor; return { events, cursor }; } metrics(): RoundMetrics { const hypotheses = this.board.filter((event) => event.type === "hypothesis"); const confirmed = new Set(this.board.filter((event) => event.type === "confirmation").flatMap((event) => event.targetCursor === undefined ? [] : [event.targetCursor])); const decided = new Set(this.board.filter((event) => event.type === "decision" && event.author === "controller" && event.verified === true).flatMap((event) => event.targetCursor === undefined ? [] : [event.targetCursor])); const resolved = new Set([...confirmed, ...decided]); const conflicts = new Set(this.board.filter((event) => event.type === "challenge" && event.targetCursor !== undefined && !resolved.has(event.targetCursor)).flatMap((event) => event.targetCursor === undefined ? [] : [event.targetCursor])); const evidence = new Set(this.board.flatMap((event) => [...(event.evidence ?? []), ...(event.type === "evidence" ? [event.claim] : [])])); return { openHypotheses: hypotheses.filter((event) => !resolved.has(event.cursor)).length, confirmedHypotheses: confirmed.size, rejectedHypotheses: 0, conflicts: conflicts.size, uniqueEvidence: evidence.size, }; } finish(metrics: RoundMetrics): RoundResult { if (this.closed) throw new Error("War Room is closed"); assertMetrics(metrics); const measured = this.metrics(); if (METRIC_FIELDS.some((name) => metrics[name] !== measured[name])) throw new Error("War Room round metrics do not match its cursor-linked board"); const previous = this.rounds.at(-1); const round = this.rounds.length + 1; const openHypothesesReduction = previous ? previous.openHypotheses - measured.openHypotheses : 0; const uncertaintyReduction = previous ? previous.openHypotheses + previous.conflicts - measured.openHypotheses - measured.conflicts : 0; const atLimit = round >= this.maxRounds; const collapse = atLimit && measured.openHypotheses > 0; const converged = measured.openHypotheses === 0; const status: RoundResult["status"] = collapse ? "collapse" : converged ? "converged" : "continue"; const result: RoundResult = Object.freeze({ ...measured, round, openHypothesesReduction, uncertaintyReduction, communicationTokens: this.usedTokens, collapse, status }); this.rounds.push(result); this.closed = status !== "continue"; return result; } closeRound(metrics: RoundMetrics): RoundResult { return this.finish(metrics); } shouldCollapse(previousOpen: number, currentOpen: number, round: number): boolean { return round >= this.maxRounds && currentOpen >= previousOpen; } }