@aglyn/plugins-ai 1.0.0-beta.221 → 1.0.0-beta.222

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 (72) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-plan.component.js +1 -4
  3. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  4. package/src/lib/jobs/ai-job-crm-step.d.ts +44 -3
  5. package/src/lib/jobs/ai-job-crm-step.js +298 -8
  6. package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
  7. package/src/lib/jobs/ai-job-page-sections.d.ts +33 -1
  8. package/src/lib/jobs/ai-job-page-sections.js +55 -1
  9. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  10. package/src/lib/jobs/ai-job-page-step.js +20 -11
  11. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  12. package/src/lib/jobs/ai-job-plan-step.js +6 -2
  13. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  14. package/src/lib/jobs/ai-job-seo-step.js +52 -15
  15. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  16. package/src/lib/jobs/ai-job-site-step.js +6 -0
  17. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  18. package/src/lib/jobs/ai-job-workflow-step.js +8 -7
  19. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  20. package/src/lib/jobs/ai-workflow-records.d.ts +2 -2
  21. package/src/lib/jobs/ai-workflow-records.js +2 -2
  22. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  23. package/src/lib/jobs/fixtures/ai-free-page-built-recording.js +1 -0
  24. package/src/lib/jobs/fixtures/ai-free-page-built-recording.js.map +1 -1
  25. package/src/lib/jobs/fixtures/ai-free-page-recording.js +2 -0
  26. package/src/lib/jobs/fixtures/ai-free-page-recording.js.map +1 -1
  27. package/src/lib/jobs/fixtures/ai-page-briefs.js +2 -0
  28. package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
  29. package/src/lib/model/ai-automation-draft.d.ts +4 -4
  30. package/src/lib/model/ai-automation-draft.js +15 -10
  31. package/src/lib/model/ai-automation-draft.js.map +1 -1
  32. package/src/lib/model/ai-automation-format.d.ts +139 -0
  33. package/src/lib/model/ai-automation-format.js +42 -0
  34. package/src/lib/model/ai-automation-format.js.map +1 -0
  35. package/src/lib/model/ai-automation-outline.d.ts +4 -4
  36. package/src/lib/model/ai-automation-outline.js +10 -8
  37. package/src/lib/model/ai-automation-outline.js.map +1 -1
  38. package/src/lib/model/ai-build-plan.d.ts +39 -2
  39. package/src/lib/model/ai-build-plan.js +80 -3
  40. package/src/lib/model/ai-build-plan.js.map +1 -1
  41. package/src/lib/model/ai-site-inventory.d.ts +5 -0
  42. package/src/lib/model/ai-site-inventory.js.map +1 -1
  43. package/src/lib/model/ai-workflow-job.js +2 -2
  44. package/src/lib/model/ai-workflow-job.js.map +1 -1
  45. package/src/lib/providers/catalog.d.ts +23 -0
  46. package/src/lib/providers/catalog.js +29 -1
  47. package/src/lib/providers/catalog.js.map +1 -1
  48. package/src/lib/runtime/ai-doctrine-validators.d.ts +8 -0
  49. package/src/lib/runtime/ai-doctrine-validators.js +34 -3
  50. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  51. package/src/lib/runtime/ai-doctrine.js +1 -1
  52. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  53. package/src/lib/runtime/ai-eval.d.ts +2 -2
  54. package/src/lib/runtime/ai-eval.js +3 -3
  55. package/src/lib/runtime/ai-eval.js.map +1 -1
  56. package/src/lib/runtime/ai-palette.generated.js +8 -1
  57. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  58. package/src/lib/runtime/seo-audit.d.ts +5 -2
  59. package/src/lib/runtime/seo-audit.js +16 -7
  60. package/src/lib/runtime/seo-audit.js.map +1 -1
  61. package/src/lib/runtime/site-inventory.js +10 -3
  62. package/src/lib/runtime/site-inventory.js.map +1 -1
  63. package/src/lib/testing/stand-in-dataset-index.js +31 -5
  64. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  65. package/src/lib/tools/ai-seo-tool.d.ts +5 -1
  66. package/src/lib/tools/ai-seo-tool.js +6 -2
  67. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  68. package/src/lib/tools/ai-workflow-tool.d.ts +15 -4
  69. package/src/lib/tools/ai-workflow-tool.js +70 -12
  70. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  71. package/src/lib/usage-axes.js +14 -1
  72. package/src/lib/usage-axes.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-ai",
3
- "version": "1.0.0-beta.221",
3
+ "version": "1.0.0-beta.222",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.221",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.221",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.221",
31
- "@aglyn/shared-data-types": "1.0.0-beta.221",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.221",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.221",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.221",
35
- "@aglyn/shared-ui-theme": "1.0.0-beta.221",
36
- "@aglyn/shared-util-email": "1.0.0-beta.221",
37
- "@aglyn/shared-util-errors": "1.0.0-beta.221",
38
- "@aglyn/shared-util-http": "1.0.0-beta.221",
39
- "@aglyn/shared-util-timestamp": "1.0.0-beta.221",
40
- "@aglyn/shared-util-tools": "1.0.0-beta.221",
41
- "@aglyn/tenant-data-admin": "1.0.0-beta.221",
42
- "@aglyn/tenant-feature-instance": "1.0.0-beta.221",
43
- "@aglyn/tenant-runtime": "1.0.0-beta.221",
28
+ "@aglyn/aglyn": "1.0.0-beta.222",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.222",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.222",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.222",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.222",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.222",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.222",
35
+ "@aglyn/shared-ui-theme": "1.0.0-beta.222",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.222",
37
+ "@aglyn/shared-util-errors": "1.0.0-beta.222",
38
+ "@aglyn/shared-util-http": "1.0.0-beta.222",
39
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.222",
40
+ "@aglyn/shared-util-tools": "1.0.0-beta.222",
41
+ "@aglyn/tenant-data-admin": "1.0.0-beta.222",
42
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.222",
43
+ "@aglyn/tenant-runtime": "1.0.0-beta.222",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -125,10 +125,7 @@ export function AiJobPlan({ job, onResume, busy = false, staff = false }) {
125
125
  variant: "body2",
126
126
  role: "listitem",
127
127
  children: [
128
- "Builds the page ",
129
- screen.title,
130
- " at ",
131
- screen.slug,
128
+ screen.record ? `Builds the record template ${screen.title}, a page per record of ${named(screen.record.dataset)} at /${screen.record.base}/…` : `Builds the page ${screen.title} at ${screen.slug}`,
132
129
  screen.duplicateOf ? `, from a copy of ${named(screen.duplicateOf)}` : '',
133
130
  screen.layout ? ` in ${named(screen.layout)}` : '',
134
131
  screen.sections.length ? `: ${screen.sections.map((section)=>section.name).join(', ')}` : ''
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-plan.component.tsx"],"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 {\n AI_BUILD_PLAN_CREATION_NOUNS,\n AI_BUILD_PLAN_EMBED_HOST_NAMES,\n isAiPlanNewRef,\n type AiBuildPlanEmbed,\n} from '../model/ai-build-plan'\nimport type { AiJobReview, AiJobSummary } from '../model/ai-jobs.types'\nimport { aiJobPlanCreditEstimate } from '../model/ai-site-job'\nimport { Box, Button, Collapse, Stack, Typography } from '@mui/material'\nimport { useState } from 'react'\n\n/**\n * A job's plan and what it waits for (AGL-2935), inside the AI jobs drawer:\n * what the job reuses from the site, what it creates and why, the screens it\n * builds — and, while the job waits for a person, the one button that\n * confirms the plan or tries a refused step again. The drawer owns the\n * request; this renders.\n */\nexport interface AiJobPlanProps {\n job: AiJobSummary\n /** Confirms the plan, or tries the refused step again. */\n onResume: (job: AiJobSummary) => void\n /** A resume for this job is in flight. */\n busy?: boolean\n /**\n * The viewer is staff (AGL-3078), who can open where a refused answer broke\n * its rules and the outline of those parts, to tell a shape the rules should\n * allow from an answer that ignored its re-ask. A member reads the findings.\n */\n staff?: boolean\n}\n\n/**\n * Where a refused answer broke its rules, and the outline of those parts, as\n * lines staff read (AGL-3078): each finding with the nodes or plan entries it\n * names, then each outlined node indented by its depth, with its element, a\n * Grid's layout as written, and the names of what it sets and holds.\n */\nexport function aiJobReviewDetails(review: AiJobReview | null): string[] {\n if (review?.reason !== 'doctrine') return []\n const lines = review.findings.flatMap((finding) => {\n const where = finding.nodeIds?.length\n ? `nodes ${finding.nodeIds.join(', ')}`\n : finding.paths?.length\n ? `at ${finding.paths.join(', ')}`\n : null\n return where ? [`${finding.rule === null ? '' : `Rule ${finding.rule} `}${finding.code}: ${where}`] : []\n })\n for (const node of review.outline ?? []) {\n lines.push(\n [\n `${' '.repeat(node.depth)}${node.id} ${node.componentId}`,\n ...Object.entries(node.grid ?? {}).map(([name, value]) => `${name}=${JSON.stringify(value)}`),\n ...(node.props.length ? [`props ${node.props.join(', ')}`] : []),\n ...(node.sx?.length ? [`sx ${node.sx.join(', ')}`] : []),\n ...(node.children.length ? [`holds ${node.children.join(', ')}`] : []),\n ].join(' · '),\n )\n }\n return lines\n}\n\n/**\n * A third-party player the plan lists, with what it costs the page, read\n * before the member confirms it (rule 16, AGL-3433). The cost is the\n * platform's sentence, never the model's: the player is framed lazily, so its\n * host's code loads when a visitor reaches it, whether or not they press play.\n */\nexport function aiPlanEmbedLine(\n embed: AiBuildPlanEmbed,\n named: (ref: string) => string,\n): string {\n const host = AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host]\n const where = isAiPlanNewRef(embed.where) ? `the component ${named(embed.where)}` : embed.where\n const link = embed.url ? ` playing ${embed.url}` : ', its link left for you to paste'\n return `Embeds a ${host} player on ${where}${link}, as you asked (“${embed.asked}”). It loads ${host}’s own code when a visitor reaches it, whether or not they press play.`\n}\n\nexport function AiJobPlan({\n job,\n onResume,\n busy = false,\n staff = false,\n}: AiJobPlanProps): JSX.Element | null {\n const [detailsOpen, setDetailsOpen] = useState(false)\n const { plan, review } = job\n if (!plan && !review) return null\n const details = staff ? aiJobReviewDetails(review) : []\n /** A reference as a person reads it: the record's name, or what the plan creates. */\n const named = (ref: string | null): string =>\n !ref\n ? ''\n : isAiPlanNewRef(ref)\n ? ref.slice('new:'.length)\n : (plan?.labels[ref] ?? ref)\n const waiting = job.status === 'needs_review' && review !== null\n // The guard rail (AGL-2911): what the plan is estimated to cost is read\n // before it is confirmed, not after it has been spent. An estimate, and\n // said to be one — the plan's own passes at the nominal credits a step\n // holds, where what a step really costs is its model's tokens. A page job\n // counts the creations it builds before its page (AGL-3031).\n const estimate =\n plan && waiting && review.reason === 'plan' ? aiJobPlanCreditEstimate(job.kind, plan) : 0\n return (\n <Box sx={{ mt: 1 }}>\n {plan && (\n <Stack spacing={0.5} role=\"list\" aria-label=\"Plan\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {plan.status === 'confirmed' ? 'Confirmed plan' : 'Proposed plan'}\n </Typography>\n {plan.reuse.map((entry, index) => (\n <Typography key={`reuse-${index}`} variant=\"body2\" role=\"listitem\">\n Reuses the {entry.kind === 'screen' ? 'page' : entry.kind} {named(entry.id)} — {entry.purpose}\n </Typography>\n ))}\n {plan.create.map((entry, index) => (\n <Typography key={`create-${index}`} variant=\"body2\" role=\"listitem\">\n Creates the {AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun} {entry.name}\n {entry.duplicateOf\n ? `, from a copy of ${named(entry.duplicateOf)}`\n : ''}{' '}\n — {entry.why}\n </Typography>\n ))}\n {plan.screens.map((screen, index) => (\n <Typography key={`screen-${index}`} variant=\"body2\" role=\"listitem\">\n Builds the page {screen.title} at {screen.slug}\n {screen.duplicateOf\n ? `, from a copy of ${named(screen.duplicateOf)}`\n : ''}\n {screen.layout ? ` in ${named(screen.layout)}` : ''}\n {screen.sections.length\n ? `: ${screen.sections.map((section) => section.name).join(', ')}`\n : ''}\n </Typography>\n ))}\n {(plan.embeds ?? []).map((embed, index) => (\n <Typography key={`embed-${index}`} variant=\"body2\" role=\"listitem\">\n {aiPlanEmbedLine(embed, named)}\n </Typography>\n ))}\n </Stack>\n )}\n {review?.reason === 'doctrine' && review.findings.length > 0 && (\n <Box\n component=\"ul\"\n sx={{ my: 0.5, pl: 2.5 }}\n aria-label=\"Building rules broken\"\n >\n {review.findings.map((finding, index) => (\n <Typography\n key={`finding-${index}`}\n component=\"li\"\n variant=\"caption\"\n >\n {finding.message}\n </Typography>\n ))}\n </Box>\n )}\n {details.length > 0 && (\n <Box sx={{ mt: 0.5 }}>\n <Button\n size=\"small\"\n aria-expanded={detailsOpen}\n onClick={() => setDetailsOpen((prior) => !prior)}\n >\n {detailsOpen ? 'Hide what was refused' : 'Show what was refused'}\n </Button>\n <Collapse in={detailsOpen} unmountOnExit>\n <Typography\n variant=\"caption\"\n component=\"pre\"\n aria-label=\"What was refused\"\n sx={{ m: 0, fontFamily: 'monospace', whiteSpace: 'pre', overflowX: 'auto' }}\n >\n {details.join('\\n')}\n </Typography>\n </Collapse>\n </Box>\n )}\n {estimate > 0 && (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ display: 'block', mt: 1 }}\n >\n {`Estimated cost: about ${estimate.toLocaleString('en-US')} credits. What it costs is what its steps spend.`}\n </Typography>\n )}\n {waiting && (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={busy}\n onClick={() => onResume(job)}\n sx={{ mt: 1 }}\n >\n {review.reason === 'plan' ? 'Confirm plan' : 'Try again'}\n </Button>\n )}\n </Box>\n )\n}\n\nexport default AiJobPlan\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","AI_BUILD_PLAN_EMBED_HOST_NAMES","isAiPlanNewRef","aiJobPlanCreditEstimate","Box","Button","Collapse","Stack","Typography","useState","aiJobReviewDetails","review","reason","lines","findings","flatMap","finding","where","nodeIds","length","join","paths","rule","code","node","outline","push","repeat","depth","id","componentId","Object","entries","grid","map","name","value","JSON","stringify","props","sx","children","aiPlanEmbedLine","embed","named","host","link","url","asked","AiJobPlan","job","onResume","busy","staff","plan","detailsOpen","setDetailsOpen","details","ref","slice","labels","waiting","status","estimate","kind","mt","spacing","role","aria-label","variant","color","reuse","entry","index","purpose","create","noun","duplicateOf","why","screens","screen","title","slug","layout","sections","section","embeds","component","my","pl","message","size","aria-expanded","onClick","prior","in","unmountOnExit","m","fontFamily","whiteSpace","overflowX","display","toLocaleString","disabled"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-plan.component.tsx"],"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 {\n AI_BUILD_PLAN_CREATION_NOUNS,\n AI_BUILD_PLAN_EMBED_HOST_NAMES,\n isAiPlanNewRef,\n type AiBuildPlanEmbed,\n} from '../model/ai-build-plan'\nimport type { AiJobReview, AiJobSummary } from '../model/ai-jobs.types'\nimport { aiJobPlanCreditEstimate } from '../model/ai-site-job'\nimport { Box, Button, Collapse, Stack, Typography } from '@mui/material'\nimport { useState } from 'react'\n\n/**\n * A job's plan and what it waits for (AGL-2935), inside the AI jobs drawer:\n * what the job reuses from the site, what it creates and why, the screens it\n * builds — and, while the job waits for a person, the one button that\n * confirms the plan or tries a refused step again. The drawer owns the\n * request; this renders.\n */\nexport interface AiJobPlanProps {\n job: AiJobSummary\n /** Confirms the plan, or tries the refused step again. */\n onResume: (job: AiJobSummary) => void\n /** A resume for this job is in flight. */\n busy?: boolean\n /**\n * The viewer is staff (AGL-3078), who can open where a refused answer broke\n * its rules and the outline of those parts, to tell a shape the rules should\n * allow from an answer that ignored its re-ask. A member reads the findings.\n */\n staff?: boolean\n}\n\n/**\n * Where a refused answer broke its rules, and the outline of those parts, as\n * lines staff read (AGL-3078): each finding with the nodes or plan entries it\n * names, then each outlined node indented by its depth, with its element, a\n * Grid's layout as written, and the names of what it sets and holds.\n */\nexport function aiJobReviewDetails(review: AiJobReview | null): string[] {\n if (review?.reason !== 'doctrine') return []\n const lines = review.findings.flatMap((finding) => {\n const where = finding.nodeIds?.length\n ? `nodes ${finding.nodeIds.join(', ')}`\n : finding.paths?.length\n ? `at ${finding.paths.join(', ')}`\n : null\n return where ? [`${finding.rule === null ? '' : `Rule ${finding.rule} `}${finding.code}: ${where}`] : []\n })\n for (const node of review.outline ?? []) {\n lines.push(\n [\n `${' '.repeat(node.depth)}${node.id} ${node.componentId}`,\n ...Object.entries(node.grid ?? {}).map(([name, value]) => `${name}=${JSON.stringify(value)}`),\n ...(node.props.length ? [`props ${node.props.join(', ')}`] : []),\n ...(node.sx?.length ? [`sx ${node.sx.join(', ')}`] : []),\n ...(node.children.length ? [`holds ${node.children.join(', ')}`] : []),\n ].join(' · '),\n )\n }\n return lines\n}\n\n/**\n * A third-party player the plan lists, with what it costs the page, read\n * before the member confirms it (rule 16, AGL-3433). The cost is the\n * platform's sentence, never the model's: the player is framed lazily, so its\n * host's code loads when a visitor reaches it, whether or not they press play.\n */\nexport function aiPlanEmbedLine(\n embed: AiBuildPlanEmbed,\n named: (ref: string) => string,\n): string {\n const host = AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host]\n const where = isAiPlanNewRef(embed.where) ? `the component ${named(embed.where)}` : embed.where\n const link = embed.url ? ` playing ${embed.url}` : ', its link left for you to paste'\n return `Embeds a ${host} player on ${where}${link}, as you asked (“${embed.asked}”). It loads ${host}’s own code when a visitor reaches it, whether or not they press play.`\n}\n\nexport function AiJobPlan({\n job,\n onResume,\n busy = false,\n staff = false,\n}: AiJobPlanProps): JSX.Element | null {\n const [detailsOpen, setDetailsOpen] = useState(false)\n const { plan, review } = job\n if (!plan && !review) return null\n const details = staff ? aiJobReviewDetails(review) : []\n /** A reference as a person reads it: the record's name, or what the plan creates. */\n const named = (ref: string | null): string =>\n !ref\n ? ''\n : isAiPlanNewRef(ref)\n ? ref.slice('new:'.length)\n : (plan?.labels[ref] ?? ref)\n const waiting = job.status === 'needs_review' && review !== null\n // The guard rail (AGL-2911): what the plan is estimated to cost is read\n // before it is confirmed, not after it has been spent. An estimate, and\n // said to be one — the plan's own passes at the nominal credits a step\n // holds, where what a step really costs is its model's tokens. A page job\n // counts the creations it builds before its page (AGL-3031).\n const estimate =\n plan && waiting && review.reason === 'plan' ? aiJobPlanCreditEstimate(job.kind, plan) : 0\n return (\n <Box sx={{ mt: 1 }}>\n {plan && (\n <Stack spacing={0.5} role=\"list\" aria-label=\"Plan\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {plan.status === 'confirmed' ? 'Confirmed plan' : 'Proposed plan'}\n </Typography>\n {plan.reuse.map((entry, index) => (\n <Typography key={`reuse-${index}`} variant=\"body2\" role=\"listitem\">\n Reuses the {entry.kind === 'screen' ? 'page' : entry.kind} {named(entry.id)} — {entry.purpose}\n </Typography>\n ))}\n {plan.create.map((entry, index) => (\n <Typography key={`create-${index}`} variant=\"body2\" role=\"listitem\">\n Creates the {AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun} {entry.name}\n {entry.duplicateOf\n ? `, from a copy of ${named(entry.duplicateOf)}`\n : ''}{' '}\n — {entry.why}\n </Typography>\n ))}\n {plan.screens.map((screen, index) => (\n <Typography key={`screen-${index}`} variant=\"body2\" role=\"listitem\">\n {screen.record\n ? `Builds the record template ${screen.title}, a page per record of ${named(screen.record.dataset)} at /${screen.record.base}/…`\n : `Builds the page ${screen.title} at ${screen.slug}`}\n {screen.duplicateOf\n ? `, from a copy of ${named(screen.duplicateOf)}`\n : ''}\n {screen.layout ? ` in ${named(screen.layout)}` : ''}\n {screen.sections.length\n ? `: ${screen.sections.map((section) => section.name).join(', ')}`\n : ''}\n </Typography>\n ))}\n {(plan.embeds ?? []).map((embed, index) => (\n <Typography key={`embed-${index}`} variant=\"body2\" role=\"listitem\">\n {aiPlanEmbedLine(embed, named)}\n </Typography>\n ))}\n </Stack>\n )}\n {review?.reason === 'doctrine' && review.findings.length > 0 && (\n <Box\n component=\"ul\"\n sx={{ my: 0.5, pl: 2.5 }}\n aria-label=\"Building rules broken\"\n >\n {review.findings.map((finding, index) => (\n <Typography\n key={`finding-${index}`}\n component=\"li\"\n variant=\"caption\"\n >\n {finding.message}\n </Typography>\n ))}\n </Box>\n )}\n {details.length > 0 && (\n <Box sx={{ mt: 0.5 }}>\n <Button\n size=\"small\"\n aria-expanded={detailsOpen}\n onClick={() => setDetailsOpen((prior) => !prior)}\n >\n {detailsOpen ? 'Hide what was refused' : 'Show what was refused'}\n </Button>\n <Collapse in={detailsOpen} unmountOnExit>\n <Typography\n variant=\"caption\"\n component=\"pre\"\n aria-label=\"What was refused\"\n sx={{ m: 0, fontFamily: 'monospace', whiteSpace: 'pre', overflowX: 'auto' }}\n >\n {details.join('\\n')}\n </Typography>\n </Collapse>\n </Box>\n )}\n {estimate > 0 && (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ display: 'block', mt: 1 }}\n >\n {`Estimated cost: about ${estimate.toLocaleString('en-US')} credits. What it costs is what its steps spend.`}\n </Typography>\n )}\n {waiting && (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={busy}\n onClick={() => onResume(job)}\n sx={{ mt: 1 }}\n >\n {review.reason === 'plan' ? 'Confirm plan' : 'Try again'}\n </Button>\n )}\n </Box>\n )\n}\n\nexport default AiJobPlan\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","AI_BUILD_PLAN_EMBED_HOST_NAMES","isAiPlanNewRef","aiJobPlanCreditEstimate","Box","Button","Collapse","Stack","Typography","useState","aiJobReviewDetails","review","reason","lines","findings","flatMap","finding","where","nodeIds","length","join","paths","rule","code","node","outline","push","repeat","depth","id","componentId","Object","entries","grid","map","name","value","JSON","stringify","props","sx","children","aiPlanEmbedLine","embed","named","host","link","url","asked","AiJobPlan","job","onResume","busy","staff","plan","detailsOpen","setDetailsOpen","details","ref","slice","labels","waiting","status","estimate","kind","mt","spacing","role","aria-label","variant","color","reuse","entry","index","purpose","create","noun","duplicateOf","why","screens","screen","record","title","dataset","base","slug","layout","sections","section","embeds","component","my","pl","message","size","aria-expanded","onClick","prior","in","unmountOnExit","m","fontFamily","whiteSpace","overflowX","display","toLocaleString","disabled"],"mappings":"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"}
@@ -104,11 +104,52 @@ export declare function aiCrmRecordInstructions(kind: AiCrmRecordKind): AiSystem
104
104
  export declare const AI_CRM_EMAIL_INSTRUCTIONS: AiSystemBlock[];
105
105
  export declare const AI_CRM_MAPPING_INSTRUCTIONS: AiSystemBlock[];
106
106
  /**
107
- * The facts as lines of text. Only the fields named here are written, whatever
108
- * else a reader reports, so what reaches a prompt is decided in two places
109
- * that both have to agree: the CRM's builders and this list.
107
+ * A record's facts as the CRM's reader marks them when it answered the whole
108
+ * record (AGL-3520): `wholeRecord: true`, set only while the organization has
109
+ * `release_crm_assist_whole_record` on. Absent, the facts are the disclosed
110
+ * ones and are written by {@link aiCrmDisclosedFactsLines}.
111
+ */
112
+ export declare const AI_CRM_WHOLE_RECORD_FIELD = "wholeRecord";
113
+ /**
114
+ * The facts as lines of text while the published pages describe the narrow
115
+ * set: only the fields named here are written, whatever else a reader
116
+ * reports, so what reaches a prompt is decided in two places that both have
117
+ * to agree — the CRM's disclosed builders and this list. A company's account
118
+ * fields beyond its headcount, and a lead's profile fields, stay out of the
119
+ * prompt as they always have.
120
+ */
121
+ export declare function aiCrmDisclosedFactsLines(kind: AiCrmRecordKind, facts: Facts): string[];
122
+ /**
123
+ * The facts as lines of text: the WHOLE record the CRM reports (AGL-3520) —
124
+ * every standard field, its contact details and addresses, its picklist
125
+ * labels, its custom fields under their labels, its consent, notes,
126
+ * timeline, open tasks and deals. A field the record does not hold writes
127
+ * no line. The CRM's builders decide what a record is; nothing here drops a
128
+ * field they report, and no reader reports a token, an account identifier
129
+ * or a record id beyond a stage's.
110
130
  */
111
131
  export declare function aiCrmFactsLines(kind: AiCrmRecordKind, facts: Facts): string[];
132
+ /**
133
+ * The most characters a record's facts take in a request (AGL-3520) —
134
+ * about six thousand tokens at the three characters a token the routing
135
+ * ceilings are measured with. The CRM's own cuts keep a record well under
136
+ * it; a record that holds everything at once is fitted by
137
+ * {@link aiCrmFitFacts}, which shortens its long texts and drops no field.
138
+ */
139
+ export declare const AI_CRM_FACTS_MAX_CHARS = 18000;
140
+ /**
141
+ * The facts, fitted to {@link AI_CRM_FACTS_MAX_CHARS} as lines: unchanged
142
+ * when they fit, otherwise with the long texts — the notes, each timeline
143
+ * entry's text, each task's notes and each custom value — cut shorter step
144
+ * by step. Every field stays; only how much of a long text is sent changes.
145
+ */
146
+ export declare function aiCrmFitFacts(kind: AiCrmRecordKind, facts: Facts): Facts;
147
+ /**
148
+ * The lines a prompt carries for a record: the whole record, fitted, when
149
+ * the reader marked it {@link AI_CRM_WHOLE_RECORD_FIELD}; otherwise the
150
+ * disclosed lines alone.
151
+ */
152
+ export declare function aiCrmPromptFactsLines(kind: AiCrmRecordKind, facts: Facts): string[];
112
153
  /** The user turn for a record. */
113
154
  export declare function aiCrmRecordPrompt(kind: AiCrmRecordKind, facts: Facts): string;
114
155
  /** The user turn for an email draft. */
@@ -170,6 +170,8 @@ function timelineLines(value) {
170
170
  ...entries.map((entry)=>{
171
171
  const detail = [
172
172
  text(entry['direction']),
173
+ entry['from'] ? `from ${text(entry['from'])}` : '',
174
+ entry['to'] ? `to ${text(entry['to'])}` : '',
173
175
  text(entry['delivery']),
174
176
  text(entry['outcome'])
175
177
  ].filter(Boolean);
@@ -188,11 +190,17 @@ function taskLines(value) {
188
190
  ...tasks.map((task)=>`- ${text(task['title'])} (${[
189
191
  text(task['kind']),
190
192
  `${text(task['priority']) || 'normal'} priority`,
193
+ ...task['status'] ? [
194
+ text(task['status'])
195
+ ] : [],
191
196
  task['due'] ? `due ${text(task['due'])}` : 'no due date',
192
197
  ...task['overdue'] === true ? [
193
198
  'overdue'
199
+ ] : [],
200
+ ...task['assignee'] ? [
201
+ `assigned to ${text(task['assignee'])}`
194
202
  ] : []
195
- ].join(', ')})`)
203
+ ].join(', ')})` + (task['notes'] ? `: ${text(task['notes'])}` : ''))
196
204
  ];
197
205
  }
198
206
  function dealLines(value) {
@@ -210,18 +218,184 @@ function dealLines(value) {
210
218
  ].filter(Boolean).join(', ')}`)
211
219
  ];
212
220
  }
221
+ /** The record's custom field values under their labels (AGL-3520). */ function customLines(value) {
222
+ const fields = list(value).filter((field)=>text(field['label']) && text(field['value']));
223
+ return fields.length ? [
224
+ 'Custom fields:',
225
+ ...fields.map((field)=>`- ${text(field['label'])}: ${text(field['value'])}`)
226
+ ] : [];
227
+ }
228
+ const yesNo = (label, value)=>value === true ? [
229
+ label
230
+ ] : [];
231
+ /**
232
+ * A record's facts as the CRM's reader marks them when it answered the whole
233
+ * record (AGL-3520): `wholeRecord: true`, set only while the organization has
234
+ * `release_crm_assist_whole_record` on. Absent, the facts are the disclosed
235
+ * ones and are written by {@link aiCrmDisclosedFactsLines}.
236
+ */ export const AI_CRM_WHOLE_RECORD_FIELD = 'wholeRecord';
237
+ /** Keeps only `keys` of each entry: the disclosed writer's allow-list for nested rows. */ const only = (value, keys)=>list(value).map((entry)=>Object.fromEntries(keys.filter((key)=>key in entry).map((key)=>[
238
+ key,
239
+ entry[key]
240
+ ])));
241
+ /** What the disclosed writer reads of a timeline entry: never who an email was from or to. */ const DISCLOSED_TIMELINE_KEYS = [
242
+ 'on',
243
+ 'kind',
244
+ 'direction',
245
+ 'subject',
246
+ 'text',
247
+ 'outcome',
248
+ 'delivery'
249
+ ];
250
+ /** What the disclosed writer reads of an open task: never its assignee, status or notes. */ const DISCLOSED_TASK_KEYS = [
251
+ 'title',
252
+ 'kind',
253
+ 'priority',
254
+ 'due',
255
+ 'overdue'
256
+ ];
257
+ /**
258
+ * The facts as lines of text while the published pages describe the narrow
259
+ * set: only the fields named here are written, whatever else a reader
260
+ * reports, so what reaches a prompt is decided in two places that both have
261
+ * to agree — the CRM's disclosed builders and this list. A company's account
262
+ * fields beyond its headcount, and a lead's profile fields, stay out of the
263
+ * prompt as they always have.
264
+ */ export function aiCrmDisclosedFactsLines(kind, facts) {
265
+ const timeline = timelineLines(only(facts['timeline'], DISCLOSED_TIMELINE_KEYS));
266
+ const openTasks = taskLines(only(facts['openTasks'], DISCLOSED_TASK_KEYS));
267
+ if (kind === 'contact') {
268
+ const orders = Number(facts['orders']) || 0;
269
+ return [
270
+ ...line('Contact', facts['name']),
271
+ ...line('Salutation', facts['salutation']),
272
+ ...line('Job title', facts['jobTitle']),
273
+ ...line('Department', facts['department']),
274
+ // A request the person made, which a drafted next step must respect (AGL-3515).
275
+ ...yesNo('Do not call: they asked not to be phoned', facts['doNotCall']),
276
+ ...line('Company', facts['company']),
277
+ ...line('Lifecycle stage', facts['lifecycleStage']),
278
+ ...line('Tags', words(facts['tags']).join(', ')),
279
+ ...line('Came in through', words(facts['sources']).join(', ')),
280
+ ...orders ? [
281
+ `Orders: ${orders}${facts['lastPurchase'] ? `, the last on ${text(facts['lastPurchase'])}` : ''}`
282
+ ] : [],
283
+ ...line('In the CRM since', facts['since']),
284
+ ...line('Last opened or clicked an email', facts['lastEmailEngagement']),
285
+ ...line('Notes', facts['notes']),
286
+ ...timeline,
287
+ ...openTasks,
288
+ ...dealLines(facts['deals'])
289
+ ];
290
+ }
291
+ if (kind === 'company') {
292
+ var _facts_employees;
293
+ return [
294
+ ...line('Company', facts['name']),
295
+ ...line('Domain', facts['domain']),
296
+ ...line('Industry', facts['industry']),
297
+ ...line('Employees', (_facts_employees = facts['employees']) != null ? _facts_employees : ''),
298
+ ...line('Tags', words(facts['tags']).join(', ')),
299
+ ...line('People in the CRM', facts['people']),
300
+ ...line('In the CRM since', facts['since']),
301
+ ...line('Notes', facts['notes']),
302
+ ...timeline,
303
+ ...openTasks,
304
+ ...dealLines(facts['deals'])
305
+ ];
306
+ }
307
+ if (kind === 'deal') {
308
+ const stages = list(facts['stages']);
309
+ const withWhom = [
310
+ text(facts['contact']),
311
+ text(facts['company'])
312
+ ].filter(Boolean).join(' at ');
313
+ return [
314
+ ...line('Deal', facts['title']),
315
+ ...line('Pipeline', facts['pipeline']),
316
+ ...line('Status', facts['status']),
317
+ ...line('Stage', facts['stage'] ? `${text(facts['stage'])} (id ${text(facts['stageId'])})` : ''),
318
+ ...line('Stages in order', stages.map((stage)=>`${text(stage['id'])} "${text(stage['name'])}" (${text(stage['kind'])})`).join('; ')),
319
+ ...line('In this stage since', facts['inStageSince']),
320
+ ...line('Amount', facts['amount']),
321
+ // Salesforce's Opportunity fields (AGL-3516).
322
+ ...line('Probability', typeof facts['probability'] === 'number' ? `${facts['probability']}%` : ''),
323
+ ...line('Forecast category', facts['forecastCategory']),
324
+ ...line('Type', facts['type']),
325
+ ...line('Lead source', facts['leadSource']),
326
+ ...line('Next step', facts['nextStep']),
327
+ ...line('Expected to close', facts['expectedClose']),
328
+ ...line('Lost reason', facts['lostReason']),
329
+ ...line('With', withWhom),
330
+ ...contactRoleLines(facts['contactRoles']),
331
+ ...line('Products on the deal', typeof facts['products'] === 'number' ? facts['products'] || '' : ''),
332
+ ...line('In the CRM since', facts['since']),
333
+ ...line('Notes', facts['notes']),
334
+ ...timeline,
335
+ ...openTasks
336
+ ];
337
+ }
338
+ return [
339
+ ...line('Lead', facts['name']),
340
+ ...line('Status', facts['status']),
341
+ ...line('Captured through', words(facts['sources']).join(', ')),
342
+ ...line('Captures', Number(facts['captures']) || ''),
343
+ ...line('First seen', facts['firstSeen']),
344
+ ...line('Last seen', facts['lastSeen']),
345
+ `Assigned to a teammate: ${facts['assigned'] === true ? 'yes' : 'no'}`,
346
+ `Converted to a contact: ${facts['converted'] === true ? 'yes' : 'no'}`,
347
+ ...line('Unqualified reason', facts['unqualifiedReason']),
348
+ ...line('Notes', facts['notes']),
349
+ ...timeline
350
+ ];
351
+ }
352
+ /** Every contact on a deal and the part each plays (AGL-3521), by name. */ function contactRoleLines(value) {
353
+ return line('Contact roles', list(value).map((row)=>{
354
+ const part = [
355
+ text(row['role']),
356
+ row['primary'] === true ? 'Primary' : ''
357
+ ].filter(Boolean).join(', ');
358
+ return part ? `${text(row['name'])} (${part})` : text(row['name']);
359
+ }).join('; '));
360
+ }
213
361
  /**
214
- * The facts as lines of text. Only the fields named here are written, whatever
215
- * else a reader reports, so what reaches a prompt is decided in two places
216
- * that both have to agree: the CRM's builders and this list.
362
+ * The facts as lines of text: the WHOLE record the CRM reports (AGL-3520) —
363
+ * every standard field, its contact details and addresses, its picklist
364
+ * labels, its custom fields under their labels, its consent, notes,
365
+ * timeline, open tasks and deals. A field the record does not hold writes
366
+ * no line. The CRM's builders decide what a record is; nothing here drops a
367
+ * field they report, and no reader reports a token, an account identifier
368
+ * or a record id beyond a stage's.
217
369
  */ export function aiCrmFactsLines(kind, facts) {
370
+ var _facts_employees;
218
371
  if (kind === 'contact') {
219
372
  const orders = Number(facts['orders']) || 0;
220
373
  return [
221
374
  ...line('Contact', facts['name']),
375
+ ...line('Salutation', facts['salutation']),
376
+ ...line('First name', facts['firstName']),
377
+ ...line('Last name', facts['lastName']),
378
+ ...line('Email', words(facts['emails']).join(', ')),
379
+ ...line('Phone', facts['phone']),
380
+ ...line('Mobile phone', facts['mobilePhone']),
381
+ ...line('Home phone', facts['homePhone']),
382
+ ...line('Other phone', facts['otherPhone']),
383
+ ...line('Fax', facts['fax']),
384
+ // A request the person made, which a drafted next step must respect (AGL-3515).
385
+ ...yesNo('Do not call: they asked not to be phoned', facts['doNotCall']),
222
386
  ...line('Job title', facts['jobTitle']),
387
+ ...line('Department', facts['department']),
223
388
  ...line('Company', facts['company']),
389
+ ...line('Reports to', facts['reportsTo']),
390
+ ...line('Assistant', facts['assistant']),
391
+ ...line('Assistant phone', facts['assistantPhone']),
392
+ ...line('Birthdate', facts['birthdate']),
393
+ ...line('Mailing address', facts['mailingAddress']),
394
+ ...line('Other address', facts['otherAddress']),
224
395
  ...line('Lifecycle stage', facts['lifecycleStage']),
396
+ ...line('Lead source', facts['leadSource']),
397
+ ...line('Owner', facts['owner']),
398
+ ...line('Marketing email consent', facts['marketingConsent']),
225
399
  ...line('Tags', words(facts['tags']).join(', ')),
226
400
  ...line('Came in through', words(facts['sources']).join(', ')),
227
401
  ...orders ? [
@@ -229,6 +403,7 @@ function dealLines(value) {
229
403
  ] : [],
230
404
  ...line('In the CRM since', facts['since']),
231
405
  ...line('Last opened or clicked an email', facts['lastEmailEngagement']),
406
+ ...customLines(facts['custom']),
232
407
  ...line('Notes', facts['notes']),
233
408
  ...timelineLines(facts['timeline']),
234
409
  ...taskLines(facts['openTasks']),
@@ -236,13 +411,33 @@ function dealLines(value) {
236
411
  ];
237
412
  }
238
413
  if (kind === 'company') {
414
+ var _facts_employees1;
239
415
  return [
240
416
  ...line('Company', facts['name']),
241
417
  ...line('Domain', facts['domain']),
418
+ ...line('Website', facts['website']),
419
+ ...line('Phone', facts['phone']),
420
+ ...line('Fax', facts['fax']),
421
+ // Salesforce's Account fields (AGL-3514).
422
+ ...line('Type', facts['type']),
242
423
  ...line('Industry', facts['industry']),
424
+ ...line('Rating', facts['rating']),
425
+ ...line('Ownership', facts['ownership']),
426
+ ...line('Account source', facts['accountSource']),
427
+ ...line('Employees', (_facts_employees1 = facts['employees']) != null ? _facts_employees1 : ''),
428
+ ...line('Annual revenue', facts['annualRevenue']),
429
+ ...line('Account number', facts['accountNumber']),
430
+ ...line('Account site', facts['site']),
431
+ ...line('Ticker symbol', facts['tickerSymbol']),
432
+ ...line('SIC code', facts['sicCode']),
433
+ ...line('Billing address', facts['billingAddress']),
434
+ ...line('Shipping address', facts['shippingAddress']),
435
+ ...line('Parent company', facts['parentCompany']),
436
+ ...line('Owner', facts['owner']),
243
437
  ...line('Tags', words(facts['tags']).join(', ')),
244
438
  ...line('People in the CRM', facts['people']),
245
439
  ...line('In the CRM since', facts['since']),
440
+ ...customLines(facts['custom']),
246
441
  ...line('Notes', facts['notes']),
247
442
  ...timelineLines(facts['timeline']),
248
443
  ...taskLines(facts['openTasks']),
@@ -251,6 +446,7 @@ function dealLines(value) {
251
446
  }
252
447
  if (kind === 'deal') {
253
448
  const stages = list(facts['stages']);
449
+ const products = list(facts['products']);
254
450
  const withWhom = [
255
451
  text(facts['contact']),
256
452
  text(facts['company'])
@@ -263,11 +459,24 @@ function dealLines(value) {
263
459
  ...line('Stages in order', stages.map((stage)=>`${text(stage['id'])} "${text(stage['name'])}" (${text(stage['kind'])})`).join('; ')),
264
460
  ...line('In this stage since', facts['inStageSince']),
265
461
  ...line('Amount', facts['amount']),
462
+ // Salesforce's Opportunity fields (AGL-3516).
463
+ ...line('Probability', typeof facts['probability'] === 'number' ? `${facts['probability']}%` : ''),
464
+ ...line('Forecast category', facts['forecastCategory']),
465
+ ...line('Type', facts['type']),
466
+ ...line('Lead source', facts['leadSource']),
467
+ ...line('Campaign', facts['campaign']),
468
+ ...line('Next step', facts['nextStep']),
266
469
  ...line('Expected to close', facts['expectedClose']),
267
470
  ...line('Lost reason', facts['lostReason']),
268
471
  ...line('With', withWhom),
269
- ...line('Products on the deal', Number(facts['products']) || ''),
472
+ ...line('Owner', facts['owner']),
473
+ ...contactRoleLines(facts['contactRoles']),
474
+ ...products.length ? [
475
+ 'Products:',
476
+ ...products.map((product)=>`- ${text(product['name'])} × ${Number(product['quantity']) || 0}` + (product['unitAmount'] ? ` at ${text(product['unitAmount'])}` : ''))
477
+ ] : [],
270
478
  ...line('In the CRM since', facts['since']),
479
+ ...customLines(facts['custom']),
271
480
  ...line('Notes', facts['notes']),
272
481
  ...timelineLines(facts['timeline']),
273
482
  ...taskLines(facts['openTasks'])
@@ -275,7 +484,28 @@ function dealLines(value) {
275
484
  }
276
485
  return [
277
486
  ...line('Lead', facts['name']),
487
+ ...line('Salutation', facts['salutation']),
488
+ ...line('First name', facts['firstName']),
489
+ ...line('Last name', facts['lastName']),
490
+ ...line('Email', facts['email']),
491
+ ...line('Phone', facts['phone']),
492
+ ...line('Mobile phone', facts['mobilePhone']),
493
+ ...line('Fax', facts['fax']),
494
+ ...yesNo('Do not call: they asked not to be phoned', facts['doNotCall']),
495
+ ...line('Company', facts['company']),
496
+ ...line('Job title', facts['jobTitle']),
497
+ ...line('Website', facts['website']),
498
+ ...line('Address', facts['address']),
499
+ ...line('Industry', facts['industry']),
500
+ ...line('Rating', facts['rating']),
501
+ ...line('Employees', (_facts_employees = facts['employees']) != null ? _facts_employees : ''),
502
+ ...line('Annual revenue', facts['annualRevenue']),
503
+ ...line('Lead source', facts['leadSource']),
278
504
  ...line('Status', facts['status']),
505
+ ...line('Owner', facts['owner']),
506
+ ...line('Campaigns', words(facts['campaigns']).join(', ')),
507
+ ...line('Marketing email consent', facts['marketingConsent']),
508
+ ...line('Tags', words(facts['tags']).join(', ')),
279
509
  ...line('Captured through', words(facts['sources']).join(', ')),
280
510
  ...line('Captures', Number(facts['captures']) || ''),
281
511
  ...line('First seen', facts['firstSeen']),
@@ -283,14 +513,74 @@ function dealLines(value) {
283
513
  `Assigned to a teammate: ${facts['assigned'] === true ? 'yes' : 'no'}`,
284
514
  `Converted to a contact: ${facts['converted'] === true ? 'yes' : 'no'}`,
285
515
  ...line('Unqualified reason', facts['unqualifiedReason']),
516
+ ...customLines(facts['custom']),
286
517
  ...line('Notes', facts['notes']),
287
- ...timelineLines(facts['timeline'])
518
+ ...timelineLines(facts['timeline']),
519
+ ...list(facts['openTasks']).length ? taskLines(facts['openTasks']) : []
288
520
  ];
289
521
  }
522
+ /**
523
+ * The most characters a record's facts take in a request (AGL-3520) —
524
+ * about six thousand tokens at the three characters a token the routing
525
+ * ceilings are measured with. The CRM's own cuts keep a record well under
526
+ * it; a record that holds everything at once is fitted by
527
+ * {@link aiCrmFitFacts}, which shortens its long texts and drops no field.
528
+ */ export const AI_CRM_FACTS_MAX_CHARS = 18000;
529
+ /** The cuts {@link aiCrmFitFacts} tries in turn: notes, then each timeline text, then each custom value and task note. */ const FIT_STEPS = [
530
+ {
531
+ notes: 300,
532
+ text: 160,
533
+ custom: 160
534
+ },
535
+ {
536
+ notes: 160,
537
+ text: 80,
538
+ custom: 80
539
+ },
540
+ {
541
+ notes: 80,
542
+ text: 40,
543
+ custom: 40
544
+ }
545
+ ];
546
+ const cut = (value, max)=>typeof value === 'string' && value.length > max ? `${value.slice(0, max - 1).trimEnd()}…` : value;
547
+ /**
548
+ * The facts, fitted to {@link AI_CRM_FACTS_MAX_CHARS} as lines: unchanged
549
+ * when they fit, otherwise with the long texts — the notes, each timeline
550
+ * entry's text, each task's notes and each custom value — cut shorter step
551
+ * by step. Every field stays; only how much of a long text is sent changes.
552
+ */ export function aiCrmFitFacts(kind, facts) {
553
+ const size = (candidate)=>aiCrmFactsLines(kind, candidate).join('\n').length;
554
+ if (size(facts) <= AI_CRM_FACTS_MAX_CHARS) return facts;
555
+ let fitted = facts;
556
+ for (const step of FIT_STEPS){
557
+ fitted = _extends({}, facts, {
558
+ notes: cut(facts['notes'], step.notes),
559
+ timeline: list(facts['timeline']).map((entry)=>_extends({}, entry, {
560
+ text: cut(entry['text'], step.text)
561
+ })),
562
+ openTasks: list(facts['openTasks']).map((task)=>_extends({}, task, {
563
+ notes: cut(task['notes'], step.text)
564
+ })),
565
+ custom: list(facts['custom']).map((field)=>_extends({}, field, {
566
+ value: cut(field['value'], step.custom)
567
+ }))
568
+ });
569
+ if (size(fitted) <= AI_CRM_FACTS_MAX_CHARS) return fitted;
570
+ }
571
+ return fitted;
572
+ }
573
+ /**
574
+ * The lines a prompt carries for a record: the whole record, fitted, when
575
+ * the reader marked it {@link AI_CRM_WHOLE_RECORD_FIELD}; otherwise the
576
+ * disclosed lines alone.
577
+ */ export function aiCrmPromptFactsLines(kind, facts) {
578
+ return facts[AI_CRM_WHOLE_RECORD_FIELD] === true ? aiCrmFactsLines(kind, aiCrmFitFacts(kind, facts)) : aiCrmDisclosedFactsLines(kind, facts);
579
+ }
290
580
  /** The user turn for a record. */ export function aiCrmRecordPrompt(kind, facts) {
291
581
  return [
292
582
  `Record: ${kind}`,
293
- ...aiCrmFactsLines(kind, facts)
583
+ ...aiCrmPromptFactsLines(kind, facts)
294
584
  ].join('\n');
295
585
  }
296
586
  /** The user turn for an email draft. */ export function aiCrmEmailPrompt(input) {
@@ -300,7 +590,7 @@ function dealLines(value) {
300
590
  `The team member asks: ${input.request.replace(/\s+/g, ' ').trim().slice(0, AI_CRM_EMAIL_REQUEST_MAX_CHARS)}`,
301
591
  '',
302
592
  'Facts:',
303
- ...aiCrmFactsLines(input.kind, input.facts)
593
+ ...aiCrmPromptFactsLines(input.kind, input.facts)
304
594
  ].join('\n');
305
595
  }
306
596
  /** The user turn for an import's columns. */ export function aiCrmMappingPrompt(input) {