import { z } from 'zod'; import type { AttributionConfidence, ConversationEventKind } from '../database/conversation-event-repository.js'; import type { ConversationPolicyMode } from './conversation-state.js'; export const LearningStageSchema = z.enum([ 'rapport', 'answer', 'flirt', 'disclosure', 'media-invitation', 'handoff', 'unknown', ]); export type LearningStage = z.infer; export const LearningTurnSchema = z.strictObject({ eventId: z.string(), timestamp: z.number(), role: z.enum(['recipient', 'operator']), actor: z.enum(['recipient', 'ai', 'human']), kind: z.enum([ 'inbound-text', 'opener', 'automation-reply', 'manual-text', 'library-reciprocal', ]), text: z.string(), }); export type LearningTurn = z.infer; export const LearningCreditClasses = [ 'human-created-success', 'human-improved-success', 'human-completed-ai-trajectory', 'autonomous-success', 'recipient-initiated-media', 'uncertain', ] as const; export type LearningCreditClass = (typeof LearningCreditClasses)[number]; export type LearningTrajectoryStatus = 'provisional' | 'eligible' | 'excluded' | 'invalidated' | 'needs-review'; export type LearningDecisionStatus = 'eligible' | 'excluded' | 'invalidated' | 'needs-review'; export interface LearningTrajectoryRecord { id: string; accountId: string; conversationId: string; sentMessageId: number | null; subreddit: string | null; sourceType: 'image' | 'chat' | null; policyMode: ConversationPolicyMode | null; firstHumanEventId: string | null; outcomeEventId: string; outcomeType: 'image-received' | 'reciprocal-engagement'; creditClass: LearningCreditClass; attributionConfidence: AttributionConfidence; status: LearningTrajectoryStatus; maturityAt: number; invalidatedAt: number | null; invalidationReason: string | null; reviewedAt: number | null; createdAt: number; updatedAt: number; } export interface LearningDecisionRecord { id: string; trajectoryId: string; accountId: string; decisionEventId: string; stage: LearningStage; context: LearningTurn[]; chosenResponse: string; turnsUntilOutcome: number; isDirectMediaInvitation: boolean; qualityStatus: LearningDecisionStatus; createdAt: number; updatedAt: number; } export interface LearningTrajectoryDetail extends LearningTrajectoryRecord { decisions: LearningDecisionRecord[]; } export interface RetrievedLearningExample { decisionId: string; trajectoryId: string; creditClass: LearningCreditClass; stage: LearningStage; context: LearningTurn[]; chosenResponse: string; turnsUntilOutcome: number; subreddit: string | null; policyMode: ConversationPolicyMode | null; } export interface ConversationLearningQuery { accountId: string; conversationId: string; subreddit: string | null; policyMode: ConversationPolicyMode; stage: LearningStage; purpose: 'reply' | 'media-ask'; incomingEventId: string; } export interface ConversationLearningProvider { getExamples(query: ConversationLearningQuery): readonly RetrievedLearningExample[]; recordGeneration(input: { query: ConversationLearningQuery; mode: 'shadow' | 'injected'; policyVersion: string; exampleIds: readonly string[]; promptSha256?: string | null; generatedAt?: number; }): void; } export interface AttributedConversationMessage { eventId: string; timestamp: number; isSelf: boolean; text: string; mediaCount: number; actor: 'recipient' | 'ai' | 'human' | 'unknown'; kind: ConversationEventKind; confidence: AttributionConfidence; }