@mvriu5/payload-ai 1.4.0 → 1.6.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 (36) hide show
  1. package/README.md +60 -0
  2. package/dist/ai/promptCaching.d.ts +23 -0
  3. package/dist/ai/promptCaching.js +120 -0
  4. package/dist/ai/proposalRepair.d.ts +30 -0
  5. package/dist/ai/proposalRepair.js +76 -0
  6. package/dist/ai/sensitiveData.d.ts +1 -0
  7. package/dist/ai/sensitiveData.js +3 -1
  8. package/dist/components/ai-input/AIInput.js +35 -5
  9. package/dist/components/ai-input/AIInput.module.css +49 -0
  10. package/dist/components/generate-field/GenerateField.d.ts +11 -0
  11. package/dist/components/generate-field/GenerateField.js +109 -0
  12. package/dist/components/generate-field/GenerateField.module.css +39 -0
  13. package/dist/components/generate-field/richText.d.ts +29 -0
  14. package/dist/components/generate-field/richText.js +30 -0
  15. package/dist/components/translate-document/TranslateDocumentButton.d.ts +2 -0
  16. package/dist/components/translate-document/TranslateDocumentButton.js +111 -0
  17. package/dist/components/translate-document/TranslateDocumentButton.module.css +11 -0
  18. package/dist/exports/client.d.ts +4 -0
  19. package/dist/exports/client.js +4 -0
  20. package/dist/handlers/chatHandler.d.ts +1 -0
  21. package/dist/handlers/chatHandler.js +464 -138
  22. package/dist/handlers/generateFieldHandler.d.ts +14 -0
  23. package/dist/handlers/generateFieldHandler.js +144 -0
  24. package/dist/handlers/translateDocumentHandler.d.ts +14 -0
  25. package/dist/handlers/translateDocumentHandler.js +186 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +112 -4
  28. package/dist/payload/documentTranslation.d.ts +15 -0
  29. package/dist/payload/documentTranslation.js +120 -0
  30. package/dist/payload/textFieldGeneration.d.ts +26 -0
  31. package/dist/payload/textFieldGeneration.js +99 -0
  32. package/dist/payload/toolFieldSelection.d.ts +25 -0
  33. package/dist/payload/toolFieldSelection.js +118 -0
  34. package/dist/payload/toolSchemas.d.ts +7 -0
  35. package/dist/payload/toolSchemas.js +117 -0
  36. package/package.json +3 -3
@@ -3,16 +3,17 @@ import { z } from "zod";
3
3
  import { signAIActionProposal } from "../ai/proposalSigning.js";
4
4
  import { isAIProvider } from "../ai/providerOptions.js";
5
5
  import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
6
- import { containsSensitiveData } from "../ai/sensitiveData.js";
6
+ import { createCachedSystemMessages, createPromptCacheProviderOptions, splitPromptCacheContext } from "../ai/promptCaching.js";
7
+ import { compactProposalRepairIssues, createProposalRepairTracker } from "../ai/proposalRepair.js";
8
+ import { containsSensitiveData, isSensitiveKey, redactSensitiveData } from "../ai/sensitiveData.js";
7
9
  import { getExceededTokenUsageLimit, recordTokenUsage } from "../ai/tokenUsage.js";
8
10
  import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
9
11
  import { prepareProposalWriteData } from "../payload/proposalData.js";
10
12
  import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, describeCollectionLikeSummary, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
11
13
  import { getLogPreview, logHandlerEvent } from "../payload/logging.js";
12
14
  import { getOptionValue, getSafeProposalLabel, hasLocalizedData, hasValueAtPath, isRecord, setValueAtPath } from "../payload/shared.js";
13
- const nonEmptyLocalizedDataSchema = z.record(z.string(), z.record(z.string(), z.unknown())).refine((value)=>Object.keys(value).length > 0, {
14
- message: "localizedData must include at least one locale entry."
15
- });
15
+ import { createLocalizedPayloadDataSchema, createPayloadDataSchema, genericPayloadDataSchema } from "../payload/toolSchemas.js";
16
+ import { createToolFieldNamesSchema, resolveToolFieldSelection } from "../payload/toolFieldSelection.js";
16
17
  const e2eModeEnabled = ()=>process.env.PAYLOAD_AI_E2E_MODE === "true";
17
18
  const createSSEEventStream = (events)=>{
18
19
  const encoder = new TextEncoder();
@@ -578,12 +579,16 @@ const getMediaAttachmentContext = async ({ allowedCollectionsBySlug, attachments
578
579
  if (seen.has(key)) continue;
579
580
  const collectionConfig = allowedCollectionsBySlug.get(attachment.collection);
580
581
  if (!collectionConfig?.upload) continue;
582
+ const fieldSelection = resolveToolFieldSelection({
583
+ config: collectionConfig
584
+ });
581
585
  const doc = await req.payload.findByID({
582
586
  collection: attachment.collection,
583
- depth: 1,
587
+ depth: fieldSelection.depth,
584
588
  id: attachment.id,
585
589
  overrideAccess: false,
586
- req
590
+ req,
591
+ select: fieldSelection.select
587
592
  }).catch(()=>null);
588
593
  if (!doc) continue;
589
594
  seen.add(key);
@@ -601,26 +606,6 @@ const getMediaAttachmentContext = async ({ allowedCollectionsBySlug, attachments
601
606
  }
602
607
  return contexts;
603
608
  };
604
- const formatProposalIssuesForRetry = (issues)=>{
605
- return issues.slice(0, 6).map((issue)=>{
606
- switch(issue.code){
607
- case "invalid_block_type":
608
- return `${issue.path}: use an exact blockType from the schema`;
609
- case "invalid_blocks":
610
- return `${issue.path}: blocks fields must be arrays of objects with blockType and exact field names`;
611
- case "invalid_array":
612
- return `${issue.path}: array fields must be arrays of complete objects`;
613
- case "missing_required_field":
614
- return `${issue.path}: required field missing`;
615
- case "unknown_field":
616
- return `${issue.path}: unknown field, use exact schema field names only`;
617
- case "invalid_relationship":
618
- return `${issue.path}: use relationship IDs or { relationTo, value }, not free text`;
619
- default:
620
- return `${issue.path}: ${issue.message}`;
621
- }
622
- }).join("; ");
623
- };
624
609
  const createCollectionAliasMap = (collections)=>{
625
610
  const aliasMap = new Map();
626
611
  const addAlias = (alias, slug)=>{
@@ -663,34 +648,139 @@ const getLikelyCollectionMatches = ({ aliasMap, prompt })=>{
663
648
  ...matches
664
649
  ];
665
650
  };
666
- const hasWriteIntent = (prompt)=>{
667
- const normalizedPrompt = prompt.toLowerCase();
668
- return /\b(create|build|generate|write|draft|make|add|update|edit|change|revise|refine|rewrite|translate|delete|remove)\b/.test(normalizedPrompt);
651
+ const normalizeIntentText = (prompt)=>prompt.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
652
+ const deleteIntentPattern = /\b(?:delete|remove|erase|destroy|discard|purge|loesch\w*|losch\w*|entfern\w*|rausnehm\w*|wegnehm\w*|verwerf\w*)\b/;
653
+ const createIntentPattern = /\b(?:create|build|generate|draft|compose|author|write|make|new|erstell\w*|anleg\w*|erzeug\w*|generier\w*|entwerf\w*|verfass\w*|schreib\w*|neu)\b/;
654
+ const updateIntentPattern = /\b(?:update|edit|change|modify|revise|refine|rewrite|translate|replace|rename|correct|fix|set|publish|unpublish|aktualisier\w*|aender\w*|bearbeit\w*|anpass\w*|ueberarbeit\w*|uberarbeit\w*|umschreib\w*|uebersetz\w*|ubersetz\w*|ersetz\w*|umbenenn\w*|korrigier\w*|reparier\w*|setz\w*|veroeffentlich\w*|veroffentlich\w*|zurueckzieh\w*|zuruckzieh\w*)\b/;
655
+ const additiveIntentPattern = /\b(?:add|insert|append|attach|assign|link|include|extend|ergaenz\w*|erganz\w*|hinzufueg\w*|hinzufug\w*|einfueg\w*|einfug\w*|anhaeng\w*|anhang\w*|zuweis\w*|verknuepf\w*|verknupf\w*)\b/;
656
+ const searchIntentPattern = /\b(?:search|find|lookup|locate|list|show|fetch|browse|query|such\w*|find\w*|nachschlag\w*|auflist\w*|anzeig\w*|zeig\w*|hol\w*|durchsuch\w*)\b/;
657
+ const separableDeleteIntentPattern = /\b(?:nimm|nehm\w*)\b(?:\s+\w+){0,8}\s+(?:raus|weg)\b/;
658
+ const separableUpdateIntentPattern = /\b(?:pass(?:e|t|en|st)?\b(?:\s+\w+){0,8}\s+an|fueg\w*\b(?:\s+\w+){0,8}\s+(?:ein|hinzu)|haeng\w*\b(?:\s+\w+){0,8}\s+an|weis\w*\b(?:\s+\w+){0,8}\s+zu)\b/;
659
+ const getChatIntent = ({ hasCurrentDocument, hasCurrentGlobal, prompt })=>{
660
+ const normalizedPrompt = normalizeIntentText(prompt);
661
+ if (deleteIntentPattern.test(normalizedPrompt) || separableDeleteIntentPattern.test(normalizedPrompt)) {
662
+ return hasCurrentGlobal ? "read" : "delete";
663
+ }
664
+ if (createIntentPattern.test(normalizedPrompt)) {
665
+ if (hasCurrentGlobal) return "updateGlobal";
666
+ if (hasCurrentDocument) return "update";
667
+ return "create";
668
+ }
669
+ if (updateIntentPattern.test(normalizedPrompt) || additiveIntentPattern.test(normalizedPrompt) || separableUpdateIntentPattern.test(normalizedPrompt)) {
670
+ return hasCurrentGlobal ? "updateGlobal" : "update";
671
+ }
672
+ if (searchIntentPattern.test(normalizedPrompt)) {
673
+ return "search";
674
+ }
675
+ return "read";
669
676
  };
670
- const getIntentToolChoice = (prompt)=>{
671
- const normalizedPrompt = prompt.toLowerCase();
672
- if (/\b(delete|remove)\b/.test(normalizedPrompt)) {
677
+ const getIntentToolChoice = ({ hasKnownCollection, hasKnownDocument, hasKnownGlobal, intent })=>{
678
+ if (intent === "create" && hasKnownCollection) {
673
679
  return {
674
- toolName: "proposeDeleteDoc",
680
+ toolName: "proposeCreateDoc",
675
681
  type: "tool"
676
682
  };
677
683
  }
678
- // Update existing document (add block, modify content, etc.)
679
- if (/\b(update|edit|change|modify|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
684
+ if (intent === "update" && hasKnownDocument) {
680
685
  return {
681
686
  toolName: "proposeUpdateDoc",
682
687
  type: "tool"
683
688
  };
684
689
  }
685
- // Create a new document
686
- if (/\b(create|build|generate|write|draft|make|add|füge|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
690
+ if (intent === "delete" && hasKnownDocument) {
687
691
  return {
688
- toolName: "proposeCreateDoc",
692
+ toolName: "proposeDeleteDoc",
693
+ type: "tool"
694
+ };
695
+ }
696
+ if (intent === "updateGlobal" && hasKnownGlobal) {
697
+ return {
698
+ toolName: "proposeUpdateGlobal",
689
699
  type: "tool"
690
700
  };
691
701
  }
692
702
  return undefined;
693
703
  };
704
+ const getToolNamesForIntent = ({ hasAttachments, hasKnownCollection, hasCurrentDocument, hasCurrentGlobal, intent })=>{
705
+ if (hasCurrentGlobal) {
706
+ return new Set(intent === "updateGlobal" ? [
707
+ "getGlobal",
708
+ "listGlobals",
709
+ "proposeUpdateGlobal"
710
+ ] : [
711
+ "getGlobal",
712
+ "listGlobals"
713
+ ]);
714
+ }
715
+ if (hasCurrentDocument) {
716
+ if (intent === "delete") return new Set([
717
+ "getDoc",
718
+ "proposeDeleteDoc"
719
+ ]);
720
+ if (intent === "update" || intent === "create") return new Set([
721
+ "getDoc",
722
+ "listCollections",
723
+ "proposeUpdateDoc"
724
+ ]);
725
+ return new Set([
726
+ "getDoc",
727
+ "listCollections"
728
+ ]);
729
+ }
730
+ if (hasKnownCollection) {
731
+ if (intent === "create") return new Set([
732
+ "getDoc",
733
+ "listCollections",
734
+ "proposeCreateDoc",
735
+ "searchDocs"
736
+ ]);
737
+ if (intent === "update") return new Set([
738
+ "getDoc",
739
+ "listCollections",
740
+ "proposeUpdateDoc",
741
+ "searchDocs"
742
+ ]);
743
+ if (intent === "delete") return new Set([
744
+ "getDoc",
745
+ "proposeDeleteDoc",
746
+ "searchDocs"
747
+ ]);
748
+ return new Set([
749
+ "getDoc",
750
+ "listCollections",
751
+ "searchDocs"
752
+ ]);
753
+ }
754
+ const toolNames = intent === "create" ? new Set([
755
+ "getDoc",
756
+ "listCollections",
757
+ "proposeCreateDoc",
758
+ "searchDocs"
759
+ ]) : intent === "update" ? new Set([
760
+ "getDoc",
761
+ "getGlobal",
762
+ "listCollections",
763
+ "listGlobals",
764
+ "proposeUpdateDoc",
765
+ "proposeUpdateGlobal",
766
+ "searchDocs"
767
+ ]) : intent === "delete" ? new Set([
768
+ "getDoc",
769
+ "listCollections",
770
+ "proposeDeleteDoc",
771
+ "searchDocs"
772
+ ]) : new Set([
773
+ "getDoc",
774
+ "getGlobal",
775
+ "listCollections",
776
+ "listGlobals",
777
+ "searchDocs"
778
+ ]);
779
+ if (hasAttachments && (intent === "create" || intent === "update")) {
780
+ toolNames.add("proposeUpdateDoc");
781
+ }
782
+ return toolNames;
783
+ };
694
784
  export const createChatHandler = (options = {})=>async (req)=>{
695
785
  if (!req.user) return Response.json({
696
786
  error: "Unauthorized"
@@ -819,6 +909,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
819
909
  try {
820
910
  const proposals = [];
821
911
  const toolFailures = [];
912
+ const proposalRepairTracker = createProposalRepairTracker();
822
913
  const registerToolFailure = (failure)=>{
823
914
  toolFailures.push(failure);
824
915
  logHandlerEvent(req, "warn", {
@@ -828,18 +919,66 @@ export const createChatHandler = (options = {})=>async (req)=>{
828
919
  promptPreview: getLogPreview(prompt)
829
920
  });
830
921
  };
831
- const createToolError = ({ collection, details, message, slug, tool })=>{
922
+ const createToolError = ({ collection, details, errorCode = "NON_RETRYABLE_TOOL_ERROR", message, slug, tool })=>{
832
923
  registerToolFailure({
833
924
  collection,
834
925
  details,
926
+ errorCode,
835
927
  message,
928
+ retryable: false,
929
+ slug,
930
+ tool
931
+ });
932
+ return {
933
+ error: message,
934
+ errorCode,
935
+ retryable: false
936
+ };
937
+ };
938
+ const createRepairableToolError = ({ collection, details, id, issues, message, slug, tool })=>{
939
+ const repair = proposalRepairTracker.registerFailure({
940
+ collection,
941
+ id,
942
+ slug,
943
+ tool
944
+ });
945
+ const responseMessage = repair.retryable ? message : "The single proposal repair attempt failed. Do not call the same proposal tool for this target again.";
946
+ registerToolFailure({
947
+ collection,
948
+ details,
949
+ errorCode: repair.errorCode,
950
+ message: responseMessage,
951
+ retryable: repair.retryable,
836
952
  slug,
837
953
  tool
838
954
  });
839
955
  return {
840
- error: message
956
+ error: responseMessage,
957
+ errorCode: repair.errorCode,
958
+ repair: {
959
+ attempt: repair.attempt,
960
+ issues,
961
+ maxAttempts: repair.maxAttempts
962
+ },
963
+ retryable: repair.retryable
841
964
  };
842
965
  };
966
+ const getProposalRepairLimitError = ({ collection, id, slug, tool })=>{
967
+ const callState = proposalRepairTracker.beginCall({
968
+ collection,
969
+ id,
970
+ slug,
971
+ tool
972
+ });
973
+ if (callState !== "blocked") return null;
974
+ return createToolError({
975
+ collection,
976
+ errorCode: "REPAIR_EXHAUSTED",
977
+ message: "The single proposal repair attempt was already used. Do not call this proposal tool for the same target again.",
978
+ slug,
979
+ tool
980
+ });
981
+ };
843
982
  const addSignedProposal = (proposal)=>{
844
983
  if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) {
845
984
  return createToolError({
@@ -921,6 +1060,12 @@ export const createChatHandler = (options = {})=>async (req)=>{
921
1060
  parent: global.slug
922
1061
  }))
923
1062
  ];
1063
+ const explicitlyMentionedDocumentKeys = new Set((body?.mentions || []).flatMap((mention)=>mention.type === "doc" && mention.collection && mention.id ? [
1064
+ `${mention.collection}:${mention.id}`
1065
+ ] : []));
1066
+ const explicitlyMentionedGlobalSlugs = new Set((body?.mentions || []).flatMap((mention)=>mention.type === "global" && mention.slug ? [
1067
+ mention.slug
1068
+ ] : []));
924
1069
  const mentionContext = await getMentionContext({
925
1070
  blockContexts,
926
1071
  collectionSlugs,
@@ -930,37 +1075,65 @@ export const createChatHandler = (options = {})=>async (req)=>{
930
1075
  req
931
1076
  });
932
1077
  if (requestedCollectionSlug && requestedDocumentID) {
933
- const currentDocument = await req.payload.findByID({
934
- collection: requestedCollectionSlug,
935
- depth: 2,
936
- id: requestedDocumentID,
937
- ...activeLocale ? {
938
- locale: activeLocale
939
- } : {},
940
- overrideAccess: false,
941
- req
942
- });
943
- mentionContext.push({
944
- collection: requestedCollectionSlug,
945
- document: currentDocument,
946
- id: requestedDocumentID,
947
- type: "currentDocument"
948
- });
1078
+ const currentDocumentKey = `${requestedCollectionSlug}:${requestedDocumentID}`;
1079
+ if (explicitlyMentionedDocumentKeys.has(currentDocumentKey)) {
1080
+ mentionContext.push({
1081
+ collection: requestedCollectionSlug,
1082
+ id: requestedDocumentID,
1083
+ source: "mention",
1084
+ type: "currentDocumentScope"
1085
+ });
1086
+ } else {
1087
+ const currentCollectionConfig = allowedCollectionsBySlug.get(requestedCollectionSlug);
1088
+ const fieldSelection = resolveToolFieldSelection({
1089
+ config: currentCollectionConfig
1090
+ });
1091
+ const currentDocument = await req.payload.findByID({
1092
+ collection: requestedCollectionSlug,
1093
+ depth: fieldSelection.depth,
1094
+ id: requestedDocumentID,
1095
+ ...activeLocale ? {
1096
+ locale: activeLocale
1097
+ } : {},
1098
+ overrideAccess: false,
1099
+ req,
1100
+ select: fieldSelection.select
1101
+ });
1102
+ mentionContext.push({
1103
+ collection: requestedCollectionSlug,
1104
+ document: currentDocument,
1105
+ id: requestedDocumentID,
1106
+ type: "currentDocument"
1107
+ });
1108
+ }
949
1109
  } else if (requestedGlobalSlug) {
950
- const currentGlobal = await req.payload.findGlobal({
951
- depth: 2,
952
- ...activeLocale ? {
953
- locale: activeLocale
954
- } : {},
955
- overrideAccess: false,
956
- req,
957
- slug: requestedGlobalSlug
958
- });
959
- mentionContext.push({
960
- global: currentGlobal,
961
- slug: requestedGlobalSlug,
962
- type: "currentGlobal"
963
- });
1110
+ if (explicitlyMentionedGlobalSlugs.has(requestedGlobalSlug)) {
1111
+ mentionContext.push({
1112
+ slug: requestedGlobalSlug,
1113
+ source: "mention",
1114
+ type: "currentGlobalScope"
1115
+ });
1116
+ } else {
1117
+ const currentGlobalConfig = globalConfigsBySlug.get(requestedGlobalSlug);
1118
+ const fieldSelection = resolveToolFieldSelection({
1119
+ config: currentGlobalConfig
1120
+ });
1121
+ const currentGlobal = await req.payload.findGlobal({
1122
+ depth: fieldSelection.depth,
1123
+ ...activeLocale ? {
1124
+ locale: activeLocale
1125
+ } : {},
1126
+ overrideAccess: false,
1127
+ req,
1128
+ select: fieldSelection.select,
1129
+ slug: requestedGlobalSlug
1130
+ });
1131
+ mentionContext.push({
1132
+ global: currentGlobal,
1133
+ slug: requestedGlobalSlug,
1134
+ type: "currentGlobal"
1135
+ });
1136
+ }
964
1137
  }
965
1138
  const mediaAttachmentContext = await getMediaAttachmentContext({
966
1139
  allowedCollectionsBySlug,
@@ -1008,7 +1181,17 @@ export const createChatHandler = (options = {})=>async (req)=>{
1008
1181
  aliasMap: collectionAliasMap,
1009
1182
  prompt
1010
1183
  });
1011
- const writeIntent = hasWriteIntent(prompt);
1184
+ const chatIntent = getChatIntent({
1185
+ hasCurrentDocument: Boolean(requestedCollectionSlug && requestedDocumentID),
1186
+ hasCurrentGlobal: Boolean(requestedGlobalSlug),
1187
+ prompt
1188
+ });
1189
+ const writeIntent = [
1190
+ "create",
1191
+ "delete",
1192
+ "update",
1193
+ "updateGlobal"
1194
+ ].includes(chatIntent);
1012
1195
  const inferredCollectionSlug = requestedCollectionSlug || (mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined);
1013
1196
  const inferredCollectionConfig = inferredCollectionSlug ? allowedCollectionsBySlug.get(inferredCollectionSlug) : undefined;
1014
1197
  if (inferredCollectionConfig && !mentionContext.some((item)=>item.type === "collection" && item.slug === inferredCollectionConfig.slug)) {
@@ -1030,10 +1213,16 @@ export const createChatHandler = (options = {})=>async (req)=>{
1030
1213
  }));
1031
1214
  }
1032
1215
  }
1033
- let intentToolChoice = inferredCollectionConfig ? getIntentToolChoice(prompt) : undefined;
1216
+ let intentToolChoice = getIntentToolChoice({
1217
+ hasKnownCollection: Boolean(inferredCollectionConfig),
1218
+ hasKnownDocument: Boolean(requestedCollectionSlug && requestedDocumentID),
1219
+ hasKnownGlobal: Boolean(requestedGlobalSlug),
1220
+ intent: chatIntent
1221
+ });
1034
1222
  logHandlerEvent(req, "info", {
1035
1223
  activeLocale,
1036
1224
  allowedCollectionCount: allowedCollections.length,
1225
+ chatIntent,
1037
1226
  collectionSlugs,
1038
1227
  focusedCollections: mentionedCollectionSlugs,
1039
1228
  globalSlugs,
@@ -1045,6 +1234,19 @@ export const createChatHandler = (options = {})=>async (req)=>{
1045
1234
  writeIntent
1046
1235
  });
1047
1236
  const collectionSlugSchema = collectionSlugs.length > 0 ? z.enum(collectionSlugs) : z.string().refine(()=>false, "No collection is in scope.");
1237
+ const focusedCollectionDataSchema = createPayloadDataSchema(inferredCollectionConfig);
1238
+ const focusedCollectionSlugSchema = inferredCollectionSlug ? z.literal(inferredCollectionSlug) : collectionSlugSchema;
1239
+ const focusedGlobalConfig = requestedGlobalSlug ? globalConfigsBySlug.get(requestedGlobalSlug) : undefined;
1240
+ const focusedGlobalDataSchema = createPayloadDataSchema(focusedGlobalConfig);
1241
+ const collectionReadFieldsSchema = createToolFieldNamesSchema(allowedCollections);
1242
+ const globalReadFieldsSchema = createToolFieldNamesSchema(globalConfigs);
1243
+ const createDataSchema = inferredCollectionConfig ? focusedCollectionDataSchema : genericPayloadDataSchema;
1244
+ const createLocalizedDataSchema = createLocalizedPayloadDataSchema(createDataSchema);
1245
+ const updateHasMultipleTargets = mediaAttachmentContext.length > 0 && !requestedDocumentID;
1246
+ const updateDataSchema = inferredCollectionConfig && !updateHasMultipleTargets ? focusedCollectionDataSchema : genericPayloadDataSchema;
1247
+ const updateLocalizedDataSchema = createLocalizedPayloadDataSchema(updateDataSchema);
1248
+ const globalDataSchema = focusedGlobalConfig ? focusedGlobalDataSchema : genericPayloadDataSchema;
1249
+ const globalLocalizedDataSchema = createLocalizedPayloadDataSchema(globalDataSchema);
1048
1250
  const getDisallowedCollectionActionError = (collection, action)=>{
1049
1251
  if (isCollectionActionAllowed({
1050
1252
  action,
@@ -1060,12 +1262,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
1060
1262
  };
1061
1263
  const allTools = {
1062
1264
  getDoc: {
1063
- description: "Read a document by collection and id.",
1265
+ description: "Read a compact document by collection and id. Optionally request up to 12 exact top-level schema fields. Explicitly @-mentioned documents remain complete.",
1064
1266
  inputSchema: z.object({
1065
1267
  collection: collectionSlugSchema,
1268
+ fields: collectionReadFieldsSchema,
1066
1269
  id: z.string().min(1)
1067
1270
  }),
1068
- execute: async ({ collection, id })=>{
1271
+ execute: async ({ collection, fields, id })=>{
1069
1272
  if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
1070
1273
  return createToolError({
1071
1274
  collection,
@@ -1073,16 +1276,37 @@ export const createChatHandler = (options = {})=>async (req)=>{
1073
1276
  tool: "getDoc"
1074
1277
  });
1075
1278
  }
1076
- return req.payload.findByID({
1279
+ const explicitlyMentioned = explicitlyMentionedDocumentKeys.has(`${collection}:${id}`);
1280
+ const collectionConfig = allowedCollectionsBySlug.get(collection);
1281
+ const fieldSelection = resolveToolFieldSelection({
1282
+ config: collectionConfig,
1283
+ requestedFields: fields
1284
+ });
1285
+ if (!explicitlyMentioned && fieldSelection.invalidFields.length > 0) {
1286
+ return createToolError({
1287
+ collection,
1288
+ details: {
1289
+ invalidFields: fieldSelection.invalidFields
1290
+ },
1291
+ errorCode: "INVALID_FIELD_SELECTION",
1292
+ message: `Unknown or sensitive fields requested for ${collection}: ${fieldSelection.invalidFields.join(", ")}.`,
1293
+ tool: "getDoc"
1294
+ });
1295
+ }
1296
+ const document = await req.payload.findByID({
1077
1297
  collection: collection,
1078
- depth: 2,
1298
+ depth: explicitlyMentioned ? 2 : fieldSelection.depth,
1079
1299
  id,
1080
1300
  ...activeLocale ? {
1081
1301
  locale: activeLocale
1082
1302
  } : {},
1083
1303
  overrideAccess: false,
1084
- req
1304
+ req,
1305
+ ...!explicitlyMentioned ? {
1306
+ select: fieldSelection.select
1307
+ } : {}
1085
1308
  });
1309
+ return redactSensitiveData(document);
1086
1310
  }
1087
1311
  },
1088
1312
  listCollections: {
@@ -1114,11 +1338,12 @@ export const createChatHandler = (options = {})=>async (req)=>{
1114
1338
  }
1115
1339
  },
1116
1340
  getGlobal: {
1117
- description: "Read a global by slug.",
1341
+ description: "Read a compact global. Optionally request up to 12 exact top-level schema fields. Explicitly @-mentioned globals remain complete.",
1118
1342
  inputSchema: z.object({
1343
+ fields: globalReadFieldsSchema,
1119
1344
  slug: z.string().min(1)
1120
1345
  }),
1121
- execute: async ({ slug })=>{
1346
+ execute: async ({ fields, slug })=>{
1122
1347
  if (requestedDocumentScope && slug !== requestedGlobalSlug) {
1123
1348
  return createToolError({
1124
1349
  message: "Only the current global can be read in this context.",
@@ -1134,15 +1359,35 @@ export const createChatHandler = (options = {})=>async (req)=>{
1134
1359
  tool: "getGlobal"
1135
1360
  });
1136
1361
  }
1137
- return req.payload.findGlobal({
1138
- depth: 2,
1362
+ const explicitlyMentioned = explicitlyMentionedGlobalSlugs.has(slug);
1363
+ const fieldSelection = resolveToolFieldSelection({
1364
+ config: globalConfig,
1365
+ requestedFields: fields
1366
+ });
1367
+ if (!explicitlyMentioned && fieldSelection.invalidFields.length > 0) {
1368
+ return createToolError({
1369
+ details: {
1370
+ invalidFields: fieldSelection.invalidFields
1371
+ },
1372
+ errorCode: "INVALID_FIELD_SELECTION",
1373
+ message: `Unknown or sensitive fields requested for global ${slug}: ${fieldSelection.invalidFields.join(", ")}.`,
1374
+ slug,
1375
+ tool: "getGlobal"
1376
+ });
1377
+ }
1378
+ const global = await req.payload.findGlobal({
1379
+ depth: explicitlyMentioned ? 2 : fieldSelection.depth,
1139
1380
  ...activeLocale ? {
1140
1381
  locale: activeLocale
1141
1382
  } : {},
1142
1383
  overrideAccess: false,
1143
1384
  req,
1385
+ ...!explicitlyMentioned ? {
1386
+ select: fieldSelection.select
1387
+ } : {},
1144
1388
  slug: slug
1145
1389
  });
1390
+ return redactSensitiveData(global);
1146
1391
  }
1147
1392
  },
1148
1393
  listGlobals: {
@@ -1174,16 +1419,23 @@ export const createChatHandler = (options = {})=>async (req)=>{
1174
1419
  proposeCreateDoc: {
1175
1420
  description: "Propose document creation. Use exact schema fields; include required fields. Use localizedData for multi-locale writes.",
1176
1421
  inputSchema: z.object({
1177
- collection: collectionSlugSchema,
1178
- data: z.record(z.string(), z.unknown()).optional(),
1422
+ collection: focusedCollectionSlugSchema,
1423
+ data: createDataSchema.optional(),
1179
1424
  label: z.string().min(1),
1180
- localizedData: nonEmptyLocalizedDataSchema.optional()
1425
+ localizedData: createLocalizedDataSchema.optional()
1181
1426
  }).refine((value)=>Boolean(value.data || value.localizedData), {
1182
1427
  message: "Either data or localizedData is required."
1183
1428
  }),
1184
1429
  execute: async ({ collection, data, label, localizedData })=>{
1185
1430
  const permissionError = getDisallowedCollectionActionError(collection, "create");
1186
1431
  if (permissionError) return permissionError;
1432
+ const repairTargetID = getSafeProposalLabel(label);
1433
+ const repairLimitError = getProposalRepairLimitError({
1434
+ collection,
1435
+ id: repairTargetID,
1436
+ tool: "proposeCreateDoc"
1437
+ });
1438
+ if (repairLimitError) return repairLimitError;
1187
1439
  const collectionConfig = allowedCollectionsBySlug.get(collection);
1188
1440
  const collectionFields = collectionConfig?.fields || [];
1189
1441
  const preparedData = prepareProposalWriteData({
@@ -1197,13 +1449,15 @@ export const createChatHandler = (options = {})=>async (req)=>{
1197
1449
  if (preparedData.issues.length > 0) {
1198
1450
  const titleFieldName = titleFieldByCollection[collection];
1199
1451
  const missingTitleField = titleFieldName ? preparedData.issues.some((issue)=>issue.path === titleFieldName || issue.path.endsWith(`.${titleFieldName}`)) : false;
1200
- return createToolError({
1452
+ return createRepairableToolError({
1201
1453
  collection,
1202
1454
  details: {
1203
1455
  issues: preparedData.issues,
1204
1456
  titleFieldName
1205
1457
  },
1206
- message: missingTitleField ? `Create proposal is missing the required title field "${titleFieldName}" for ${collection}. Infer a concise title from the user request and retry.` : `Create proposal for ${collection} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
1458
+ id: repairTargetID,
1459
+ issues: compactProposalRepairIssues(preparedData.issues),
1460
+ message: missingTitleField ? `Create proposal is missing the required title field "${titleFieldName}" for ${collection}.` : `Create proposal data for ${collection} is invalid. Correct only repair.issues and call the same tool once more.`,
1207
1461
  tool: "proposeCreateDoc"
1208
1462
  });
1209
1463
  }
@@ -1234,13 +1488,19 @@ export const createChatHandler = (options = {})=>async (req)=>{
1234
1488
  }
1235
1489
  const missingBlockTypes = requestedBlockTypes.filter((blockType)=>!proposalBlockTypes.has(blockType));
1236
1490
  if (missingBlockTypes.length > 0) {
1237
- return createToolError({
1491
+ return createRepairableToolError({
1238
1492
  collection,
1239
1493
  details: {
1240
1494
  missingBlockTypes,
1241
1495
  requestedBlockTypes
1242
1496
  },
1243
- message: `Create proposal for ${collection} is missing required block types from the request: ${missingBlockTypes.join(", ")}. Add them to the appropriate blocks field using exact blockType values and complete required fields.`,
1497
+ id: repairTargetID,
1498
+ issues: missingBlockTypes.map((blockType)=>({
1499
+ code: "missing_requested_block",
1500
+ hint: `Add a complete "${blockType}" block using the exact blockType and schema fields.`,
1501
+ path: "blocks"
1502
+ })),
1503
+ message: `Create proposal for ${collection} is missing requested block types: ${missingBlockTypes.join(", ")}.`,
1244
1504
  tool: "proposeCreateDoc"
1245
1505
  });
1246
1506
  }
@@ -1344,11 +1604,11 @@ export const createChatHandler = (options = {})=>async (req)=>{
1344
1604
  proposeUpdateDoc: {
1345
1605
  description: "Propose document update. Use exact schema fields. Use localizedData for multi-locale writes.",
1346
1606
  inputSchema: z.object({
1347
- collection: collectionSlugSchema,
1348
- data: z.record(z.string(), z.unknown()).optional(),
1607
+ collection: updateHasMultipleTargets ? collectionSlugSchema : focusedCollectionSlugSchema,
1608
+ data: updateDataSchema.optional(),
1349
1609
  id: z.string().min(1),
1350
1610
  label: z.string().min(1),
1351
- localizedData: nonEmptyLocalizedDataSchema.optional()
1611
+ localizedData: updateLocalizedDataSchema.optional()
1352
1612
  }).refine((value)=>Boolean(value.data || value.localizedData), {
1353
1613
  message: "Either data or localizedData is required."
1354
1614
  }),
@@ -1362,6 +1622,12 @@ export const createChatHandler = (options = {})=>async (req)=>{
1362
1622
  }
1363
1623
  const permissionError = getDisallowedCollectionActionError(collection, "update");
1364
1624
  if (permissionError) return permissionError;
1625
+ const repairLimitError = getProposalRepairLimitError({
1626
+ collection,
1627
+ id,
1628
+ tool: "proposeUpdateDoc"
1629
+ });
1630
+ if (repairLimitError) return repairLimitError;
1365
1631
  const collectionConfig = allowedCollectionsBySlug.get(collection);
1366
1632
  const collectionFields = collectionConfig?.fields || [];
1367
1633
  const preparedData = prepareProposalWriteData({
@@ -1373,12 +1639,14 @@ export const createChatHandler = (options = {})=>async (req)=>{
1373
1639
  mode: "update"
1374
1640
  });
1375
1641
  if (preparedData.issues.length > 0) {
1376
- return createToolError({
1642
+ return createRepairableToolError({
1377
1643
  collection,
1378
1644
  details: {
1379
1645
  issues: preparedData.issues
1380
1646
  },
1381
- message: `Update proposal for ${collection} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
1647
+ id,
1648
+ issues: compactProposalRepairIssues(preparedData.issues),
1649
+ message: `Update proposal data for ${collection}:${id} is invalid. Correct only repair.issues and call the same tool once more.`,
1382
1650
  tool: "proposeUpdateDoc"
1383
1651
  });
1384
1652
  }
@@ -1449,10 +1717,10 @@ export const createChatHandler = (options = {})=>async (req)=>{
1449
1717
  proposeUpdateGlobal: {
1450
1718
  description: "Propose global update. Use localizedData for multi-locale writes.",
1451
1719
  inputSchema: z.object({
1452
- data: z.record(z.string(), z.unknown()).optional(),
1720
+ data: globalDataSchema.optional(),
1453
1721
  label: z.string().min(1),
1454
- localizedData: nonEmptyLocalizedDataSchema.optional(),
1455
- slug: z.string().min(1)
1722
+ localizedData: globalLocalizedDataSchema.optional(),
1723
+ slug: requestedGlobalSlug ? z.literal(requestedGlobalSlug) : z.string().min(1)
1456
1724
  }).refine((value)=>Boolean(value.data || value.localizedData), {
1457
1725
  message: "Either data or localizedData is required."
1458
1726
  }),
@@ -1472,6 +1740,11 @@ export const createChatHandler = (options = {})=>async (req)=>{
1472
1740
  tool: "proposeUpdateGlobal"
1473
1741
  });
1474
1742
  }
1743
+ const repairLimitError = getProposalRepairLimitError({
1744
+ slug,
1745
+ tool: "proposeUpdateGlobal"
1746
+ });
1747
+ if (repairLimitError) return repairLimitError;
1475
1748
  const preparedData = prepareProposalWriteData({
1476
1749
  collectionConfig: {
1477
1750
  fields: globalConfig.fields || [],
@@ -1484,11 +1757,12 @@ export const createChatHandler = (options = {})=>async (req)=>{
1484
1757
  mode: "update"
1485
1758
  });
1486
1759
  if (preparedData.issues.length > 0) {
1487
- return createToolError({
1760
+ return createRepairableToolError({
1488
1761
  details: {
1489
1762
  issues: preparedData.issues
1490
1763
  },
1491
- message: `Update proposal for global ${slug} is invalid. Retry with exact schema fields and complete array/block objects: ${formatProposalIssuesForRetry(preparedData.issues)}`,
1764
+ issues: compactProposalRepairIssues(preparedData.issues),
1765
+ message: `Update proposal data for global ${slug} is invalid. Correct only repair.issues and call the same tool once more.`,
1492
1766
  slug,
1493
1767
  tool: "proposeUpdateGlobal"
1494
1768
  });
@@ -1510,20 +1784,37 @@ export const createChatHandler = (options = {})=>async (req)=>{
1510
1784
  }
1511
1785
  },
1512
1786
  searchDocs: {
1513
- description: "Search documents in one collection.",
1787
+ description: "Search compact documents in one collection. Returns default identity fields or up to 12 requested exact top-level schema fields.",
1514
1788
  inputSchema: z.object({
1515
1789
  collection: collectionSlugSchema,
1790
+ fields: collectionReadFieldsSchema,
1516
1791
  limit: z.number().int().min(1).max(10).default(5),
1517
1792
  query: z.string().optional()
1518
1793
  }),
1519
- execute: async ({ collection, limit, query })=>{
1794
+ execute: async ({ collection, fields, limit, query })=>{
1520
1795
  const collectionConfig = allowedCollectionsBySlug.get(collection);
1796
+ const fieldSelection = resolveToolFieldSelection({
1797
+ config: collectionConfig,
1798
+ mode: "search",
1799
+ requestedFields: fields
1800
+ });
1801
+ if (fieldSelection.invalidFields.length > 0) {
1802
+ return createToolError({
1803
+ collection,
1804
+ details: {
1805
+ invalidFields: fieldSelection.invalidFields
1806
+ },
1807
+ errorCode: "INVALID_FIELD_SELECTION",
1808
+ message: `Unknown or sensitive fields requested for ${collection}: ${fieldSelection.invalidFields.join(", ")}.`,
1809
+ tool: "searchDocs"
1810
+ });
1811
+ }
1521
1812
  const searchableFields = collectionConfig?.fields.flatMap((field)=>{
1522
1813
  if (!("name" in field) || ![
1523
1814
  "email",
1524
1815
  "text",
1525
1816
  "textarea"
1526
- ].includes(field.type) || !field.name) return [];
1817
+ ].includes(field.type) || !field.name || isSensitiveKey(field.name)) return [];
1527
1818
  return [
1528
1819
  field.name
1529
1820
  ];
@@ -1535,34 +1826,35 @@ export const createChatHandler = (options = {})=>async (req)=>{
1535
1826
  }
1536
1827
  }))
1537
1828
  } : undefined;
1538
- return req.payload.find({
1829
+ const result = await req.payload.find({
1539
1830
  collection: collection,
1540
- depth: 1,
1831
+ depth: fieldSelection.depth,
1541
1832
  limit,
1542
1833
  ...activeLocale ? {
1543
1834
  locale: activeLocale
1544
1835
  } : {},
1545
1836
  overrideAccess: false,
1546
1837
  req,
1838
+ select: fieldSelection.select,
1547
1839
  where
1548
1840
  });
1841
+ return redactSensitiveData({
1842
+ docs: result.docs,
1843
+ hasNextPage: result.hasNextPage
1844
+ });
1549
1845
  }
1550
1846
  }
1551
1847
  };
1552
- const scopedToolNames = requestedCollectionSlug ? requestedDocumentID ? new Set([
1553
- "getDoc",
1554
- "listCollections",
1555
- "proposeUpdateDoc"
1556
- ]) : new Set([
1557
- "listCollections",
1558
- "proposeCreateDoc"
1559
- ]) : requestedGlobalSlug ? new Set([
1560
- "getGlobal",
1561
- "listGlobals",
1562
- "proposeUpdateGlobal"
1563
- ]) : null;
1564
- const tools = scopedToolNames ? Object.fromEntries(Object.entries(allTools).filter(([name])=>scopedToolNames.has(name))) : allTools;
1565
- if (intentToolChoice && scopedToolNames && !scopedToolNames.has(intentToolChoice.toolName)) {
1848
+ const scopedToolNames = getToolNamesForIntent({
1849
+ hasAttachments: Boolean(body?.attachments?.length),
1850
+ hasKnownCollection: Boolean(inferredCollectionConfig),
1851
+ hasCurrentDocument: Boolean(requestedCollectionSlug && requestedDocumentID),
1852
+ hasCurrentGlobal: Boolean(requestedGlobalSlug),
1853
+ intent: chatIntent
1854
+ });
1855
+ const tools = Object.fromEntries(Object.entries(allTools).filter(([name])=>scopedToolNames.has(name)));
1856
+ debug.tools = Object.keys(tools);
1857
+ if (intentToolChoice && !scopedToolNames.has(intentToolChoice.toolName)) {
1566
1858
  intentToolChoice = undefined;
1567
1859
  }
1568
1860
  const encoder = new TextEncoder();
@@ -1577,34 +1869,68 @@ export const createChatHandler = (options = {})=>async (req)=>{
1577
1869
  model: providerConfig.modelID,
1578
1870
  provider
1579
1871
  });
1872
+ const promptCaching = options.promptCaching !== false;
1873
+ const explicitPromptCaching = promptCaching && !managedProvider?.baseURL;
1874
+ const sanitizedMentionContext = redactSensitiveData(mentionContext);
1875
+ const { cacheable: cacheableMentionContext, dynamic: dynamicMentionContext } = splitPromptCacheContext(sanitizedMentionContext);
1876
+ const staticSystemInstructions = [
1877
+ "You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
1878
+ "Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
1879
+ "Writes are proposals only. Never claim changes were applied before user confirmation.",
1880
+ "Uploaded media attachments appear in context as mediaAttachment entries. Use their exact IDs for upload fields.",
1881
+ "If an uploaded media document has editable descriptive fields, propose an update to that media document with suitable values.",
1882
+ "If a collection has a required `slug` field and the user does not provide it, generate a URL-friendly slug from the title or from the first meaningful word of the prompt.",
1883
+ "For create/update/delete requests, you must use proposal tools. Do not end with plain text if the user asked for a content change.",
1884
+ "If the user asks to create, update, refine, translate, remove, or delete content, produce at least one proposal tool call unless blocked by missing schema information or permissions.",
1885
+ "Put concrete field values only in tool data, not visible text.",
1886
+ "For blocks fields: use exact blockType values from schema, exact field names, and complete objects for required block fields.",
1887
+ "For arrays: every item must be an object matching the child field schema, not free text.",
1888
+ "If schema details are missing, call listCollections/listGlobals with a slug before proposing. If an inferred collection schema is already present in context, use it directly.",
1889
+ "Read tools return compact identity fields by default. Request only the additional fields needed for the current task; full schemas remain available separately.",
1890
+ "If a proposal tool returns retryable=true, correct only repair.issues and call the same proposal tool once more. If retryable=false, do not retry that tool target."
1891
+ ];
1892
+ const cacheableSystemInstructions = [
1893
+ `Collection aliases: ${JSON.stringify(collectionAliasMap)}.`,
1894
+ `Focused required create fields: ${JSON.stringify(focusedRequiredFieldsByCollection)}.`,
1895
+ `Focused title fields: ${JSON.stringify(focusedTitleFieldByCollection)}. Infer concise titles when needed.`
1896
+ ];
1897
+ const dynamicSystemInstructions = [
1898
+ `Likely collection matches for this prompt: ${JSON.stringify(likelyCollectionMatches)}.`,
1899
+ `Request intent: ${chatIntent}. Preferred proposal tool: ${intentToolChoice?.toolName || "none"}.`,
1900
+ "Visible response: plain text, under 40 words, no Markdown, no proposed content."
1901
+ ];
1902
+ const system = createCachedSystemMessages({
1903
+ cacheableContext: cacheableMentionContext,
1904
+ cacheableInstructions: cacheableSystemInstructions,
1905
+ dynamicInstructions: dynamicSystemInstructions,
1906
+ enabled: explicitPromptCaching,
1907
+ provider,
1908
+ staticInstructions: staticSystemInstructions
1909
+ });
1910
+ const promptCacheProviderOptions = createPromptCacheProviderOptions({
1911
+ cacheKeyParts: [
1912
+ cacheableMentionContext,
1913
+ cacheableSystemInstructions,
1914
+ [
1915
+ ...scopedToolNames
1916
+ ].sort()
1917
+ ],
1918
+ enabled: explicitPromptCaching,
1919
+ model: providerConfig.modelID,
1920
+ provider
1921
+ });
1580
1922
  const result = streamText({
1581
1923
  maxOutputTokens: options.maxOutputTokens || 700,
1582
1924
  model,
1583
1925
  prompt: buildPromptWithMentionContext({
1584
- mentionContext,
1926
+ mentionContext: dynamicMentionContext,
1585
1927
  prompt
1586
1928
  }),
1929
+ ...promptCacheProviderOptions ? {
1930
+ providerOptions: promptCacheProviderOptions
1931
+ } : {},
1587
1932
  stopWhen: stepCountIs(6),
1588
- system: [
1589
- "You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
1590
- "Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
1591
- "Writes are proposals only. Never claim changes were applied before user confirmation.",
1592
- "Uploaded media attachments appear in context as mediaAttachment entries. Use their exact IDs for upload fields.",
1593
- "If an uploaded media document has editable descriptive fields, propose an update to that media document with suitable values.",
1594
- "If a collection has a required `slug` field and the user does not provide it, generate a URL‑friendly slug from the title or from the first meaningful word of the prompt.",
1595
- "For create/update/delete requests, you must use proposal tools. Do not end with plain text if the user asked for a content change.",
1596
- "If the user asks to create, update, refine, translate, remove, or delete content, produce at least one proposal tool call unless blocked by missing schema information or permissions.",
1597
- "Put concrete field values only in tool data, not visible text.",
1598
- "For blocks fields: use exact blockType values from schema, exact field names, and complete objects for required block fields.",
1599
- "For arrays: every item must be an object matching the child field schema, not free text.",
1600
- "If schema details are missing, call listCollections/listGlobals with a slug before proposing. If an inferred collection schema is already present in context, use it directly.",
1601
- `Collection aliases: ${JSON.stringify(collectionAliasMap)}.`,
1602
- `Likely collection matches for this prompt: ${JSON.stringify(likelyCollectionMatches)}.`,
1603
- `Focused required create fields: ${JSON.stringify(focusedRequiredFieldsByCollection)}.`,
1604
- `Focused title fields: ${JSON.stringify(focusedTitleFieldByCollection)}. Infer concise titles when needed.`,
1605
- `Preferred proposal tool for this prompt: ${intentToolChoice?.toolName || "none"}.`,
1606
- "Visible response: plain text, under 40 words, no Markdown, no proposed content."
1607
- ].join("\n"),
1933
+ system,
1608
1934
  ...intentToolChoice ? {
1609
1935
  toolChoice: intentToolChoice
1610
1936
  } : {},