/** * Apply a sequence of patch operations (the tool_use calls returned by * `refineLearningContent`) to an article object, in order. The returned * article is a fresh object — the input is not mutated. * * Recognised tool names mirror `llmTools.js`: * set_intro, set_section, add_section, remove_section, replace_takeaways. * * Unknown tool names are ignored on purpose; the caller validates the * result against `learningArticleSchema` and rejects the whole turn if * the patches produced an invalid article. */ import { learningArticleSchema } from './llmSchemas'; function matchesHeading(section, heading) { return (section.heading ?? '').trim().toLowerCase() === heading.trim().toLowerCase(); } function cloneArticle(article) { return { ...article, sections: article.sections.map((s) => ({ ...s })), keyTakeaways: [...article.keyTakeaways], }; } export function applyArticlePatches(article, toolUses) { let next = cloneArticle(article); for (const tu of toolUses) { switch (tu.name) { case 'set_intro': next = { ...next, intro: tu.input.intro }; break; case 'set_section': { const idx = next.sections.findIndex((s) => matchesHeading(s, tu.input.heading)); if (idx === -1) { // No matching section — fall back to appending so the model's // intent (provide that body) is preserved rather than lost. next.sections = [...next.sections, { heading: tu.input.heading, body: tu.input.body }]; } else { next.sections = next.sections.map((s, i) => (i === idx ? { ...s, body: tu.input.body } : s)); } break; } case 'add_section': { const newSection = { heading: tu.input.heading, body: tu.input.body }; next.sections = tu.input.position === 'start' ? [newSection, ...next.sections] : [...next.sections, newSection]; break; } case 'remove_section': next.sections = next.sections.filter((s) => !matchesHeading(s, tu.input.heading)); break; case 'replace_takeaways': next = { ...next, keyTakeaways: [...tu.input.items] }; break; default: // Unknown patch op — ignore. break; } } return next; } /** * Apply the patches and re-validate against the article schema. Throws * a clear error if the result is invalid. */ export function applyAndValidate(article, toolUses) { const updated = applyArticlePatches(article, toolUses); const parsed = learningArticleSchema.safeParse({ article: updated }); if (!parsed.success) { const err = new Error(`Refinement produced an invalid article: ${parsed.error.message}`); err.cause = parsed.error; throw err; } return parsed.data.article; }