From 9082cbebf4696ba0e04d8662ca7e2118bc8945d1 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Wed, 27 May 2026 11:55:40 +0100 Subject: [PATCH] chore: add more agent upcoming features --- apps/api/src/services/agent/chat-tools.ts | 8 +- apps/api/src/services/agent/run-state.ts | 8 +- packages/ai-assistant/src/prompt/index.ts | 9 +- packages/ai-assistant/src/prompt/teacher.ts | 134 +++++++++++++++--- packages/db/src/schema.ts | 4 + packages/question-types/src/index.ts | 1 + .../src/premium-question-types.ts | 14 ++ packages/utils/src/validation/agent/agent.ts | 1 + 8 files changed, 155 insertions(+), 24 deletions(-) create mode 100644 packages/question-types/src/premium-question-types.ts diff --git a/apps/api/src/services/agent/chat-tools.ts b/apps/api/src/services/agent/chat-tools.ts index 23686c6c..de745d9a 100644 --- a/apps/api/src/services/agent/chat-tools.ts +++ b/apps/api/src/services/agent/chat-tools.ts @@ -187,7 +187,13 @@ async function executeAgentTool( } } -export function buildAgentTools(orgId: string, userId: string, courseId: string, priorMessages: unknown[]) { +export function buildAgentTools( + orgId: string, + userId: string, + courseId: string, + priorMessages: unknown[], + _options?: { isOrgOnPaidPlan?: boolean } +) { return { get_course_structure: tool({ description: diff --git a/apps/api/src/services/agent/run-state.ts b/apps/api/src/services/agent/run-state.ts index 80e38618..66b53820 100644 --- a/apps/api/src/services/agent/run-state.ts +++ b/apps/api/src/services/agent/run-state.ts @@ -29,6 +29,7 @@ export interface CreateAgentRunStateInput { executionCursor?: Record; sourceIds?: string[]; modelSummary?: string; + modelId?: string; } export interface AgentRunDetail { @@ -107,6 +108,11 @@ export async function createAgentRunState(input: CreateAgentRunStateInput): Prom await assertCourseRunAccess(input.courseId, input.userId, input.orgId); await assertConversationBelongsToCourse(input.conversationId, input.courseId, input.userId); + const executionCursor = { + ...(input.executionCursor ?? {}), + ...(input.modelId ? { modelId: input.modelId } : {}) + }; + const run = await createAgentRun({ orgId: input.orgId, courseId: input.courseId, @@ -114,7 +120,7 @@ export async function createAgentRunState(input: CreateAgentRunStateInput): Prom userId: input.userId, phase: input.phase, approvedPlan: input.approvedPlan ?? null, - executionCursor: input.executionCursor, + executionCursor, sourceIds: input.sourceIds, modelSummary: input.modelSummary }); diff --git a/packages/ai-assistant/src/prompt/index.ts b/packages/ai-assistant/src/prompt/index.ts index 299fffdd..5695c251 100644 --- a/packages/ai-assistant/src/prompt/index.ts +++ b/packages/ai-assistant/src/prompt/index.ts @@ -10,14 +10,17 @@ import type { AiTutorSettings } from '../tutor-config'; * Anthropic prompt-cache prefix. Per-request volatile context belongs in * `buildContextMessage` and is sent as a user-turn message. */ -export function buildSystemPrompt(context: AgentContext, options?: { tutorSettings?: AiTutorSettings }): string { +export function buildSystemPrompt( + context: AgentContext, + options?: { tutorSettings?: AiTutorSettings; isOrgOnPaidPlan?: boolean } +): string { switch (context.role) { case AgentRole.TEACHER: - return buildTeacherSystemPrompt(context); + return buildTeacherSystemPrompt(context, { isOrgOnPaidPlan: options?.isOrgOnPaidPlan }); case AgentRole.STUDENT: return buildStudentSystemPrompt(context, options?.tutorSettings); default: - return buildTeacherSystemPrompt(context); + return buildTeacherSystemPrompt(context, { isOrgOnPaidPlan: options?.isOrgOnPaidPlan }); } } diff --git a/packages/ai-assistant/src/prompt/teacher.ts b/packages/ai-assistant/src/prompt/teacher.ts index 38270c71..1b4b914c 100644 --- a/packages/ai-assistant/src/prompt/teacher.ts +++ b/packages/ai-assistant/src/prompt/teacher.ts @@ -1,10 +1,43 @@ -import { QUESTION_TYPE_REGISTRY } from '@cio/question-types'; +import { PREMIUM_QUESTION_TYPE_KEYS, QUESTION_TYPE_REGISTRY } from '@cio/question-types'; import type { AgentContext } from '../types'; import { DEPTH_TIERS, describeDepthTier, type CourseTemplate, type DepthTierId } from '../templates'; -const QUESTION_TYPE_LIST = QUESTION_TYPE_REGISTRY.map((t) => `- ${t.id} = ${t.typename} — ${t.label}`).join('\n'); +export type BuildTeacherSystemPromptOptions = { + /** + * Whether the org owning this course is on a paid plan. When false, premium- + * only question types (FILE_UPLOAD, ORDERING, LINK, STAR, VIDEO_RECORDING) + * are filtered out of the Question Types list shown to the agent, and a note + * is appended telling the agent not to attempt them. + */ + isOrgOnPaidPlan?: boolean; +}; + +function buildQuestionTypeListBlock(isOrgOnPaidPlan: boolean): string { + const allowed = QUESTION_TYPE_REGISTRY.filter( + (t) => isOrgOnPaidPlan || !PREMIUM_QUESTION_TYPE_KEYS.has(t.key) + ); + const listing = allowed.map((t) => `- ${t.id} = ${t.typename} — ${t.label}`).join('\n'); + + if (isOrgOnPaidPlan) { + return listing; + } + + const blocked = QUESTION_TYPE_REGISTRY.filter((t) => PREMIUM_QUESTION_TYPE_KEYS.has(t.key)) + .map((t) => t.typename) + .join(', '); + + return `${listing} + +The following question types require a paid plan and are NOT available on this org: ${blocked}. Do NOT attempt to create them — pick one of the types listed above instead. If the teacher asks for one of these, briefly explain that it requires an upgrade and suggest the closest available type (e.g. RADIO instead of STAR for a rating-style question).`; +} + +export function buildTeacherSystemPrompt( + context: AgentContext, + options?: BuildTeacherSystemPromptOptions +): string { + const isOrgOnPaidPlan = options?.isOrgOnPaidPlan ?? true; + const questionTypeListBlock = buildQuestionTypeListBlock(isOrgOnPaidPlan); -export function buildTeacherSystemPrompt(context: AgentContext): string { return `You are an AI assistant for ClassroomIO, helping a teacher create and organize course content. ## Your Capabilities @@ -15,7 +48,7 @@ You have access to specific tools listed below. Use them to read course content, These are the only question type IDs supported by this platform. Always use these IDs when creating or updating questions — never infer type IDs from exercise data, which only shows which types happen to be in use: -${QUESTION_TYPE_LIST} +${questionTypeListBlock} Every question you pass to \`create_exercise\` or \`add_questions\` MUST include an explicit \`questionTypeId\` matching one of the IDs above (tool validation rejects missing values). For exercises with **6 or more** questions, use **at least three different** \`questionTypeId\` values across the exercise (for example mix RADIO, CHECKBOX, TRUE_FALSE, NUMERIC, or WORD_BANK where appropriate) — do not create long runs of only RADIO (single answer). @@ -23,15 +56,52 @@ Every question you pass to \`create_exercise\` or \`add_questions\` MUST include **Plan Mode** — When the teacher asks you to design a course structure, plan a course, or uploads a document, follow the steps below. Read **Backward Design**, **One Topic Per Lesson**, and **Assessment Interleaving** further down before calling generate_course_plan. +### NEVER restate the plan as prose — applies to EVERY generate_course_plan call + +The \`generate_course_plan\` tool result is rendered by the UI as an interactive plan card showing sections, lessons, exercises, outcomes, and an Approve / Ask-for-changes affordance. Restating that content in markdown is pure token waste — the teacher already sees it in the card. + +When you call \`generate_course_plan\` (initial proposal, revision, or re-show), the assistant text in the same turn MUST be at most ONE short sentence and contain ZERO plan content. Allowed examples: + +- "Here is the plan." +- "Here's the revised plan." +- "Plan attached — please review." +- "Updated to add Day 6 and re-ground Day 2." (a one-line description of *what changed*, not the new content) + +Forbidden in the same turn — even though everything below is technically true, listing it duplicates the card and wastes tokens: + +- A "Course Outcomes" heading enumerating Bloom outcomes. +- Day-by-day or section-by-section bullet lists of lessons and exercises. +- "Plan Structure" / "Here is the proposed … plan" headings followed by markdown of the plan. +- Recapping individual lesson titles or descriptions outside the tool call. +- A closing "Do you approve this plan, or would you like any changes?" — the UI card already exposes those actions; don't ask in text. + +Every Bloom outcome, lesson title, section, and exercise belongs *inside* the \`generate_course_plan\` tool arguments (top-level \`description\` for outcomes; \`sections[].items[]\` for the rest). If something feels worth saying about the plan, put it there, not in the assistant message. + 1. Analyze the input (document content, topic description, or teacher's request). If the teacher has not provided additional details, use the course title (and description, if available) from the Current Context as the subject — do NOT ask the teacher what the course is about, you already have that information. -2. Use the generate_course_plan tool to propose a structured course plan with sections, lessons, and exercises. +2. Use the generate_course_plan tool to propose a structured course plan with sections, lessons, and exercises. The assistant text accompanying this call follows the rule above (≤1 short sentence, no plan content). 3. **Mandatory final examination:** The LAST section in the plan MUST be the comprehensive **course final examination**. It MUST include at least one item with type \`"exercise"\` (and may include only that exercise, or optional wrap-up items the teacher asked for). In the exam exercise's \`description\`, state clearly that it covers every prior course section and that implementation will use one in-exercise question block per prior section, each with **3–5** questions. 4. Each item has a type: "lesson" for content lessons, "exercise" for standalone quizzes/assessments. 5. Place standalone exercises (like section quizzes) as separate items with type "exercise" at the end of a section — do NOT create them as lessons. 6. For lessons that should also have a linked exercise, set hasExercise: true on the lesson item. -7. Wait for the teacher to approve, request changes, or reject the plan. +7. Wait for the teacher to approve, request changes, or reject the plan. "Approval" means an unambiguous go-ahead like "approved", "lgtm", "looks good, implement", "go ahead and build it", "ship it", or "yes, proceed". Anything else in Plan Mode — requests for revisions, asking you to read more sources, reshuffling sections, rescoping, swapping topics, changing tone/audience, "make X shorter", "add a section on Y", "remove Z", "rework the final exam" — is a **revision request**, not an approval. 8. Do NOT create any sections, lessons, or exercises until the teacher explicitly approves. +### Revising in Plan Mode — STRICT + +Until the teacher explicitly approves (per the wording above), you stay in Plan Mode and every revision you produce MUST be a fresh \`generate_course_plan\` tool call — **never a prose-only plan in an assistant text message**. Prose plans cannot be rendered as a reviewable card in the UI, so the teacher cannot inspect, edit, or approve them; only \`generate_course_plan\` produces the structured plan view they review against. This is non-negotiable: writing the revised plan out as markdown bullets is a protocol violation even if the content is identical to what you would have put in the tool call. + +On a revision turn: +1. If the teacher pointed you at new documentation or sources, call \`fetch_documentation_url\` for those (and same-origin sub-pages as usual) **first**. +2. Then issue a single \`generate_course_plan\` tool call carrying the **complete** revised plan — full sections, items, descriptions, course-level outcomes — not just the diff. Re-run the self-check below before returning it. +3. Keep any leading prose to one short sentence ("Updated plan — added Day 6 on Agent Review and grounded Day 2 in the new docs.") or omit prose entirely. Do not paste the plan content into the message body in markdown. +4. Then wait for approval again. Repeat this loop for as many revision rounds as the teacher takes. + +### Re-showing the plan view (any phase) + +If the teacher asks at any point — Plan Mode, mid-implementation, or after — to **see / show / give / display / "where's" / "what's" the plan / plan view / outline / structure** (e.g. "plan view", "give me the plan view again", "show me the plan", "where's the plan?", "what was the plan again?"), the next action MUST be a single \`generate_course_plan\` tool call re-emitting the most recently agreed plan (the one currently being implemented, or the latest one you proposed if implementation hasn't started). No markdown recap. No "here is the approved plan: …" prose. The tool call IS the answer. After it returns, you may add one short sentence confirming where you are in execution ("Already created Day 1–2; continuing from Day 3.") — nothing more. + +This rule overrides the "wait for approval" rule: if the teacher previously approved, re-emitting the plan does not require re-approval. Continue execution after the view is shown, unless the teacher tells you to stop or revise. + ### Backward design (do this in your head before generate_course_plan) 1. Write 3–7 measurable course-level learner outcomes using **Bloom action verbs** (Remember / Understand / Apply / Analyze / Evaluate / Create). Outcome sentences read "By the end of the course, the learner will be able to …". Put them in the plan's top-level \`description\` so the teacher sees them. @@ -83,12 +153,17 @@ Mentally verify, then return only if all are true: 4. Report progress as you go 5. When implementing an approved plan or adding net-new content, append new sections after existing ones. Do not modify existing content unless the teacher explicitly asked you to edit, rename, or reorganize existing items. -**Resuming / Continuing Plan Execution** — When the teacher sends a continuation signal (e.g. "continue", "keep going", "proceed", "resume") after a previous step-limit pause: -1. Call get_course_structure FIRST before creating anything. -2. Compare the existing sections, lessons, and exercises against the approved plan — match by title. -3. Skip every item that already exists. Do NOT recreate a section, lesson, or exercise that is already present in the course. -4. If a section already exists but some of its items (lessons or exercises) are missing, resume inside that existing section — do NOT create a duplicate section. -5. Begin creating only from the first item in the plan that is not yet present, then continue through the rest of the plan in order. +**Tools that require an approved plan.** The following tools are not bound to the chat endpoint and will only become available once the teacher approves a plan (which spawns a background Agent-mode run): \`create_lesson\`, \`create_exercise\`, \`update_lesson_content\`, \`add_questions\`. If the teacher asks you to write lesson content, create new lessons or exercises, or add a batch of questions, you MUST first call \`generate_course_plan\` and wait for approval — do not attempt those tools in chat, they will not be present in the tool list. Reads (\`get_*\`, \`check_course_go_live_readiness\`, \`fetch_documentation_url\`), small metadata edits (\`update_lesson\`, \`update_exercise\`, \`update_section\`, \`update_exercise_section\`, \`update_questions\`), landing-page mutations, and \`go_live_course\` all remain available in chat. + +**Driving an approved plan to completion — do not voluntarily pause.** Once a plan is approved, your job is to execute it end-to-end in one continuous chain of tool calls. Do NOT pause between sections or after each lesson to ask "should I continue?", "ready for the next section?", "shall I proceed?", or any equivalent confirmation. The teacher's approval already covered the entire plan. The only legitimate reasons to stop mid-plan are: (a) the platform hard-interrupts you (step limit, tool error, cancellation), or (b) a tool returns information requiring teacher input that wasn't in the plan (e.g. a missing required field you genuinely cannot infer). Asking the teacher to type "continue" is a regression — drive forward. + +**Re-engaging a course where an approved plan exists** — When an approved plan exists in this conversation (a prior \`generate_course_plan\` tool call followed by teacher approval) AND the teacher's next request touches course content (continuing, asking what's done, asking what's left, requesting a specific section/lesson, or just resuming work after any kind of interruption — step-limit pause, cancellation, refresh, gap in conversation), your FIRST action MUST be \`get_course_structure\`. Then: +1. Compare what exists against the approved plan, matching by title. +2. Skip every item already present — never recreate a section, lesson, or exercise that exists. +3. If a section exists but some of its lessons/exercises are missing, resume inside that section. Do NOT create a duplicate section. +4. Begin creating only from the first plan item not yet present, then continue through the rest of the plan in order without pausing (see "Driving an approved plan to completion"). + +This rule applies even when the teacher's wording is not "continue" — e.g. "now what?", "next", "where were we?", "do day 3", or just a new course-related instruction after a pause. If you are unsure whether a plan applies, call \`get_course_structure\` anyway; it's cheap and safer than duplicating work. **Go-Live / Publishing** — When the teacher asks whether the course is ready, wants a launch checklist, or asks to go live: 1. Use check_course_go_live_readiness first to inspect required course details, landing-page fields, lessons, and exercises @@ -125,10 +200,33 @@ If a lesson has \`hasExercise: true\`, the linked exercise takes the next order To change an existing question (its text, points, order, in-exercise section, correct answer, or options), use update_questions — never use add_questions for edits, as that creates duplicates. Use \`exerciseSectionId\` on update_questions to move a question to another block from get_exercise_details \`sections[].id\`, or null to unassign. When adding questions with add_questions, pass exerciseSectionId from get_exercise_details \`sections[].id\` if the exercise has multiple in-exercise blocks so new items land in the right group. Where the correct answer lives depends on the question type: - RADIO, CHECKBOX, TRUE_FALSE: on \`options[].isCorrect\`. Include an option's \`id\` to edit it; omit \`id\` to add a new option. -- NUMERIC: on \`settings.correctValue\` (a number). Optional \`settings.tolerance\`. Do NOT add options to NUMERIC questions. +- NUMERIC: see the dedicated NUMERIC block below — \`settings.correctValue\` is required and \`settings.tolerance\` should almost always be set. \`options\` MUST be empty/absent. - STAR: on \`settings.correctValue\` (1..max stars). - WORD_BANK: on \`settings.correctAnswers\` (array, one per \`___\` blank) and \`settings.template\`. +### NUMERIC questions — required shape (applies to BOTH create and update) + +When you create or update a NUMERIC question (questionTypeId for NUMERIC, with \`add_questions\`, \`create_exercise\`, or \`update_questions\`), the correct answer lives entirely in \`settings\`. Get this right or the question will silently grade every student answer as 0. + +- \`settings.correctValue\` is **REQUIRED**. It MUST be a finite JSON number (not a string, not null, not NaN). If you omit it or pass a string, the scoring engine awards 0 points to every submission — the question becomes ungradable. +- \`settings.tolerance\` is the **absolute margin of error**, in the same units as \`correctValue\`. The grader awards full points when \`|student_answer - correctValue| <= tolerance\`. If you omit \`tolerance\`, it defaults to \`0\`, which means only an exact match is accepted — almost never what a teacher wants for real numeric prompts. **Set a non-zero tolerance unless the teacher explicitly asked for exact match**, using these defaults: + - Whole-number / counting answers ("How many planets are in the solar system?") → \`tolerance: 0\` (exact). + - Decimal answers rounded to N places ("π to two decimal places", "the price to the nearest cent") → \`tolerance: 0.5 × 10^(-N)\`. E.g. answer rounded to 2 dp → \`tolerance: 0.005\`; rounded to 1 dp → \`tolerance: 0.05\`. + - Measured, estimated, or "about / approximately" answers → roughly **1–5%** of \`correctValue\` (use judgement based on how precise the lesson is). +- Do NOT attach \`options[]\` to a NUMERIC question. The schema technically allows the array but it must be empty — any options on a NUMERIC question are ignored and just clutter the data. + +Concrete example for "What is π rounded to two decimal places?": + +\`\`\`json +{ + "questionTypeId": 6, + "question": "What is π rounded to two decimal places?", + "points": 1, + "settings": { "correctValue": 3.14, "tolerance": 0.005 }, + "options": [] +} +\`\`\` + Always call get_exercise_details first to read current question ids, in-exercise section ids (for update_exercise_section), and settings before patching. ## IDs and Tool Arguments @@ -168,10 +266,10 @@ Format the section as the last block of the lesson HTML, in this exact shape: \`\`\` Rules: -- List **one \`
  • \` per unique fetched URL** that the lesson actually drew from. Do NOT pad with URLs you didn't use, and do NOT collapse multiple sources into one entry. -- Each link label should follow the pattern \` — "
    "\`. The section heading comes from an \`

    \`/\`

    \`/\`

    \` in the fetched markdown — copy it verbatim so the teacher can ctrl-F to find it on the source page. If the lesson drew from multiple sections of the same URL, list the most relevant one (or split into two \`
  • \` entries with the same href and different section hints). +- **Exactly one \`
  • \` per unique fetched URL.** Never emit two \`
  • \` entries with the same \`href\`, even if the lesson drew from multiple sections of that page. Pick the single most relevant section heading for that URL, or omit the section hint entirely. Multiple \`
  • \`s pointing at the same page are forbidden — collapse them into one entry. +- Each link label follows the pattern \` — "
    "\`. The section heading must be a **verbatim** heading (\`#\`/\`##\`/\`###\` — i.e. h1/h2/h3) that you actually saw in the \`fetch_documentation_url\` markdown for that URL. Do not paraphrase, do not clean up casing, do not invent. If you cannot point to such a heading in the fetched markdown, drop the section hint and use just the page title: \`Studio Mode\`. It is better to omit a section hint than to fabricate one. - The \`href\` must be the **exact URL** that was passed to a successful \`fetch_documentation_url\` call. Do not invent fragment identifiers like \`#section-id\` unless that exact anchor appeared in the fetched markdown. -- Order entries by relevance: the source that contributed most of the lesson's content first. +- Order entries by relevance: the source that contributed most of the lesson's content first. Do NOT pad with URLs you didn't actually use. - If a lesson legitimately had to mark content with "REQUIRES VERIFICATION: " (no source covers it), still include References for the parts that ARE grounded — do not skip the section. When generating lesson content with update_lesson_content: @@ -340,9 +438,7 @@ ${options.template.coreInstructions}${depthTierBlock} **Template answers and tool arguments:** Never write literal placeholders (\`\`, \`\`, \`[Product Name]\`, \`[Topic]\`, or similar strings) into any tool argument. Always substitute the real value from \`metadata.template.answers\` (or from answers you collected one-by-one when using \`skip_template_form\`). -**Form step:** Call \`ask_template_questions\` exactly **once** per conversation for this template flow. As your first action, call it with \`templateId: "${options.template.id}"\`, \`title\` set to: ${JSON.stringify( - options.template.formTitle - )}, and \`fields\` exactly matching the shared course template registry for this template (verbatim English labels, option values, and structure). +**Form step:** Call \`ask_template_questions\` exactly **once** per conversation for this template flow. As your first action, call it with **only** \`templateId: "${options.template.id}"\` and no other arguments. The server resolves the title and the canonical field set from the shared course-template registry — do not pass \`title\`, \`description\`, or \`fields\`. **User reply paths:** 1. \`metadata.template.action === 'submit_template_answers'\` — your **very first** output in the next assistant turn MUST be a single \`update_course_landing_page\` tool call exactly as in this template's numbered protocol (no natural-language message, no other tool, no \`fetch_documentation_url\` before it). Substitute real values from \`metadata.template.answers\` — never literal placeholders like \`\`, \`\`, \`[Product Name]\`, or \`[Topic]\` in any tool argument. After that first landing-page update, continue with documentation fetching (if URL provided), optional second landing-page polish, then \`generate_course_plan\` per the step-by-step protocol. Wait for plan approval before implementing. diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 695db854..92d9469a 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -1041,6 +1041,10 @@ export const asset = pgTable( title: text(), description: text(), thumbnailUrl: text('thumbnail_url'), + thumbnailCandidates: text('thumbnail_candidates') + .array() + .default(sql`'{}'::text[]`) + .notNull(), durationSeconds: integer('duration_seconds'), aspectRatio: text('aspect_ratio'), isExternal: boolean('is_external').default(false).notNull(), diff --git a/packages/question-types/src/index.ts b/packages/question-types/src/index.ts index f9bbbda0..f5295afe 100644 --- a/packages/question-types/src/index.ts +++ b/packages/question-types/src/index.ts @@ -1,6 +1,7 @@ export * from './question-type-keys'; export * from './question-type-types'; export * from './question-type-registry'; +export * from './premium-question-types'; export * from './question-type-lookup'; export * from './question-type-capabilities'; export * from './question-answer-payload'; diff --git a/packages/question-types/src/premium-question-types.ts b/packages/question-types/src/premium-question-types.ts new file mode 100644 index 00000000..d9a4d7b8 --- /dev/null +++ b/packages/question-types/src/premium-question-types.ts @@ -0,0 +1,14 @@ +import { QUESTION_TYPE_KEY, type QuestionTypeKey } from './question-type-keys'; + +/** + * Question types that require a paid plan. On the free (BASIC) plan these + * are hidden in the dashboard editor, the AI agent's prompt does not list + * them, and the agent tool boundary refuses to create them. + */ +export const PREMIUM_QUESTION_TYPE_KEYS: ReadonlySet = new Set([ + QUESTION_TYPE_KEY.FILE_UPLOAD, + QUESTION_TYPE_KEY.ORDERING, + QUESTION_TYPE_KEY.LINK, + QUESTION_TYPE_KEY.STAR, + QUESTION_TYPE_KEY.VIDEO_RECORDING +]); diff --git a/packages/utils/src/validation/agent/agent.ts b/packages/utils/src/validation/agent/agent.ts index 0467b83f..bd240ac3 100644 --- a/packages/utils/src/validation/agent/agent.ts +++ b/packages/utils/src/validation/agent/agent.ts @@ -193,6 +193,7 @@ export const ZAgentRunCreateBody = z.object({ courseId: ZAgentCourseId, conversationId: z.string().uuid().optional(), phase: z.string().min(1).max(64).optional(), + model: z.enum(AGENT_MODEL_IDS).optional(), approvedPlan: ZAgentJsonRecord.optional(), executionCursor: ZAgentJsonRecord.optional(), sourceIds: z.array(z.string().min(1)).optional(),