import { z } from "zod"; import { canonicalJson, sha256, type JsonObject } from "../core/json.js"; import type { ThoughtEvent } from "../events/types.js"; import { conversationCompactionOutputSchema, type ConversationCompactionOutput, } from "./output-contracts.js"; export const CONVERSATION_COMPACTION_EVENT_TYPE = "stream.thought.derived.conversation.compaction"; export const CONVERSATION_COMPACTION_SCHEMA_VERSION = 1; const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/); export const conversationCompactionPlanSchema = z.object({ version: z.literal(1), targetAgentId: z.string().min(1).max(200), historyAgentIds: z.array(z.string().min(1).max(200)).min(1).max(32), activationSnapshotId: z.string().min(1), activationSnapshotSha256: sha256Schema, activationSourceSequence: z.number().int().nonnegative(), conversationSource: z.string().min(1).max(200), chatId: z.string().min(1).max(100), senderId: z.string().min(1).max(100), previousBoundaryEventId: z.string().min(1).nullable(), previousBoundarySha256: sha256Schema.nullable(), previousCoveredThroughSourceSequence: z.number().int().nonnegative(), coveredThroughTurnEventId: z.string().min(1), coveredThroughSourceSequence: z.number().int().positive(), triggerInputChars: z.number().int().min(16).max(1_000_000), retainInputChars: z.number().int().positive().max(999_999), uncompactedInputChars: z.number().int().positive().max(2_000_000), retainedInputChars: z.number().int().positive().max(1_000_000), coveredSourceEvents: z.number().int().positive().max(1_000), inputTurns: z.number().int().positive().max(1_000), inputChars: z.number().int().positive().max(1_000_000), inputTurnEventIdsSha256: sha256Schema, inputContentSha256: sha256Schema, }).strict().superRefine((value, context) => { if ((value.previousBoundaryEventId === null) !== (value.previousBoundarySha256 === null)) { context.addIssue({ code: "custom", path: ["previousBoundaryEventId"], message: "Prior boundary id and hash must appear together" }); } if (value.previousBoundaryEventId === null && value.previousCoveredThroughSourceSequence !== value.activationSourceSequence) { context.addIssue({ code: "custom", path: ["previousCoveredThroughSourceSequence"], message: "First boundary must begin at its persisted activation frontier" }); } if (value.previousBoundaryEventId !== null && value.previousCoveredThroughSourceSequence < 1) { context.addIssue({ code: "custom", path: ["previousCoveredThroughSourceSequence"], message: "Recursive boundary requires positive prior coverage" }); } if (value.coveredThroughSourceSequence <= value.previousCoveredThroughSourceSequence) { context.addIssue({ code: "custom", path: ["coveredThroughSourceSequence"], message: "Compaction coverage must advance" }); } if (value.retainInputChars >= value.triggerInputChars) { context.addIssue({ code: "custom", path: ["retainInputChars"], message: "Compaction must retain fewer input characters than its trigger" }); } if (value.uncompactedInputChars < value.triggerInputChars) { context.addIssue({ code: "custom", path: ["uncompactedInputChars"], message: "Compaction input must meet the character trigger" }); } if (value.retainedInputChars > value.retainInputChars) { context.addIssue({ code: "custom", path: ["retainedInputChars"], message: "Retained exact tail exceeds its character target" }); } }); export type ConversationCompactionPlan = z.infer; export const conversationCompactionActivationSchema = z.object({ version: z.literal(1), targetAgentId: z.string().min(1).max(200), compactorAgentId: z.string().min(1).max(200), compactorAgentVersion: z.number().int().positive(), compactorDeclarationFingerprint: sha256Schema, conversationSource: z.string().min(1).max(200), chatId: z.string().min(1).max(100), senderId: z.string().min(1).max(100), historyAgentIds: z.array(z.string().min(1).max(200)).min(1).max(32), triggerInputChars: z.number().int().min(16).max(1_000_000), retainInputChars: z.number().int().positive().max(999_999), activationSourceSequence: z.number().int().nonnegative(), activatedByEventId: z.string().min(1), activatedBySourceSequence: z.number().int().positive(), }).strict(); export type ConversationCompactionActivation = z.infer; const outputContractIdentitySchema = z.object({ id: z.string().min(1), version: z.number().int().positive(), sha256: sha256Schema, }).strict(); const modelSchema = z.object({ provider: z.string().min(1).max(200), id: z.string().min(1).max(500), revision: z.string().min(1).max(500).optional(), }).strict(); export const conversationCompactionContextSnapshotSchema = z.object({ id: z.string().min(1), storage: z.literal("jazz-document-version"), textSha256: sha256Schema, systemTextSha256: sha256Schema, messagesSha256: sha256Schema, manifestSha256: sha256Schema, }).strict(); export const conversationCompactionEventPayloadSchema = z.object({ runId: z.string().min(1), executionKey: z.string().min(1), inputEventId: z.string().min(1), inputSourceSequence: z.number().int().positive(), compactorAgentVersion: z.number().int().positive(), compactorDeclarationFingerprint: sha256Schema, promptHash: sha256Schema, contextSnapshot: conversationCompactionContextSnapshotSchema, summary: z.string().min(1).max(2_000), confidence: z.number().min(0).max(1), outputContract: outputContractIdentitySchema, structuredOutput: conversationCompactionOutputSchema, compactionPlan: conversationCompactionPlanSchema, boundarySha256: sha256Schema, model: modelSchema.optional(), }).strict().superRefine((value, context) => { if (value.summary !== value.structuredOutput.summary) { context.addIssue({ code: "custom", path: ["summary"], message: "Boundary preview must match structured output" }); } if (value.confidence !== value.structuredOutput.confidence) { context.addIssue({ code: "custom", path: ["confidence"], message: "Boundary confidence must match structured output" }); } if (value.boundarySha256 !== conversationCompactionBoundarySha256( value.compactionPlan, value.structuredOutput, )) { context.addIssue({ code: "custom", path: ["boundarySha256"], message: "Boundary content hash does not match" }); } }) as unknown as z.ZodType; export interface ResolvedConversationCompactionBoundary { event: ThoughtEvent; plan: ConversationCompactionPlan; output: ConversationCompactionOutput; boundarySha256: string; amendment?: { content: string; sha256: string; } | undefined; } export class ConversationCompactionNotNeeded extends Error { readonly code = "conversation-compaction-not-needed"; constructor(readonly evidence: JsonObject) { super("Conversation history has not reached the next compaction boundary"); this.name = "ConversationCompactionNotNeeded"; } } export function conversationCompactionBoundarySha256( plan: ConversationCompactionPlan, output: ConversationCompactionOutput, ): string { return sha256(canonicalJson({ compactionPlan: plan as unknown as JsonObject, structuredOutput: output as unknown as JsonObject, })); } export function renderConversationCompactionBoundary(output: ConversationCompactionOutput): string { const sections = [ output.boundary.trim(), renderList("Open obligations", output.openLoops), renderList("Decisions", output.decisions), renderList("Exact references", output.exactReferences), renderList("Unresolved", output.unresolved), renderList("Lookup hints", output.lookupHints), ].filter(Boolean); const rendered = sections.join("\n\n"); if (rendered.length > 64_000) throw new Error("Rendered conversation compaction boundary exceeds the native message limit"); return rendered; } function renderList(title: string, values: string[]): string { return values.length === 0 ? "" : [`## ${title}`, ...values.map((value) => `- ${value}`)].join("\n"); }