@agent-native/core 0.80.0 → 0.80.2

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 (228) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
  5. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
  7. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
  9. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
  11. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
  13. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
  15. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
  17. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
  19. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
  21. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
  23. package/corpus/core/docs/content/skills-guide.mdx +5 -0
  24. package/corpus/core/docs/content/template-design.mdx +14 -1
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/application-state/handlers.ts +1 -1
  27. package/corpus/core/src/brand-kit/index.ts +1 -1
  28. package/corpus/core/src/cli/skills.ts +18 -0
  29. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  30. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  31. package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
  32. package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
  33. package/corpus/core/src/extensions/path.ts +1 -4
  34. package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
  35. package/corpus/core/src/mcp/build-server.ts +2 -2
  36. package/corpus/core/src/mcp/embed-app.ts +35 -1
  37. package/corpus/core/src/org/handlers.ts +5 -5
  38. package/corpus/core/src/org/plugin.ts +2 -2
  39. package/corpus/core/src/scripts/call-agent.ts +1 -1
  40. package/corpus/core/src/scripts/db/safety.ts +1 -1
  41. package/corpus/core/src/server/auth.ts +3 -0
  42. package/corpus/core/src/server/builder-design-systems.ts +308 -0
  43. package/corpus/core/src/server/core-routes-plugin.ts +29 -3
  44. package/corpus/core/src/server/index.ts +8 -0
  45. package/corpus/core/src/styles/agent-native.css +14 -7
  46. package/corpus/core/src/usage/store.ts +2 -2
  47. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
  48. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
  49. package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
  50. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
  51. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
  52. package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
  53. package/corpus/templates/analytics/app/i18n-data.ts +404 -0
  54. package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
  57. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
  58. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
  59. package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
  66. package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
  67. package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
  68. package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
  69. package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
  70. package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
  71. package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
  72. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
  73. package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
  74. package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
  75. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
  76. package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
  77. package/corpus/templates/content/shared/document-export.ts +1 -1
  78. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
  79. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
  80. package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
  81. package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
  82. package/corpus/templates/design/actions/update-file.ts +9 -2
  83. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
  84. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
  85. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
  86. package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
  87. package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
  88. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
  89. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
  90. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
  91. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
  92. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
  93. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
  94. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
  95. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
  96. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
  97. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
  98. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
  99. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
  100. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
  101. package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
  102. package/corpus/templates/design/app/global.css +29 -3
  103. package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
  104. package/corpus/templates/design/app/i18n-data.ts +239 -1
  105. package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
  106. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
  107. package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
  108. package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
  109. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
  110. package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
  111. package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
  112. package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
  113. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
  114. package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
  115. package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
  116. package/corpus/templates/design/shared/code-layer.ts +209 -9
  117. package/corpus/templates/design/shared/html-content.ts +21 -0
  118. package/corpus/templates/design/shared/pen-path.ts +207 -0
  119. package/corpus/templates/mail/app/lib/utils.ts +2 -2
  120. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
  121. package/corpus/templates/slides/AGENTS.md +3 -2
  122. package/corpus/templates/slides/actions/import-file.ts +26 -17
  123. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
  124. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
  125. package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
  126. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
  127. package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
  128. package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
  129. package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
  130. package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
  131. package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
  132. package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
  133. package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
  134. package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
  135. package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
  136. package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
  137. package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
  138. package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
  139. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  140. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
  141. package/dist/application-state/handlers.js +1 -1
  142. package/dist/application-state/handlers.js.map +1 -1
  143. package/dist/brand-kit/index.d.ts +1 -1
  144. package/dist/brand-kit/index.js +1 -1
  145. package/dist/brand-kit/index.js.map +1 -1
  146. package/dist/cli/skills.d.ts.map +1 -1
  147. package/dist/cli/skills.js +18 -0
  148. package/dist/cli/skills.js.map +1 -1
  149. package/dist/client/AgentPanel.js +1 -1
  150. package/dist/client/AgentPanel.js.map +1 -1
  151. package/dist/client/MultiTabAssistantChat.js +1 -1
  152. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  153. package/dist/client/extensions/portable-extension.d.ts +1 -1
  154. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  155. package/dist/client/extensions/portable-extension.js.map +1 -1
  156. package/dist/code-agents/transcript-normalizer.js +1 -1
  157. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  158. package/dist/collab/awareness.d.ts +2 -2
  159. package/dist/collab/awareness.d.ts.map +1 -1
  160. package/dist/collab/routes.d.ts +1 -1
  161. package/dist/extensions/path.d.ts +1 -1
  162. package/dist/extensions/path.d.ts.map +1 -1
  163. package/dist/extensions/path.js.map +1 -1
  164. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  165. package/dist/integrations/a2a-continuation-processor.js +1 -1
  166. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  167. package/dist/mcp/build-server.d.ts +1 -1
  168. package/dist/mcp/build-server.d.ts.map +1 -1
  169. package/dist/mcp/build-server.js +1 -1
  170. package/dist/mcp/build-server.js.map +1 -1
  171. package/dist/mcp/embed-app.d.ts.map +1 -1
  172. package/dist/mcp/embed-app.js +35 -1
  173. package/dist/mcp/embed-app.js.map +1 -1
  174. package/dist/notifications/routes.d.ts +3 -3
  175. package/dist/observability/routes.d.ts +8 -8
  176. package/dist/org/handlers.js +5 -5
  177. package/dist/org/handlers.js.map +1 -1
  178. package/dist/org/plugin.js +2 -2
  179. package/dist/org/plugin.js.map +1 -1
  180. package/dist/resources/handlers.d.ts +1 -1
  181. package/dist/scripts/call-agent.js +1 -1
  182. package/dist/scripts/call-agent.js.map +1 -1
  183. package/dist/scripts/db/safety.js +1 -1
  184. package/dist/scripts/db/safety.js.map +1 -1
  185. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  186. package/dist/server/auth.d.ts.map +1 -1
  187. package/dist/server/auth.js +3 -0
  188. package/dist/server/auth.js.map +1 -1
  189. package/dist/server/builder-design-systems.d.ts +28 -0
  190. package/dist/server/builder-design-systems.d.ts.map +1 -0
  191. package/dist/server/builder-design-systems.js +217 -0
  192. package/dist/server/builder-design-systems.js.map +1 -0
  193. package/dist/server/core-routes-plugin.d.ts +1 -0
  194. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  195. package/dist/server/core-routes-plugin.js +20 -3
  196. package/dist/server/core-routes-plugin.js.map +1 -1
  197. package/dist/server/index.d.ts +1 -0
  198. package/dist/server/index.d.ts.map +1 -1
  199. package/dist/server/index.js +1 -0
  200. package/dist/server/index.js.map +1 -1
  201. package/dist/styles/agent-native.css +14 -7
  202. package/dist/usage/store.js +2 -2
  203. package/dist/usage/store.js.map +1 -1
  204. package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  205. package/docs/content/locales/ar-SA/template-design.mdx +14 -1
  206. package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  207. package/docs/content/locales/de-DE/template-design.mdx +15 -1
  208. package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  209. package/docs/content/locales/es-ES/template-design.mdx +15 -1
  210. package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  211. package/docs/content/locales/fr-FR/template-design.mdx +15 -1
  212. package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  213. package/docs/content/locales/hi-IN/template-design.mdx +15 -1
  214. package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  215. package/docs/content/locales/ja-JP/template-design.mdx +16 -1
  216. package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  217. package/docs/content/locales/ko-KR/template-design.mdx +14 -1
  218. package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  219. package/docs/content/locales/pt-BR/template-design.mdx +14 -1
  220. package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  221. package/docs/content/locales/zh-CN/template-design.mdx +14 -1
  222. package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  223. package/docs/content/locales/zh-TW/template-design.mdx +14 -1
  224. package/docs/content/skills-guide.mdx +5 -0
  225. package/docs/content/template-design.mdx +14 -1
  226. package/package.json +1 -1
  227. package/corpus/templates/design/actions/import-figma-file.ts +0 -166
  228. package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
@@ -69,9 +69,33 @@ const MCP_DATA_SOURCE_TOKENS = [
69
69
  "stripe",
70
70
  ];
71
71
 
72
+ function normalizeActionToolName(name: string): string {
73
+ return name
74
+ .trim()
75
+ .toLowerCase()
76
+ .replace(/[\s_]+/g, "-")
77
+ .replace(/-+/g, "-");
78
+ }
79
+
80
+ function isToolName(name: string, expected: string): boolean {
81
+ return normalizeActionToolName(name) === expected;
82
+ }
83
+
84
+ function isDataQueryActionName(name: string): boolean {
85
+ return DATA_QUERY_ACTIONS.has(normalizeActionToolName(name));
86
+ }
87
+
88
+ function isCorpusSourceActionName(name: string): boolean {
89
+ return CORPUS_SOURCE_ACTIONS.has(normalizeActionToolName(name));
90
+ }
91
+
92
+ function isCorpusReductionActionName(name: string): boolean {
93
+ return CORPUS_REDUCTION_ACTIONS.has(normalizeActionToolName(name));
94
+ }
95
+
72
96
  function isMcpDataSourceTool(name: string): boolean {
73
- if (!name.startsWith("mcp__")) return false;
74
97
  const normalized = name.toLowerCase();
98
+ if (!normalized.startsWith("mcp__")) return false;
75
99
  return MCP_DATA_SOURCE_TOKENS.some((token) => normalized.includes(token));
76
100
  }
77
101
 
@@ -94,13 +118,13 @@ function getRunCodeBridgeToolNames(content: string | undefined): string[] {
94
118
 
95
119
  function hasRunCodeDataQueryAttempt(content: string | undefined): boolean {
96
120
  return getRunCodeBridgeToolNames(content).some(
97
- (name) => DATA_QUERY_ACTIONS.has(name) || isMcpDataSourceTool(name),
121
+ (name) => isDataQueryActionName(name) || isMcpDataSourceTool(name),
98
122
  );
99
123
  }
100
124
 
101
125
  function hasRunCodeCorpusWorkflowAttempt(content: string | undefined): boolean {
102
126
  return getRunCodeBridgeToolNames(content).some(
103
- (name) => CORPUS_SOURCE_ACTIONS.has(name) || isCorpusCapableMcpTool(name),
127
+ (name) => isCorpusSourceActionName(name) || isCorpusCapableMcpTool(name),
104
128
  );
105
129
  }
106
130
 
@@ -208,7 +232,7 @@ const UNSUPPORTED_RESULT_CLAIM =
208
232
  /(?:\b\d[\d,.]*(?:\.\d+)?\s*(?:%|percent|users?|customers?|accounts?|sessions?|events?|deals?|tickets?|issues?|calls?|messages?|signups?|pageviews?)\b|\$\s*\d|\b(?:data|query|results?)\s+(?:shows?|showed|indicates?|returned|found)\b|\b(?:i found|the top|the bottom|highest|lowest|increased|decreased|grew|declined|converted|churned|retained|averaged|total(?:ed)?|count(?:ed)?)\b)/i;
209
233
 
210
234
  const SAFE_NO_DATA_RESPONSE =
211
- /\b(?:i can't|i cannot|can't retrieve|cannot retrieve|couldn't retrieve|unable to retrieve|don't have access|do not have access|not configured|missing credentials?|need (?:a|the)? ?data source|need to know which source|which source|which data source|clarify|can you|once (?:that'?s|it is) (?:connected|configured|available)|no data source|without a successful|before (?:i|we) can (?:calculate|report|answer|analyze)|i need to query)\b/i;
235
+ /\b(?:i can't|i cannot|can't retrieve|cannot retrieve|couldn't retrieve|unable to retrieve|don't have access|do not have access|not configured|missing credentials?|need (?:a|the)? ?data source|need to know which source|which source|which data source|clarify|can you|once (?:that'?s|it is) (?:connected|configured|available)|no data source|without a successful|query failed|source query failed|sql failed|error running|before (?:i|we) can (?:calculate|report|answer|analyze)|i need to query)\b/i;
212
236
 
213
237
  export function isSafeNoDataAnalyticsResponse(text: string): boolean {
214
238
  const trimmed = text.trim();
@@ -358,10 +382,10 @@ export function hasIncompleteDataEvidence(
358
382
  const name = String(result.name ?? "");
359
383
  if (
360
384
  name &&
361
- !DATA_QUERY_ACTIONS.has(name) &&
385
+ !isDataQueryActionName(name) &&
362
386
  !isMcpDataSourceTool(name) &&
363
- name !== "run-code" &&
364
- name !== "provider-api-request"
387
+ !isToolName(name, "run-code") &&
388
+ !isToolName(name, "provider-api-request")
365
389
  ) {
366
390
  return false;
367
391
  }
@@ -419,11 +443,50 @@ export function hasDataQueryAttempt(
419
443
  if (result.isError) return false;
420
444
  if (isProviderErrorOnlyContent(result.content)) return false;
421
445
  const name = String(result.name ?? "");
422
- if (name === "run-code") return hasRunCodeDataQueryAttempt(result.content);
423
- return DATA_QUERY_ACTIONS.has(name) || isMcpDataSourceTool(name);
446
+ if (isToolName(name, "run-code")) {
447
+ return hasRunCodeDataQueryAttempt(result.content);
448
+ }
449
+ return isDataQueryActionName(name) || isMcpDataSourceTool(name);
424
450
  });
425
451
  }
426
452
 
453
+ function isFailedDataQueryAttempt(result: {
454
+ name?: string;
455
+ isError?: boolean;
456
+ content?: string;
457
+ }): boolean {
458
+ const name = String(result.name ?? "");
459
+ const isDataQuery =
460
+ isDataQueryActionName(name) ||
461
+ isMcpDataSourceTool(name) ||
462
+ (isToolName(name, "run-code") &&
463
+ hasRunCodeDataQueryAttempt(result.content));
464
+ if (!isDataQuery) return false;
465
+ return result.isError === true || isProviderErrorOnlyContent(result.content);
466
+ }
467
+
468
+ function compactToolFailure(content: string | undefined): string {
469
+ const trimmed = String(content ?? "").trim();
470
+ if (!trimmed) return "the tool returned an error result without details";
471
+ const max = 900;
472
+ return trimmed.length > max ? `${trimmed.slice(0, max)}...` : trimmed;
473
+ }
474
+
475
+ export function failedDataQueryAttemptMessage(
476
+ toolResults:
477
+ | Array<{ name?: string; isError?: boolean; content?: string }>
478
+ | undefined,
479
+ ): string | null {
480
+ const failed = (toolResults ?? []).find(isFailedDataQueryAttempt);
481
+ if (!failed) return null;
482
+ const name = String(failed.name ?? "data-source query");
483
+ return (
484
+ `I did try \`${name}\`, but it did not return a successful data result: ` +
485
+ compactToolFailure(failed.content) +
486
+ "\n\nI need to fix and rerun that query, or report that exact source error instead of giving numbers."
487
+ );
488
+ }
489
+
427
490
  export function hasCorpusWorkflowAttempt(
428
491
  toolResults:
429
492
  | Array<{ name?: string; isError?: boolean; content?: string }>
@@ -433,8 +496,8 @@ export function hasCorpusWorkflowAttempt(
433
496
  if (result.isError) return false;
434
497
  if (isProviderErrorOnlyContent(result.content)) return false;
435
498
  const name = String(result.name ?? "");
436
- if (CORPUS_SOURCE_ACTIONS.has(name)) return true;
437
- if (name === "run-code") {
499
+ if (isCorpusSourceActionName(name)) return true;
500
+ if (isToolName(name, "run-code")) {
438
501
  return hasRunCodeCorpusWorkflowAttempt(result.content);
439
502
  }
440
503
 
@@ -453,10 +516,7 @@ export function hasFailedCorpusWorkflowEvidence(
453
516
  ): boolean {
454
517
  return (toolResults ?? []).some((result) => {
455
518
  const name = String(result.name ?? "");
456
- if (
457
- !CORPUS_SOURCE_ACTIONS.has(name) &&
458
- !CORPUS_REDUCTION_ACTIONS.has(name)
459
- ) {
519
+ if (!isCorpusSourceActionName(name) && !isCorpusReductionActionName(name)) {
460
520
  return false;
461
521
  }
462
522
  if (result.isError) return true;
@@ -576,19 +636,20 @@ function actionEvidenceTextForSourceRecords(result: {
576
636
  content?: string;
577
637
  }): string {
578
638
  const name = String(result.name ?? "");
639
+ const normalizedName = normalizeActionToolName(name);
579
640
  const content = String(result.content ?? "");
580
641
  const parsed = tryParseJsonContent(content);
581
642
 
582
- if (name === "provider-corpus-job") {
643
+ if (normalizedName === "provider-corpus-job") {
583
644
  return corpusJobSourceEvidenceText(parsed);
584
645
  }
585
- if (name === "provider-api-request") {
646
+ if (normalizedName === "provider-api-request") {
586
647
  return providerRequestEvidenceText(parsed);
587
648
  }
588
- if (name === "query-staged-dataset") {
649
+ if (normalizedName === "query-staged-dataset") {
589
650
  return queryStagedDatasetEvidenceText(parsed);
590
651
  }
591
- if (name === "gong-calls") {
652
+ if (normalizedName === "gong-calls") {
592
653
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
593
654
  return "";
594
655
  }
@@ -600,7 +661,7 @@ function actionEvidenceTextForSourceRecords(result: {
600
661
  transcripts: record.transcripts,
601
662
  });
602
663
  }
603
- if (name === "run-code") {
664
+ if (normalizedName === "run-code") {
604
665
  return content;
605
666
  }
606
667
  if (isCorpusCapableMcpTool(name)) {
@@ -0,0 +1,79 @@
1
+ import { listFileUploadProviders } from "@agent-native/core/file-upload";
2
+ import {
3
+ resolveHasBuilderPrivateKey,
4
+ runWithRequestContext,
5
+ } from "@agent-native/core/server";
6
+
7
+ /**
8
+ * Session replay chunk bytes are stored through the framework file-upload /
9
+ * private-blob path (Builder.io CDN by default, or an S3-compatible bucket).
10
+ * In production we require a configured provider — storing replay chunks inline
11
+ * in SQL is a local/dev-only fallback. This mirrors the Clips video-storage
12
+ * contract so replay capture fails loudly (and the UI can prompt setup) instead
13
+ * of silently recording empty, unplayable sessions.
14
+ */
15
+ export const REPLAY_STORAGE_SETUP_REQUIRED_REASON =
16
+ "Session replay storage is not connected yet. Connect Builder.io or configure S3-compatible storage to record replays.";
17
+
18
+ function appDatabaseUrl(): string {
19
+ const appName = process.env.APP_NAME?.toUpperCase().replace(/-/g, "_");
20
+ if (appName) {
21
+ const appUrl = process.env[`${appName}_DATABASE_URL`];
22
+ if (appUrl) return appUrl;
23
+ }
24
+ return process.env.DATABASE_URL || process.env.NETLIFY_DATABASE_URL || "";
25
+ }
26
+
27
+ function isLikelyLocalDatabase(): boolean {
28
+ const url = appDatabaseUrl();
29
+ return url === "" || url.startsWith("file:") || !url.includes("://");
30
+ }
31
+
32
+ export function requiresConfiguredReplayStorage(): boolean {
33
+ return process.env.NODE_ENV === "production" || !isLikelyLocalDatabase();
34
+ }
35
+
36
+ export interface ReplayStorageResolveContext {
37
+ userEmail?: string;
38
+ orgId?: string | null;
39
+ }
40
+
41
+ /**
42
+ * Whether a durable upload provider (non-inline) is configured for the given
43
+ * owner/org scope. Iterates registered providers (e.g. S3) first, then falls
44
+ * back to a resolvable Builder.io private key. When a scope is supplied the
45
+ * check runs inside that user/org request context so org-scoped `app_secrets`
46
+ * credentials resolve — exactly the scope the anonymous replay-ingest path
47
+ * lacks on its own.
48
+ */
49
+ export async function hasRequestReplayStorage(
50
+ context?: ReplayStorageResolveContext,
51
+ ): Promise<boolean> {
52
+ const resolve = async () => {
53
+ for (const provider of listFileUploadProviders()) {
54
+ if (provider.id === "builder") continue;
55
+ if (provider.isConfigured()) return true;
56
+ if (provider.isConfiguredForRequest) {
57
+ try {
58
+ if (await provider.isConfiguredForRequest()) return true;
59
+ } catch {
60
+ // Treat a failed scoped lookup as not configured.
61
+ }
62
+ }
63
+ }
64
+
65
+ try {
66
+ return await resolveHasBuilderPrivateKey();
67
+ } catch {
68
+ return false;
69
+ }
70
+ };
71
+
72
+ if (context?.userEmail) {
73
+ return runWithRequestContext(
74
+ { userEmail: context.userEmail, orgId: context.orgId ?? undefined },
75
+ resolve,
76
+ );
77
+ }
78
+ return resolve();
79
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * S3-compatible file upload provider.
3
+ *
4
+ * Works with AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO, Backblaze B2,
5
+ * and any other S3-compatible object storage. Uses SigV4 signing via Web Crypto
6
+ * — no SDK dependency.
7
+ *
8
+ * Env vars (S3_* or R2_* prefix, first found wins):
9
+ * S3_BUCKET | R2_BUCKET — required
10
+ * S3_ACCESS_KEY_ID | R2_ACCESS_KEY_ID — required
11
+ * S3_SECRET_ACCESS_KEY | R2_SECRET_ACCESS_KEY — required
12
+ * S3_ENDPOINT | R2_ENDPOINT — required (e.g. https://s3.us-east-1.amazonaws.com
13
+ * or https://<acct>.r2.cloudflarestorage.com)
14
+ * S3_REGION | R2_REGION — optional, default "auto"
15
+ * S3_PUBLIC_BASE_URL | R2_PUBLIC_BASE_URL — optional (for public read URLs)
16
+ */
17
+
18
+ import type { FileUploadProvider } from "@agent-native/core/file-upload";
19
+ import { resolveSecret } from "@agent-native/core/server";
20
+
21
+ interface S3Config {
22
+ region: string;
23
+ bucket: string;
24
+ accessKeyId: string;
25
+ secretAccessKey: string;
26
+ endpoint: string;
27
+ publicBaseUrl: string | null;
28
+ }
29
+
30
+ function cleanValue(value: string | null | undefined): string | undefined {
31
+ const cleaned = value?.trim();
32
+ return cleaned ? cleaned : undefined;
33
+ }
34
+
35
+ function buildS3Config(values: {
36
+ bucket?: string;
37
+ accessKeyId?: string;
38
+ secretAccessKey?: string;
39
+ endpoint?: string;
40
+ region?: string;
41
+ publicBaseUrl?: string;
42
+ }): S3Config | null {
43
+ const bucket = cleanValue(values.bucket);
44
+ const accessKeyId = cleanValue(values.accessKeyId);
45
+ const secretAccessKey = cleanValue(values.secretAccessKey);
46
+ const endpoint = cleanValue(values.endpoint);
47
+ if (!bucket || !accessKeyId || !secretAccessKey || !endpoint) return null;
48
+ return {
49
+ region: cleanValue(values.region) ?? "auto",
50
+ bucket,
51
+ accessKeyId,
52
+ secretAccessKey,
53
+ endpoint: endpoint.replace(/\/+$/, ""),
54
+ publicBaseUrl:
55
+ cleanValue(values.publicBaseUrl)?.replace(/\/+$/, "") ?? null,
56
+ };
57
+ }
58
+
59
+ function readS3EnvConfig(): S3Config | null {
60
+ const env = process.env;
61
+ return buildS3Config({
62
+ bucket: env.S3_BUCKET || env.R2_BUCKET,
63
+ accessKeyId: env.S3_ACCESS_KEY_ID || env.R2_ACCESS_KEY_ID,
64
+ secretAccessKey: env.S3_SECRET_ACCESS_KEY || env.R2_SECRET_ACCESS_KEY,
65
+ endpoint: env.S3_ENDPOINT || env.R2_ENDPOINT,
66
+ region: env.S3_REGION || env.R2_REGION,
67
+ publicBaseUrl: env.S3_PUBLIC_BASE_URL || env.R2_PUBLIC_BASE_URL,
68
+ });
69
+ }
70
+
71
+ async function resolveS3Secret(primary: string, fallback: string) {
72
+ return (
73
+ cleanValue(await resolveSecret(primary).catch(() => null)) ??
74
+ cleanValue(await resolveSecret(fallback).catch(() => null))
75
+ );
76
+ }
77
+
78
+ async function readS3Config(): Promise<S3Config | null> {
79
+ return buildS3Config({
80
+ bucket: await resolveS3Secret("S3_BUCKET", "R2_BUCKET"),
81
+ accessKeyId: await resolveS3Secret("S3_ACCESS_KEY_ID", "R2_ACCESS_KEY_ID"),
82
+ secretAccessKey: await resolveS3Secret(
83
+ "S3_SECRET_ACCESS_KEY",
84
+ "R2_SECRET_ACCESS_KEY",
85
+ ),
86
+ endpoint: await resolveS3Secret("S3_ENDPOINT", "R2_ENDPOINT"),
87
+ region: await resolveS3Secret("S3_REGION", "R2_REGION"),
88
+ publicBaseUrl: await resolveS3Secret(
89
+ "S3_PUBLIC_BASE_URL",
90
+ "R2_PUBLIC_BASE_URL",
91
+ ),
92
+ });
93
+ }
94
+
95
+ // ── SigV4 helpers (Web Crypto, no SDK) ────────────────────────────────
96
+
97
+ async function hmac(key: ArrayBuffer, msg: string): Promise<ArrayBuffer> {
98
+ const k = await crypto.subtle.importKey(
99
+ "raw",
100
+ key,
101
+ { name: "HMAC", hash: "SHA-256" },
102
+ false,
103
+ ["sign"],
104
+ );
105
+ return crypto.subtle.sign("HMAC", k, new TextEncoder().encode(msg));
106
+ }
107
+
108
+ async function sha256(data: Uint8Array): Promise<string> {
109
+ const ab = new ArrayBuffer(data.byteLength);
110
+ new Uint8Array(ab).set(data);
111
+ const buf = await crypto.subtle.digest("SHA-256", ab);
112
+ return toHex(buf);
113
+ }
114
+
115
+ function toHex(buf: ArrayBuffer): string {
116
+ const bytes = new Uint8Array(buf);
117
+ let out = "";
118
+ for (let i = 0; i < bytes.length; i++) {
119
+ out += bytes[i].toString(16).padStart(2, "0");
120
+ }
121
+ return out;
122
+ }
123
+
124
+ async function deriveSigningKey(
125
+ secret: string,
126
+ dateStamp: string,
127
+ region: string,
128
+ ): Promise<ArrayBuffer> {
129
+ const kSecret = new TextEncoder().encode(`AWS4${secret}`);
130
+ const kDate = await hmac(kSecret.buffer as ArrayBuffer, dateStamp);
131
+ const kRegion = await hmac(kDate, region);
132
+ const kService = await hmac(kRegion, "s3");
133
+ return hmac(kService, "aws4_request");
134
+ }
135
+
136
+ function rfc3986(str: string): string {
137
+ return encodeURIComponent(str).replace(
138
+ /[!'()*]/g,
139
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
140
+ );
141
+ }
142
+
143
+ async function putObject(
144
+ cfg: S3Config,
145
+ key: string,
146
+ body: Uint8Array,
147
+ contentType: string,
148
+ ): Promise<string> {
149
+ const now = new Date();
150
+ const amzDate =
151
+ now
152
+ .toISOString()
153
+ .replace(/[:-]|\.\d{3}/g, "")
154
+ .slice(0, 15) + "Z";
155
+ const dateStamp = amzDate.slice(0, 8);
156
+ const credentialScope = `${dateStamp}/${cfg.region}/s3/aws4_request`;
157
+
158
+ const hostUrl = new URL(cfg.endpoint);
159
+ const host = hostUrl.host;
160
+ const canonicalUri = `/${cfg.bucket}/${key.split("/").map(rfc3986).join("/")}`;
161
+
162
+ const payloadHash = await sha256(body);
163
+
164
+ const headers: Record<string, string> = {
165
+ host,
166
+ "content-type": contentType,
167
+ "x-amz-content-sha256": payloadHash,
168
+ "x-amz-date": amzDate,
169
+ };
170
+
171
+ const signedHeaderKeys = Object.keys(headers).sort();
172
+ const signedHeaders = signedHeaderKeys.join(";");
173
+ const canonicalHeaders =
174
+ signedHeaderKeys.map((k) => `${k}:${headers[k]}`).join("\n") + "\n";
175
+
176
+ const canonicalRequest = [
177
+ "PUT",
178
+ canonicalUri,
179
+ "", // no query string
180
+ canonicalHeaders,
181
+ signedHeaders,
182
+ payloadHash,
183
+ ].join("\n");
184
+
185
+ const crHash = await sha256(new TextEncoder().encode(canonicalRequest));
186
+ const stringToSign = [
187
+ "AWS4-HMAC-SHA256",
188
+ amzDate,
189
+ credentialScope,
190
+ crHash,
191
+ ].join("\n");
192
+
193
+ const signingKey = await deriveSigningKey(
194
+ cfg.secretAccessKey,
195
+ dateStamp,
196
+ cfg.region,
197
+ );
198
+ const signature = toHex(await hmac(signingKey, stringToSign));
199
+
200
+ const authorization =
201
+ `AWS4-HMAC-SHA256 Credential=${cfg.accessKeyId}/${credentialScope}, ` +
202
+ `SignedHeaders=${signedHeaders}, Signature=${signature}`;
203
+
204
+ const url = `${cfg.endpoint}${canonicalUri}`;
205
+ const res = await fetch(url, {
206
+ method: "PUT",
207
+ headers: {
208
+ ...headers,
209
+ Authorization: authorization,
210
+ "Content-Length": String(body.byteLength),
211
+ },
212
+ body: body.buffer.slice(
213
+ body.byteOffset,
214
+ body.byteOffset + body.byteLength,
215
+ ) as BodyInit,
216
+ });
217
+
218
+ if (!res.ok) {
219
+ const text = await res.text().catch(() => "");
220
+ throw new Error(
221
+ `S3 PutObject failed (${res.status}): ${text || res.statusText}`,
222
+ );
223
+ }
224
+
225
+ return cfg.publicBaseUrl
226
+ ? `${cfg.publicBaseUrl}/${key}`
227
+ : `${cfg.endpoint}/${cfg.bucket}/${key}`;
228
+ }
229
+
230
+ // ── Provider ──────────────────────────────────────────────────────────
231
+
232
+ export const s3FileUploadProvider: FileUploadProvider = {
233
+ id: "s3",
234
+ name: "S3-compatible storage",
235
+ isConfigured: () => readS3EnvConfig() !== null,
236
+ isConfiguredForRequest: async () => (await readS3Config()) !== null,
237
+ upload: async ({ data, filename, mimeType }) => {
238
+ const cfg = await readS3Config();
239
+ if (!cfg) throw new Error("S3 credentials are not configured");
240
+
241
+ const ext = filename?.split(".").pop() ?? "bin";
242
+ const stamp = Date.now();
243
+ const rand = Math.random().toString(36).slice(2, 10);
244
+ const objectKey = `replays/${stamp}-${rand}.${ext}`;
245
+ const contentType = mimeType || "application/octet-stream";
246
+
247
+ const bytes =
248
+ data instanceof Uint8Array
249
+ ? data
250
+ : new Uint8Array(data as unknown as ArrayBuffer);
251
+
252
+ const publicUrl = await putObject(cfg, objectKey, bytes, contentType);
253
+ return { url: publicUrl, provider: "s3" };
254
+ },
255
+ };
@@ -8,7 +8,7 @@ import {
8
8
  readPrivateBlob,
9
9
  type PrivateBlobHandle,
10
10
  } from "@agent-native/core/private-blob";
11
- import { recordChange } from "@agent-native/core/server";
11
+ import { recordChange, runWithRequestContext } from "@agent-native/core/server";
12
12
  import {
13
13
  accessFilter,
14
14
  resolveAccess,
@@ -988,6 +988,9 @@ function rowToSessionRecordingSummary(
988
988
  }
989
989
 
990
990
  function hasVisibleSessionRecordingIdentity(row: any): boolean {
991
+ // /sessions intentionally lists signed-in, email-backed recordings only (see
992
+ // analytics CLAUDE.md + the "rejects anonymous recordings" spec). Keep this in
993
+ // sync with replayVisibleIdentityCondition().
991
994
  return Boolean(replayEmail(row.userId) || replayEmail(row.userKey));
992
995
  }
993
996
 
@@ -1050,6 +1053,8 @@ function replayTextContains(column: unknown, query: string) {
1050
1053
  }
1051
1054
 
1052
1055
  function replayVisibleIdentityCondition() {
1056
+ // Email-backed identity only — /sessions lists signed-in recordings (see
1057
+ // analytics CLAUDE.md). Mirror of hasVisibleSessionRecordingIdentity().
1053
1058
  return or(
1054
1059
  replayTextContains(schema.sessionRecordings.userId, "@"),
1055
1060
  replayTextContains(schema.sessionRecordings.userKey, "@"),
@@ -1140,7 +1145,12 @@ export async function recordSessionReplayChunks(
1140
1145
  lastIngestedAt: ingestedAt,
1141
1146
  ownerEmail: key.ownerEmail,
1142
1147
  orgId: key.orgId,
1143
- visibility: "private",
1148
+ // Session replay is an org-analytics surface: a recording captured under
1149
+ // an org-scoped analytics key must be visible to everyone in that org
1150
+ // (via accessFilter's "org" branch), not only the key owner. Without an
1151
+ // org we fall back to owner-private. This is what makes /sessions show
1152
+ // recordings to teammates instead of only the single key owner.
1153
+ visibility: key.orgId ? "org" : "private",
1144
1154
  })
1145
1155
  .onConflictDoNothing();
1146
1156
 
@@ -1204,12 +1214,23 @@ export async function recordSessionReplayChunks(
1204
1214
  413,
1205
1215
  );
1206
1216
  }
1207
- const chunk = await storeReplayChunkBlob(rawChunk, {
1208
- publicKeyId: key.id,
1209
- recordingId: recording.id,
1210
- ownerEmail: key.ownerEmail,
1211
- orgId: key.orgId,
1212
- });
1217
+ // Replay ingest is anonymous + cross-origin (no session), so blob storage
1218
+ // would otherwise have no request context and `resolveBuilderPrivateKey()`
1219
+ // (and any S3 provider's scoped-secret lookup) would resolve nothing —
1220
+ // every chunk upload then 503s and recordings persist as empty shells.
1221
+ // Run the upload in the public key owner's user/org scope so the org's
1222
+ // connected Builder (or S3) credential in `app_secrets` resolves. Mirrors
1223
+ // the resources upload precedent (core resources/handlers.ts).
1224
+ const chunk = await runWithRequestContext(
1225
+ { userEmail: key.ownerEmail, orgId: key.orgId ?? undefined },
1226
+ () =>
1227
+ storeReplayChunkBlob(rawChunk, {
1228
+ publicKeyId: key.id,
1229
+ recordingId: recording.id,
1230
+ ownerEmail: key.ownerEmail,
1231
+ orgId: key.orgId,
1232
+ }),
1233
+ );
1213
1234
  if (rawChunk.storageKind !== "blob" && chunk.storageKind === "blob") {
1214
1235
  const ref = decodeReplayBlobRef(chunk.storageRef);
1215
1236
  if (ref) uploadedBlobHandles.push(ref.handle);
@@ -9,6 +9,7 @@ import {
9
9
  import actionsRegistry from "../../.generated/actions-registry.js";
10
10
  import { renderDataDictionary } from "../lib/data-dictionary-context";
11
11
  import {
12
+ failedDataQueryAttemptMessage,
12
13
  hasExplicitPartialDisclosure,
13
14
  hasFailedCorpusWorkflowEvidence,
14
15
  hasDataQueryAttempt,
@@ -39,6 +40,7 @@ const INITIAL_TOOL_NAMES = [
39
40
  "get-sql-dashboard",
40
41
  "mutate-dashboard",
41
42
  "generate-chart",
43
+ "query-agent-native-analytics",
42
44
  "bigquery",
43
45
  "search-bigquery-schema",
44
46
  "bigquery-table-info",
@@ -139,6 +141,13 @@ function realDataFinalGuard(context: AgentLoopFinalResponseGuardContext) {
139
141
  }
140
142
  if (hasDataQueryAttempt(context.toolResults)) return null;
141
143
  if (isSafeNoDataAnalyticsResponse(context.text)) return null;
144
+ const failedQueryMessage = failedDataQueryAttemptMessage(context.toolResults);
145
+ if (failedQueryMessage) {
146
+ return {
147
+ retryMessage: failedQueryMessage,
148
+ fallbackMessage: failedQueryMessage,
149
+ };
150
+ }
142
151
 
143
152
  return {
144
153
  retryMessage:
@@ -175,8 +184,8 @@ export default createAgentChatPlugin({
175
184
  "SURFACE DIFFERENTIATION — You are the analytics assistant for definitions, deep-dive analysis, and action. For questions about what a metric, model, or table means, use the Data Dictionary and configured schema tools first. For trends, comparisons, anomalies, current data, or anything that requires querying live data, answer directly in chat with the relevant provider query, dashboard analysis, and inline charts when useful. " +
176
185
  "DASHBOARD CREATION RULE — You may create dashboards, analyses, SQL panels, or other resources only when the user explicitly asks you to (e.g. 'build me a dashboard for...', 'create a new analysis', 'add a chart for...'). Never create any resource proactively during research, trend analysis, or answering questions. If you think a dashboard would be useful, suggest it and wait for explicit confirmation before creating anything. Never add new items to the sidebar or modify existing dashboards without an explicit user directive. " +
177
186
  "DASHBOARD MUTATION RULE — For dashboard edits, default to `mutate-dashboard` with the typed `dashboard.*` script API so the main payload is a string and avoids native-array serialization traps. It can move panels by id, edit titles/SQL/config, insert, duplicate, remove, and patch dashboard fields in one atomic save. The script API is constrained: no variables/imports/loops/functions, only JSON-compatible arguments on documented dashboard methods. Do not count shifting `/panels/<index>` positions for ordinary dashboard edits unless the user specifically asks for low-level JSON-pointer operations. " +
178
- "DASHBOARD READ RULE — `get-sql-dashboard` is compact by default: use its `panels` summaries plus `layout.panelOrder` and `layout.firstPanelIds` for orientation and verification. Pass `includeConfig: true` only when you truly need full panel SQL/config. " +
179
- 'DASHBOARD REORDER RULE — For simple chart/section moves, use `mutate-dashboard` code such as `dashboard.panels(["panel-a","panel-b"]).moveToTop();`. Never count shifting `/panels/<index>` positions for ordinary \'move this chart\' requests. Use `get-sql-dashboard.layout.firstPanelIds` or `mutate-dashboard.panelOrder` as proof of placement. ' +
187
+ "DASHBOARD READ RULE — `get-sql-dashboard` is compact by default: use its `panels` summaries plus `layout.panelOrder`, `layout.firstPanelIds`, and `layout.groups[].rows[].rowNumber/panelIds` for orientation and verification. Pass `includeConfig: true` only when you truly need full panel SQL/config. " +
188
+ 'DASHBOARD REORDER RULE — For simple chart/section moves, use `mutate-dashboard` code such as `dashboard.panels(["panel-a","panel-b"]).moveToTop();`. For visible placement requests like "second row" or "next to return rates", use row-aware placement such as `dashboard.insertPanel({...}).nextTo("retention-over-time")`, `.atRow(2)`, or `dashboard.panel("panel-a").moveNextTo("panel-b")`; these keep panels in the intended rendered row and expand/rebalance that row when needed. Never count shifting `/panels/<index>` positions for ordinary \'move this chart\' requests. Use `get-sql-dashboard.layout.groups[].rows` as proof of visible row placement, not only flat `panelOrder`. ' +
180
189
  "Use configured data sources and actions only. Call `data-source-status` when you need to know which providers are connected, and treat provider actions as unavailable for analysis if they return missing credentials, permission, syntax, quota, or network errors. " +
181
190
  "The built-in `demo` dashboard source is a demo-environment Prometheus source reserved for the Node Exporter demo. It must never satisfy REAL_DATA_REQUIRED or be cited as user analytics evidence unless the user explicitly asks to inspect the demo dashboard. " +
182
191
  "When the user names a provider or tool such as Jira, Pylon, HubSpot, Gong, Slack, Sentry, GA4, or BigQuery, that named source is authoritative for the turn: use that provider's real tool/API surface, not a warehouse or different-provider substitute, unless the user explicitly asks for the copy/fallback. For bounded lookups where a first-class action fully models the requested source, object, filter, and pagination need, that shortcut is fine. For broad provider searches, cross-source joins, corpus-wide counts, exact cohort coverage, or any answer where absence matters, do not start and stop with shortcuts; use the broad provider API/MCP and corpus/code workflow as the primary path. " +