Something went wrong. Try again.
A local-first event pipeline for independent agents, built on Jazz.
Something went wrong. Try again.
5.2 kB · 125 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126import { z } from "zod";import { canonicalJson, type JsonObject } from "../core/json.js";import type { ThoughtEvent } from "../events/types.js";import { eventIdFor, type JazzThoughtStore } from "../jazz/store.js";import { POST_TRAINING_COURSE, POST_TRAINING_COURSE_ID, postTrainingLesson, postTrainingLessonContext,} from "./post-training.js";
export const COURSE_QUESTION_EVENT_TYPE = "stream.thought.source.course.question";export const COURSE_QUESTION_SCHEMA_VERSION = 1;export const COURSE_QUESTION_SOURCE = `web-course:${POST_TRAINING_COURSE_ID}`;export const COURSE_TUTOR_AGENT_ID = "post-training-course-tutor";export const COURSE_QUESTION_MAX_CHARS = 2_000;
const requestIdSchema = z.string().min(1).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
export const courseQuestionRequestSchema = z.object({ requestId: requestIdSchema, courseRevision: z.string().regex(/^[a-f0-9]{64}$/), lessonId: z.string().min(1).max(100), sectionId: z.string().min(1).max(100).optional(), question: z.string().trim().min(1).max(COURSE_QUESTION_MAX_CHARS),}).strict();
export const courseQuestionPayloadSchema = z.object({ requestId: requestIdSchema, courseId: z.literal(POST_TRAINING_COURSE_ID), courseRevision: z.string().regex(/^[a-f0-9]{64}$/), lessonId: z.string().min(1).max(100), lessonTitle: z.string().min(1).max(200), sectionId: z.string().min(1).max(100).optional(), sectionTitle: z.string().min(1).max(200).optional(), question: z.string().min(1).max(COURSE_QUESTION_MAX_CHARS), lessonContext: z.string().min(1).max(48_000), interface: z.literal("thought-stream-inspector"),}).strict();
export type CourseQuestionRequest = z.infer<typeof courseQuestionRequestSchema>;export type CourseQuestionPayload = z.infer<typeof courseQuestionPayloadSchema>;
export async function appendPostTrainingCourseQuestion( store: JazzThoughtStore, input: CourseQuestionRequest, options: { occurredAt?: string | undefined; actor?: string | undefined } = {},) { const request = courseQuestionRequestSchema.parse(input); if (request.courseRevision !== POST_TRAINING_COURSE.revision) { throw new CourseRevisionConflictError(); } const lesson = postTrainingLesson(request.lessonId); if (!lesson) throw new Error("Course lesson is unknown"); const section = request.sectionId ? lesson.sections.find((candidate) => candidate.id === request.sectionId) : undefined; if (request.sectionId && !section) throw new Error("Course section is unknown"); const payload = courseQuestionPayloadSchema.parse({ requestId: request.requestId, courseId: POST_TRAINING_COURSE_ID, courseRevision: POST_TRAINING_COURSE.revision, lessonId: lesson.id, lessonTitle: lesson.title, ...(section ? { sectionId: section.id, sectionTitle: section.title } : {}), question: request.question, lessonContext: postTrainingLessonContext(lesson.id, section?.id), interface: "thought-stream-inspector", }); const idempotencyKey = `course-question-v1:${payload.requestId}`; const eventId = eventIdFor(COURSE_QUESTION_SOURCE, idempotencyKey); const existing = await store.getEvent(eventId); if (existing) { const actual = parseCourseQuestionEvent(existing); if (!actual || canonicalJson(actual as unknown as JsonObject) !== canonicalJson(payload as unknown as JsonObject)) { throw new CourseQuestionConflictError(); } return { event: existing, inserted: false }; } const appended = await store.appendEvent({ type: COURSE_QUESTION_EVENT_TYPE, schemaVersion: COURSE_QUESTION_SCHEMA_VERSION, source: COURSE_QUESTION_SOURCE, sourceKind: "web", externalId: payload.requestId, idempotencyKey, occurredAt: options.occurredAt ?? new Date().toISOString(), actor: options.actor ?? "operator:cameron", correlationId: payload.requestId, privacy: "sensitive", payload: payload as unknown as JsonObject, }); const actual = parseCourseQuestionEvent(appended.event); if (!actual || canonicalJson(actual as unknown as JsonObject) !== canonicalJson(payload as unknown as JsonObject)) { throw new Error("Course question receipt is inconsistent"); } return appended;}
export function parseCourseQuestionEvent(event: ThoughtEvent): CourseQuestionPayload | undefined { if (event.type !== COURSE_QUESTION_EVENT_TYPE || event.schemaVersion !== COURSE_QUESTION_SCHEMA_VERSION || event.source !== COURSE_QUESTION_SOURCE || event.sourceKind !== "web" || event.privacy !== "sensitive" || event.externalId !== event.correlationId || event.rootEventId !== event.id) return undefined; const parsed = courseQuestionPayloadSchema.safeParse(event.payload); if (!parsed.success || parsed.data.requestId !== event.externalId) return undefined; return parsed.data;}
export class CourseRevisionConflictError extends Error { constructor() { super("Course revision changed"); this.name = "CourseRevisionConflictError"; }}
export class CourseQuestionConflictError extends Error { constructor() { super("Course question identity changed"); this.name = "CourseQuestionConflictError"; }}