@aglyn/plugins-ai 1.0.0-beta.293 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +17 -3
  3. package/src/lib/components/ai-job-links.js +32 -5
  4. package/src/lib/components/ai-job-links.js.map +1 -1
  5. package/src/lib/components/ai-job-requests.d.ts +9 -0
  6. package/src/lib/components/ai-job-requests.js +34 -0
  7. package/src/lib/components/ai-job-requests.js.map +1 -1
  8. package/src/lib/components/ai-site-build-page.component.js +28 -2
  9. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  10. package/src/lib/components/assist-edit-canvas.d.ts +12 -6
  11. package/src/lib/components/assist-edit-canvas.js +86 -36
  12. package/src/lib/components/assist-edit-canvas.js.map +1 -1
  13. package/src/lib/components/assist-edit-card.component.js +9 -8
  14. package/src/lib/components/assist-edit-card.component.js.map +1 -1
  15. package/src/lib/components/assist-jobs-drawer.component.d.ts +7 -1
  16. package/src/lib/components/assist-jobs-drawer.component.js +20 -6
  17. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  18. package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
  19. package/src/lib/components/assist-page-change-card.component.js +166 -0
  20. package/src/lib/components/assist-page-change-card.component.js.map +1 -0
  21. package/src/lib/components/assist-panel.component.d.ts +105 -1
  22. package/src/lib/components/assist-panel.component.js +242 -36
  23. package/src/lib/components/assist-panel.component.js.map +1 -1
  24. package/src/lib/jobs/ai-job-edit-step.d.ts +82 -16
  25. package/src/lib/jobs/ai-job-edit-step.js +209 -62
  26. package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-navigation-step.js +4 -2
  28. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  29. package/src/lib/model/ai-edit-job.d.ts +33 -16
  30. package/src/lib/model/ai-edit-job.js +50 -34
  31. package/src/lib/model/ai-edit-job.js.map +1 -1
  32. package/src/lib/model/ai-job-activity.d.ts +6 -11
  33. package/src/lib/model/ai-job-activity.js +8 -15
  34. package/src/lib/model/ai-job-activity.js.map +1 -1
  35. package/src/lib/model/ai-job-notice.js +3 -1
  36. package/src/lib/model/ai-job-notice.js.map +1 -1
  37. package/src/lib/model/ai-jobs.types.d.ts +6 -0
  38. package/src/lib/model/ai-jobs.types.js.map +1 -1
  39. package/src/lib/model/ai-site-inventory.d.ts +6 -0
  40. package/src/lib/model/ai-site-inventory.js.map +1 -1
  41. package/src/lib/model/assist-edit.d.ts +19 -1
  42. package/src/lib/model/assist-edit.js +7 -0
  43. package/src/lib/model/assist-edit.js.map +1 -1
  44. package/src/lib/model/assist-page-change.d.ts +100 -0
  45. package/src/lib/model/assist-page-change.js +107 -0
  46. package/src/lib/model/assist-page-change.js.map +1 -0
  47. package/src/lib/runtime/ai-eval.js +3 -1
  48. package/src/lib/runtime/ai-eval.js.map +1 -1
  49. package/src/lib/runtime/ai-node-tree.d.ts +5 -0
  50. package/src/lib/runtime/ai-node-tree.js +18 -3
  51. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  52. package/src/lib/runtime/site-inventory.d.ts +8 -1
  53. package/src/lib/runtime/site-inventory.js +18 -0
  54. package/src/lib/runtime/site-inventory.js.map +1 -1
  55. package/src/lib/server/assist-build.js +2 -0
  56. package/src/lib/server/assist-build.js.map +1 -1
  57. package/src/lib/server/assist-chat.js +56 -3
  58. package/src/lib/server/assist-chat.js.map +1 -1
  59. package/src/lib/server/assist-edit.d.ts +61 -4
  60. package/src/lib/server/assist-edit.js +248 -38
  61. package/src/lib/server/assist-edit.js.map +1 -1
  62. package/src/lib/server/assist-page-change.d.ts +62 -0
  63. package/src/lib/server/assist-page-change.js +140 -0
  64. package/src/lib/server/assist-page-change.js.map +1 -0
@@ -62,6 +62,8 @@ import { aiJobStepRunnerFor } from "../jobs/ai-jobs.js";
62
62
  'Building on this site:',
63
63
  `- When the user asks you to make or set up things on their site — pages, a header and footer, forms, or anything listed in the "A build here can make" block — write one or two sentences saying what you will plan, then call the ${ASSIST_BUILD_TOOL_NAME} tool once with the whole request. Write no JSON in your message.`,
64
64
  '- brief: everything they asked for, in their words, with each page, form and other part named and what each is for. Include what the conversation already settled. Do not invent parts they did not ask for beyond what their request plainly needs (a page that shows a form needs that form).',
65
+ // AGL-4116: each follow-up to a plan made another plan beside it.
66
+ '- When they change a plan you proposed earlier in this conversation and have not confirmed, call the tool again with the whole request as it now stands: what still holds from before, with their change. The new plan replaces the waiting one; say you have updated the plan.',
65
67
  '- publish: true only when they asked for the new pages to go live or be published; otherwise false. New work is an unpublished draft by default.',
66
68
  '- summary: one short sentence, such as "Plan three pages, a contact form and a booking service".',
67
69
  // AGL-4092: a brief the chat wrote asked the build to "use bracketed placeholders where details are missing".
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-build.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n ASSIST_BUILD_ACTION_ID,\n ASSIST_BUILD_SUMMARY_MAX_CHARS,\n ASSIST_BUILD_TOOL_NAME,\n type AssistBuildProposal,\n} from '../model/assist-build'\nimport type { AiTool } from '../providers/contract'\nimport { aiBuildIntents, aiBuildOps } from '../jobs/ai-build-capabilities'\nimport { aiBuildFreeTaste } from '../jobs/ai-job-build-step'\nimport { AI_JOB_BRIEF_MAX_CHARS } from '../jobs/ai-job-text-step'\nimport { aiJobStepRunnerFor } from '../jobs/ai-jobs'\n\n/**\n * The chat's build rung (AGL-3616): on a site, a request that names things\n * to make — \"a few new pages, a booking form and a contact form, then an\n * about page\" — is proposed as ONE build through a strict tool. The tool\n * carries the request, not a plan: planning is the build job's own plan\n * step, against the site's real inventory, with its re-ask and its limits.\n * The panel starts that job and shows its plan card with one price; nothing\n * is built until the person confirms it, and what is built is a draft unless\n * they asked to publish and tick the card's box.\n */\n\nconst STRING = { type: 'string' } as const\n\n/** The strict tool the model proposes a build through. */\nexport function assistBuildTool(): AiTool {\n return {\n name: ASSIST_BUILD_TOOL_NAME,\n description:\n 'Propose building what the user asked for on this site: pages, a header ' +\n 'and footer, forms and the other things listed. The user sees a plan with ' +\n 'its price and confirms it before anything is made. Call at most once per ' +\n 'message, with the whole request.',\n inputSchema: {\n type: 'object',\n properties: {\n summary: STRING,\n brief: STRING,\n publish: { type: 'boolean' },\n },\n required: ['summary', 'brief', 'publish'],\n additionalProperties: false,\n },\n strict: true,\n }\n}\n\n/**\n * The build protocol — the same for every site, so it sits inside the cached\n * prefix. What a build can make on THIS site is per site and rides\n * `assistBuildIntentsBlock`, after every breakpoint.\n */\nexport function assistBuildBlock(): string {\n return [\n 'Building on this site:',\n `- When the user asks you to make or set up things on their site — pages, a header and footer, forms, or anything listed in the \"A build here can make\" block — write one or two sentences saying what you will plan, then call the ${ASSIST_BUILD_TOOL_NAME} tool once with the whole request. Write no JSON in your message.`,\n '- brief: everything they asked for, in their words, with each page, form and other part named and what each is for. Include what the conversation already settled. Do not invent parts they did not ask for beyond what their request plainly needs (a page that shows a form needs that form).',\n '- publish: true only when they asked for the new pages to go live or be published; otherwise false. New work is an unpublished draft by default.',\n '- summary: one short sentence, such as \"Plan three pages, a contact form and a booking service\".',\n // AGL-4092: a brief the chat wrote asked the build to \"use bracketed placeholders where details are missing\".\n '- Never ask the build for placeholders, brackets or \"to be added\" text. Where the user gave no fact, say nothing of it: the build reads the site’s business profile and contact details itself.',\n '- Nothing is built when you call the tool: the user is shown a plan with its price and confirms it. Never say anything is made, and never quote a price.',\n '- Only offer what the \"A build here can make\" block lists. If they ask for something it does not list, say plainly it cannot be built here from the chat and build the rest.',\n '- A change to the site’s theme or look (its colors, fonts or corners) is offered only where the \"A build here can make\" block lists it, which it does on paid plans. When it is asked for and not listed, say a build cannot change the theme on this workspace’s plan and that they can change it in Setup → Theme, where the Theme assistant can propose one; then build the rest.',\n '- A question, or a change to the canvas the user has open, is not a build: answer it, or use the canvas edit tool where it is offered.',\n ].join('\\n')\n}\n\n/** What a build can make on this site, in each owner's own words. Per site: volatile. */\nexport function assistBuildIntentsBlock(intents: readonly string[]): string {\n return ['A build here can make:', ...intents.map((intent) => `- ${intent}`)].join('\\n')\n}\n\n/**\n * Where a build's facts come from, said once at the end of every brief the\n * chat proposes (AGL-4092): never a placeholder for one it lacks.\n */\nexport const ASSIST_BUILD_FACTS_LINE =\n 'Facts: use the site’s business profile and contact details on file; where a fact is missing, leave it out — no placeholders, no brackets.'\n\n/**\n * A clause of a brief that asks for a gap instead of a fact: \"use bracketed\n * placeholders where details are missing\", \"mark it in square brackets\",\n * \"show TBD\". A shop's \"shelf brackets\" asks for none.\n */\nconst ASKS_FOR_GAPS = /\\b(?:placeholders?|bracketed|in (?:square )?brackets|to be (?:added|filled|confirmed)|tbd)\\b/i\n\n/**\n * The brief with every clause that asks for placeholders, brackets or \"to be\n * added\" text taken out (AGL-4092): \"Do not invent names; use bracketed\n * placeholders where details are missing.\" keeps \"Do not invent names.\" A\n * sentence nothing is left of goes. Pure.\n */\nexport function assistBuildBriefWithoutGaps(brief: string): string {\n return brief\n .split('\\n')\n .map((line) =>\n line\n .split(/(?<=[.!?])\\s+/)\n .map((sentence) => {\n if (!ASKS_FOR_GAPS.test(sentence)) return sentence\n const kept = sentence\n .replace(/[.!?]$/, '')\n .split(/\\s*;\\s*/)\n .filter((clause) => !ASKS_FOR_GAPS.test(clause))\n .join('; ')\n .trim()\n return kept ? `${kept}.` : ''\n })\n .filter(Boolean)\n .join(' '),\n )\n .filter((line) => line.trim())\n .join('\\n')\n .trim()\n}\n\n/**\n * The tool call held to what a build job accepts, or `null` when it is\n * unusable. The site is the request's own, never the model's; the brief is\n * clamped to the job door's limit, with a line naming the console page it\n * was asked from so the plan step can read \"this page\" against it. Before\n * it, the brief asks for no placeholder and says where facts come from\n * (AGL-4092).\n */\nexport function resolveAssistBuild(\n input: Record<string, unknown> | null,\n scope: { hostId: string; screen: string | null },\n): AssistBuildProposal | null {\n if (!input) return null\n const asked = typeof input['brief'] === 'string' ? assistBuildBriefWithoutGaps(input['brief'].trim()) : ''\n if (!asked) return null\n const where = scope.screen ? `\\nAsked from the console page: ${scope.screen}` : ''\n const facts = `\\n${ASSIST_BUILD_FACTS_LINE}`\n const brief = `${asked.slice(0, AI_JOB_BRIEF_MAX_CHARS - facts.length - where.length).trim()}${facts}${where}`.trim()\n const summary =\n typeof input['summary'] === 'string' && input['summary'].trim()\n ? input['summary'].trim().slice(0, ASSIST_BUILD_SUMMARY_MAX_CHARS)\n : 'Plan what you asked for'\n return {\n id: ASSIST_BUILD_ACTION_ID,\n hostId: scope.hostId,\n brief,\n publish: input['publish'] === true,\n summary,\n }\n}\n\nexport interface AssistBuildIntentsDeps {\n ops?: typeof aiBuildOps\n hasBuildRunner?: () => boolean\n}\n\n/**\n * What a build may make on this site, or `null` where none can be built: a\n * deployment that has not loaded the build kind, or a site where no\n * operation passes its owner's gates. The same operations the build's plan\n * step offers, so the chat never offers what the plan cannot make.\n */\nexport async function assistBuildIntents(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; hostId: string; org: Record<string, unknown> },\n deps: AssistBuildIntentsDeps = {},\n): Promise<string[] | null> {\n const hasBuildRunner = deps.hasBuildRunner ?? (() => aiJobStepRunnerFor('build') !== null)\n if (!hasBuildRunner()) return null\n const host = ((await firestore.collection('hosts').doc(input.hostId).get()).data() ?? null) as Record<\n string,\n unknown\n > | null\n if (!host) return null\n const org = input.org as Partial<AglynOrgBilling>\n const ops = await (deps.ops ?? aiBuildOps)({\n orgId: input.orgId,\n hostId: input.hostId,\n org,\n host,\n freeTaste: aiBuildFreeTaste(org),\n })\n const intents = aiBuildIntents(ops)\n return intents.length ? intents : null\n}\n"],"names":["ASSIST_BUILD_ACTION_ID","ASSIST_BUILD_SUMMARY_MAX_CHARS","ASSIST_BUILD_TOOL_NAME","aiBuildIntents","aiBuildOps","aiBuildFreeTaste","AI_JOB_BRIEF_MAX_CHARS","aiJobStepRunnerFor","STRING","type","assistBuildTool","name","description","inputSchema","properties","summary","brief","publish","required","additionalProperties","strict","assistBuildBlock","join","assistBuildIntentsBlock","intents","map","intent","ASSIST_BUILD_FACTS_LINE","ASKS_FOR_GAPS","assistBuildBriefWithoutGaps","split","line","sentence","test","kept","replace","filter","clause","trim","Boolean","resolveAssistBuild","input","scope","asked","where","screen","facts","slice","length","id","hostId","assistBuildIntents","firestore","deps","hasBuildRunner","host","collection","doc","get","data","org","ops","orgId","freeTaste"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-build.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n ASSIST_BUILD_ACTION_ID,\n ASSIST_BUILD_SUMMARY_MAX_CHARS,\n ASSIST_BUILD_TOOL_NAME,\n type AssistBuildProposal,\n} from '../model/assist-build'\nimport type { AiTool } from '../providers/contract'\nimport { aiBuildIntents, aiBuildOps } from '../jobs/ai-build-capabilities'\nimport { aiBuildFreeTaste } from '../jobs/ai-job-build-step'\nimport { AI_JOB_BRIEF_MAX_CHARS } from '../jobs/ai-job-text-step'\nimport { aiJobStepRunnerFor } from '../jobs/ai-jobs'\n\n/**\n * The chat's build rung (AGL-3616): on a site, a request that names things\n * to make — \"a few new pages, a booking form and a contact form, then an\n * about page\" — is proposed as ONE build through a strict tool. The tool\n * carries the request, not a plan: planning is the build job's own plan\n * step, against the site's real inventory, with its re-ask and its limits.\n * The panel starts that job and shows its plan card with one price; nothing\n * is built until the person confirms it, and what is built is a draft unless\n * they asked to publish and tick the card's box.\n */\n\nconst STRING = { type: 'string' } as const\n\n/** The strict tool the model proposes a build through. */\nexport function assistBuildTool(): AiTool {\n return {\n name: ASSIST_BUILD_TOOL_NAME,\n description:\n 'Propose building what the user asked for on this site: pages, a header ' +\n 'and footer, forms and the other things listed. The user sees a plan with ' +\n 'its price and confirms it before anything is made. Call at most once per ' +\n 'message, with the whole request.',\n inputSchema: {\n type: 'object',\n properties: {\n summary: STRING,\n brief: STRING,\n publish: { type: 'boolean' },\n },\n required: ['summary', 'brief', 'publish'],\n additionalProperties: false,\n },\n strict: true,\n }\n}\n\n/**\n * The build protocol — the same for every site, so it sits inside the cached\n * prefix. What a build can make on THIS site is per site and rides\n * `assistBuildIntentsBlock`, after every breakpoint.\n */\nexport function assistBuildBlock(): string {\n return [\n 'Building on this site:',\n `- When the user asks you to make or set up things on their site — pages, a header and footer, forms, or anything listed in the \"A build here can make\" block — write one or two sentences saying what you will plan, then call the ${ASSIST_BUILD_TOOL_NAME} tool once with the whole request. Write no JSON in your message.`,\n '- brief: everything they asked for, in their words, with each page, form and other part named and what each is for. Include what the conversation already settled. Do not invent parts they did not ask for beyond what their request plainly needs (a page that shows a form needs that form).',\n // AGL-4116: each follow-up to a plan made another plan beside it.\n '- When they change a plan you proposed earlier in this conversation and have not confirmed, call the tool again with the whole request as it now stands: what still holds from before, with their change. The new plan replaces the waiting one; say you have updated the plan.',\n '- publish: true only when they asked for the new pages to go live or be published; otherwise false. New work is an unpublished draft by default.',\n '- summary: one short sentence, such as \"Plan three pages, a contact form and a booking service\".',\n // AGL-4092: a brief the chat wrote asked the build to \"use bracketed placeholders where details are missing\".\n '- Never ask the build for placeholders, brackets or \"to be added\" text. Where the user gave no fact, say nothing of it: the build reads the site’s business profile and contact details itself.',\n '- Nothing is built when you call the tool: the user is shown a plan with its price and confirms it. Never say anything is made, and never quote a price.',\n '- Only offer what the \"A build here can make\" block lists. If they ask for something it does not list, say plainly it cannot be built here from the chat and build the rest.',\n '- A change to the site’s theme or look (its colors, fonts or corners) is offered only where the \"A build here can make\" block lists it, which it does on paid plans. When it is asked for and not listed, say a build cannot change the theme on this workspace’s plan and that they can change it in Setup → Theme, where the Theme assistant can propose one; then build the rest.',\n '- A question, or a change to the canvas the user has open, is not a build: answer it, or use the canvas edit tool where it is offered.',\n ].join('\\n')\n}\n\n/** What a build can make on this site, in each owner's own words. Per site: volatile. */\nexport function assistBuildIntentsBlock(intents: readonly string[]): string {\n return ['A build here can make:', ...intents.map((intent) => `- ${intent}`)].join('\\n')\n}\n\n/**\n * Where a build's facts come from, said once at the end of every brief the\n * chat proposes (AGL-4092): never a placeholder for one it lacks.\n */\nexport const ASSIST_BUILD_FACTS_LINE =\n 'Facts: use the site’s business profile and contact details on file; where a fact is missing, leave it out — no placeholders, no brackets.'\n\n/**\n * A clause of a brief that asks for a gap instead of a fact: \"use bracketed\n * placeholders where details are missing\", \"mark it in square brackets\",\n * \"show TBD\". A shop's \"shelf brackets\" asks for none.\n */\nconst ASKS_FOR_GAPS = /\\b(?:placeholders?|bracketed|in (?:square )?brackets|to be (?:added|filled|confirmed)|tbd)\\b/i\n\n/**\n * The brief with every clause that asks for placeholders, brackets or \"to be\n * added\" text taken out (AGL-4092): \"Do not invent names; use bracketed\n * placeholders where details are missing.\" keeps \"Do not invent names.\" A\n * sentence nothing is left of goes. Pure.\n */\nexport function assistBuildBriefWithoutGaps(brief: string): string {\n return brief\n .split('\\n')\n .map((line) =>\n line\n .split(/(?<=[.!?])\\s+/)\n .map((sentence) => {\n if (!ASKS_FOR_GAPS.test(sentence)) return sentence\n const kept = sentence\n .replace(/[.!?]$/, '')\n .split(/\\s*;\\s*/)\n .filter((clause) => !ASKS_FOR_GAPS.test(clause))\n .join('; ')\n .trim()\n return kept ? `${kept}.` : ''\n })\n .filter(Boolean)\n .join(' '),\n )\n .filter((line) => line.trim())\n .join('\\n')\n .trim()\n}\n\n/**\n * The tool call held to what a build job accepts, or `null` when it is\n * unusable. The site is the request's own, never the model's; the brief is\n * clamped to the job door's limit, with a line naming the console page it\n * was asked from so the plan step can read \"this page\" against it. Before\n * it, the brief asks for no placeholder and says where facts come from\n * (AGL-4092).\n */\nexport function resolveAssistBuild(\n input: Record<string, unknown> | null,\n scope: { hostId: string; screen: string | null },\n): AssistBuildProposal | null {\n if (!input) return null\n const asked = typeof input['brief'] === 'string' ? assistBuildBriefWithoutGaps(input['brief'].trim()) : ''\n if (!asked) return null\n const where = scope.screen ? `\\nAsked from the console page: ${scope.screen}` : ''\n const facts = `\\n${ASSIST_BUILD_FACTS_LINE}`\n const brief = `${asked.slice(0, AI_JOB_BRIEF_MAX_CHARS - facts.length - where.length).trim()}${facts}${where}`.trim()\n const summary =\n typeof input['summary'] === 'string' && input['summary'].trim()\n ? input['summary'].trim().slice(0, ASSIST_BUILD_SUMMARY_MAX_CHARS)\n : 'Plan what you asked for'\n return {\n id: ASSIST_BUILD_ACTION_ID,\n hostId: scope.hostId,\n brief,\n publish: input['publish'] === true,\n summary,\n }\n}\n\nexport interface AssistBuildIntentsDeps {\n ops?: typeof aiBuildOps\n hasBuildRunner?: () => boolean\n}\n\n/**\n * What a build may make on this site, or `null` where none can be built: a\n * deployment that has not loaded the build kind, or a site where no\n * operation passes its owner's gates. The same operations the build's plan\n * step offers, so the chat never offers what the plan cannot make.\n */\nexport async function assistBuildIntents(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; hostId: string; org: Record<string, unknown> },\n deps: AssistBuildIntentsDeps = {},\n): Promise<string[] | null> {\n const hasBuildRunner = deps.hasBuildRunner ?? (() => aiJobStepRunnerFor('build') !== null)\n if (!hasBuildRunner()) return null\n const host = ((await firestore.collection('hosts').doc(input.hostId).get()).data() ?? null) as Record<\n string,\n unknown\n > | null\n if (!host) return null\n const org = input.org as Partial<AglynOrgBilling>\n const ops = await (deps.ops ?? aiBuildOps)({\n orgId: input.orgId,\n hostId: input.hostId,\n org,\n host,\n freeTaste: aiBuildFreeTaste(org),\n })\n const intents = aiBuildIntents(ops)\n return intents.length ? intents : null\n}\n"],"names":["ASSIST_BUILD_ACTION_ID","ASSIST_BUILD_SUMMARY_MAX_CHARS","ASSIST_BUILD_TOOL_NAME","aiBuildIntents","aiBuildOps","aiBuildFreeTaste","AI_JOB_BRIEF_MAX_CHARS","aiJobStepRunnerFor","STRING","type","assistBuildTool","name","description","inputSchema","properties","summary","brief","publish","required","additionalProperties","strict","assistBuildBlock","join","assistBuildIntentsBlock","intents","map","intent","ASSIST_BUILD_FACTS_LINE","ASKS_FOR_GAPS","assistBuildBriefWithoutGaps","split","line","sentence","test","kept","replace","filter","clause","trim","Boolean","resolveAssistBuild","input","scope","asked","where","screen","facts","slice","length","id","hostId","assistBuildIntents","firestore","deps","hasBuildRunner","host","collection","doc","get","data","org","ops","orgId","freeTaste"],"mappings":"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"}
@@ -34,6 +34,8 @@ import { composeDocsLinksAnswer, deflectToDocs, questionStandsAlone, sectionLabe
34
34
  import { ASSIST_EDIT_TOOL_NAME, assistEditDocumentOf } from "../model/assist-edit.js";
35
35
  import { ASSIST_EDIT_MAX_OUTPUT_TOKENS, assistEditTool, editCanvasBlock, editSelectionBlock, parseAssistEditContext, resolveAssistEdit } from "./assist-edit.js";
36
36
  import { ASSIST_BUILD_TOOL_NAME } from "../model/assist-build.js";
37
+ import { ASSIST_PAGE_CHANGE_TOOL_NAME } from "../model/assist-page-change.js";
38
+ import { assistPageChangeBlock, assistPageChangeTool, assistSitePagesBlock, readAssistSitePages, resolveAssistPageChange } from "./assist-page-change.js";
37
39
  import { assistBuildDraftsBlock, assistDraftsForTurn, parseAssistBuildDrafts } from "../model/assist-follow-up.js";
38
40
  import { assistBuildBlock, assistBuildIntents, assistBuildIntentsBlock, assistBuildTool, resolveAssistBuild } from "./assist-build.js";
39
41
  import { describeView, extractAssistAction, finalAssistText, resolveAssistProposal, safeOrgFacts, sanitiseId, sanitiseRoute, viewFactsBlock, viewScreenBlock, visibleAssistText, withAssistOpenDraftAction } from "./assist-view-context.js";
@@ -522,7 +524,11 @@ ${AI_ACCEPTABLE_USE_BLOCK}`;
522
524
  const hostId = sanitiseId(body.context.hostId);
523
525
  if (!document || !hostId) return null;
524
526
  if (!checkEntitlement(org, 'aiGenerative')) return null;
525
- const context = parseAssistEditContext(body.canvas);
527
+ // The chat in the Besigner sends the whole page when it fits (AGL-4116),
528
+ // held to the same limits as a job's read of it.
529
+ const context = parseAssistEditContext(body.canvas, {
530
+ whole: body.canvas.whole === true
531
+ });
526
532
  if (!context) return null;
527
533
  if (!staff && !await isServerReleaseFlagOnForOrg('release_ai_generative', body.orgId)) {
528
534
  return null;
@@ -849,7 +855,7 @@ async function handler(request) {
849
855
  });
850
856
  }
851
857
  try {
852
- var _resolved_org, _org_slug, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
858
+ var _resolved_org, _org_slug, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _assistSitePagesBlock;
853
859
  var _body_context, _body_context1, _body_context2, _body_context3, _quota_allotment, _quota_allotment1, _body_context4, _body_context5;
854
860
  const app = firebaseAdmin.app();
855
861
  const decoded = await app.auth().verifyIdToken(idToken);
@@ -971,6 +977,22 @@ async function handler(request) {
971
977
  member: resolved.member,
972
978
  firestore
973
979
  });
980
+ // A change to a page the site already has (AGL-4116): wherever a build
981
+ // may be proposed and no canvas is open (the canvas's own edit answers
982
+ // there), on every plan — the change goes into the page's draft, and a
983
+ // new version only where it is asked for and the plan keeps them. The
984
+ // edit job's door checks the page again when the panel starts it.
985
+ const pageChangeRung = buildRung && !editRung ? await readAssistSitePages(firestore, buildRung.hostId).then((pages)=>pages.length ? {
986
+ hostId: buildRung.hostId,
987
+ pages,
988
+ versioning: checkEntitlement(org, 'versioning')
989
+ } : null).catch((error)=>{
990
+ console.error('assist page change rung failed', {
991
+ orgId: body.orgId,
992
+ error
993
+ });
994
+ return null;
995
+ }) : null;
974
996
  // ── Retrieval FIRST (AGL-2486) ────────────────────────────────────────
975
997
  // Level-1 grounding for everyone. Hoisted above the reservation and the
976
998
  // model call because it is now a candidate ANSWER, not only a prompt
@@ -1360,9 +1382,11 @@ async function handler(request) {
1360
1382
  }
1361
1383
  ] : [],
1362
1384
  // The build protocol (AGL-3616): the same for every site, so cached.
1385
+ // A page change's protocol rides the same block (AGL-4116), so it
1386
+ // takes no breakpoint of its own.
1363
1387
  ...buildRung ? [
1364
1388
  {
1365
- text: assistBuildBlock(),
1389
+ text: pageChangeRung ? `${assistBuildBlock()}\n\n${assistPageChangeBlock()}` : assistBuildBlock(),
1366
1390
  cacheBreakpoint: true
1367
1391
  }
1368
1392
  ] : [],
@@ -1411,6 +1435,15 @@ async function handler(request) {
1411
1435
  volatile: true
1412
1436
  }
1413
1437
  ] : [],
1438
+ // The pages a change may name (AGL-4116): per site, so volatile.
1439
+ ...pageChangeRung ? [
1440
+ {
1441
+ text: (_assistSitePagesBlock = assistSitePagesBlock(pageChangeRung.pages, {
1442
+ versioning: pageChangeRung.versioning
1443
+ })) != null ? _assistSitePagesBlock : '',
1444
+ volatile: true
1445
+ }
1446
+ ] : [],
1414
1447
  ...docsBlock ? [
1415
1448
  {
1416
1449
  text: docsBlock,
@@ -1434,6 +1467,9 @@ async function handler(request) {
1434
1467
  ] : [],
1435
1468
  ...buildRung ? [
1436
1469
  assistBuildTool()
1470
+ ] : [],
1471
+ ...pageChangeRung ? [
1472
+ assistPageChangeTool()
1437
1473
  ] : []
1438
1474
  ]
1439
1475
  } : {}, CHAT_ROUTE.thinking ? {
@@ -1478,6 +1514,7 @@ async function handler(request) {
1478
1514
  let stopReason = null;
1479
1515
  /** The first call of the edit tool, as the stream delivered it. */ let editInput = null;
1480
1516
  /** The first call of the build tool (AGL-3616). */ let buildInput = null;
1517
+ /** The first call of the page change tool (AGL-4116). */ let pageChangeInput = null;
1481
1518
  /**
1482
1519
  * What the model wrote into its tool calls, when the stream stopped
1483
1520
  * at its ceiling (AGL-3143). Held here only long enough to be
@@ -1531,6 +1568,9 @@ async function handler(request) {
1531
1568
  if (buildRung && !buildInput && event.name === ASSIST_BUILD_TOOL_NAME) {
1532
1569
  buildInput = event.input;
1533
1570
  }
1571
+ if (pageChangeRung && !pageChangeInput && event.name === ASSIST_PAGE_CHANGE_TOOL_NAME) {
1572
+ pageChangeInput = event.input;
1573
+ }
1534
1574
  } else if (event.type === 'done') {
1535
1575
  var _event_rawOutput;
1536
1576
  usage = event.usage;
@@ -1598,6 +1638,16 @@ async function handler(request) {
1598
1638
  hostId: buildRung.hostId,
1599
1639
  screen: (_ref = (_ref1 = guide == null ? void 0 : (_guide_view = guide.view) == null ? void 0 : _guide_view.screen) != null ? _ref1 : (_describeView = describeView(sanitiseRoute((_ref2 = (_body_context = body.context) == null ? void 0 : _body_context.route) != null ? _ref2 : ''))) == null ? void 0 : _describeView.screen) != null ? _ref : null
1600
1640
  }) : null;
1641
+ // The page change (AGL-4116), held to the site's own pages. Still
1642
+ // nothing is changed: the panel starts the edit job, whose draft
1643
+ // the person opens in the Besigner and publishes.
1644
+ const pageChange = pageChangeRung && pageChangeInput && stopReason !== 'max_tokens' ? resolveAssistPageChange(pageChangeInput, pageChangeRung) : null;
1645
+ if (pageChangeRung && pageChangeInput && !pageChange && stopReason !== 'max_tokens') {
1646
+ emit({
1647
+ type: 'error',
1648
+ error: 'That page could not be found on this site, so nothing was changed. Say which page and ask again.'
1649
+ });
1650
+ }
1601
1651
  // A refusal is an HTTP 200 with an empty or partial answer, not an
1602
1652
  // error — so without this the user watches the spinner stop and
1603
1653
  // gets nothing back. Truncation is the same shape: the tokens are
@@ -1698,6 +1748,9 @@ async function handler(request) {
1698
1748
  // The build proposal (AGL-3616): the panel starts its job and
1699
1749
  // shows the plan card; nothing is built before it is confirmed.
1700
1750
  build,
1751
+ // The page change (AGL-4116): the panel starts its edit job and
1752
+ // links the draft it makes in the Besigner.
1753
+ pageChange,
1701
1754
  // The reservation VERBATIM (AGL-2238). It already describes the
1702
1755
  // standing after the message, because it is what moved the
1703
1756
  // counter — `reserveAssistMessage` returns `used + 1` and