@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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 (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. package/src/modules/notifications/setup.ts +11 -0
@@ -153,12 +153,19 @@ export async function login(page: Page, role: Role = 'admin'): Promise<void> {
153
153
  const apiLoginForm = new URLSearchParams();
154
154
  apiLoginForm.set('email', creds.email);
155
155
  apiLoginForm.set('password', creds.password);
156
- const apiLoginResponse = await page.request.post('/api/auth/login', {
157
- headers: {
158
- 'content-type': 'application/x-www-form-urlencoded',
159
- },
160
- data: apiLoginForm.toString(),
161
- }).catch(() => null);
156
+ // Retry-on-429 against the auth rate limit (5/60s per email). Capped
157
+ // exponential backoff: 1s, 2s, 4s — worst-case ~7s.
158
+ let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;
159
+ for (let retry = 0; retry < 4; retry += 1) {
160
+ apiLoginResponse = await page.request.post('/api/auth/login', {
161
+ headers: {
162
+ 'content-type': 'application/x-www-form-urlencoded',
163
+ },
164
+ data: apiLoginForm.toString(),
165
+ }).catch(() => null);
166
+ if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;
167
+ await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));
168
+ }
162
169
  if (apiLoginResponse?.ok()) {
163
170
  const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;
164
171
  const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;
@@ -53,11 +53,14 @@ type RoleSnapshots = {
53
53
 
54
54
  const RESERVED_ROLE_NAMES = new Set(['superadmin', 'admin'])
55
55
 
56
- function assertRoleNameAllowed(name: string | undefined | null) {
57
- if (typeof name !== 'string') return
56
+ function isReservedRoleName(name: string | undefined | null): boolean {
57
+ if (typeof name !== 'string') return false
58
58
  const normalized = name.trim().toLowerCase()
59
- if (!normalized) return
60
- if (RESERVED_ROLE_NAMES.has(normalized)) {
59
+ return normalized.length > 0 && RESERVED_ROLE_NAMES.has(normalized)
60
+ }
61
+
62
+ function assertRoleNameAllowed(name: string | undefined | null) {
63
+ if (isReservedRoleName(name)) {
61
64
  throw new CrudHttpError(400, { error: 'Role name is reserved' })
62
65
  }
63
66
  }
@@ -220,22 +223,17 @@ const updateRoleCommand: CommandHandler<Record<string, unknown>, Role> = {
220
223
  async execute(rawInput, ctx) {
221
224
  const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput)
222
225
  const em = (ctx.container.resolve('em') as EntityManager)
226
+ const current = await findOneWithDecryption(em, Role, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null })
227
+ if (!current) throw new CrudHttpError(404, { error: 'Role not found' })
223
228
  if (parsed.name !== undefined) {
224
- assertRoleNameAllowed(parsed.name)
225
- const current = await findOneWithDecryption(em, Role, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null })
226
- if (!current) throw new CrudHttpError(404, { error: 'Role not found' })
227
- assertRoleNameAllowed(current.name)
228
229
  const nextName = parsed.name
230
+ if (nextName !== current.name) assertRoleNameAllowed(nextName)
229
231
  if (nextName !== current.name) {
230
232
  const assignments = await em.count(UserRole, { role: current, deletedAt: null })
231
233
  if (assignments > 0) {
232
234
  throw new CrudHttpError(400, { error: 'Role name cannot be changed while users are assigned' })
233
235
  }
234
236
  }
235
- } else {
236
- const current = await findOneWithDecryption(em, Role, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null })
237
- if (!current) throw new CrudHttpError(404, { error: 'Role not found' })
238
- assertRoleNameAllowed(current.name)
239
237
  }
240
238
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
241
239
  const role = await de.updateOrmEntity({
@@ -64,3 +64,14 @@ The default pipeline emits `catalog.pricing.resolve.before|after` events.
64
64
  Key events follow the standard pattern in `events.ts`:
65
65
  - `catalog.product.created/updated/deleted` — CRUD events
66
66
  - `catalog.pricing.resolve.before/after` — pricing lifecycle (excluded from workflow triggers)
67
+
68
+ ## AI Agents in This Module
69
+
70
+ Two typed AI agents ship from `ai-agents.ts`. See `/framework/ai-assistant/agents` for the full guide. Copy `packages/core/src/modules/customers/ai-agents.ts` + `ai-tools.ts` first; use this module's agents as the catalog-specific reference.
71
+
72
+ | Agent ID | Mode | Policy | Purpose |
73
+ |----------|------|--------|---------|
74
+ | `catalog.catalog_assistant` | chat | `read-only` | General operator explorer for products, categories, variants, prices, offers, product media, tags, option schemas, and unit conversions via the base catalog tool pack + general-purpose packs. |
75
+ | `catalog.merchandising_assistant` | chat | `read-only` (mutation-capable via per-tenant override unlocking `catalog.update_product`, `catalog.bulk_update_products`, `catalog.apply_attribute_extraction`, `catalog.update_product_media_descriptions`) | D18 demo agent: proposes descriptions, attribute extractions, title variants, and price adjustments for the current selection on the products list page. |
76
+
77
+ The merchandising assistant is the Phase 2 D18 demo. `<AiChat agent="catalog.merchandising_assistant" />` is injected via `MerchandisingAssistantSheet.tsx` on `/backend/catalog/catalog/products` (see `packages/core/src/modules/catalog/backend/catalog/products/page.tsx`). Mutation-capable authoring tools route through `prepareMutation` + the approval-card contract; confirmed batches refresh the DataTable via `catalog.product.updated` events on the DOM event bridge.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Page-context hydration helpers for the catalog agents
3
+ * (Phase 3 WS-A, Step 5.2).
4
+ *
5
+ * Two flavors:
6
+ *
7
+ * 1. `hydrateCatalogAssistantContext` — `catalog.catalog_assistant`. Loads
8
+ * a lightweight product summary for a single UUID (`catalog.product`),
9
+ * or a batch of up to 10 summaries when the request carries a
10
+ * comma-separated UUID list keyed as `catalog.products.list`.
11
+ *
12
+ * 2. `hydrateMerchandisingAssistantContext` —
13
+ * `catalog.merchandising_assistant`. Loads the full
14
+ * `catalog.get_product_bundle` aggregate for a single product, or a
15
+ * capped-at-10 selection via `catalog.list_selected_products`. When
16
+ * the request carries the products-list page view, the incoming
17
+ * `pageContext.extra.filter` is pretty-printed into the context block
18
+ * so the agent can reason about the narrowed set even when no
19
+ * selection is active.
20
+ *
21
+ * Both helpers route every read through an existing tool-pack handler
22
+ * (Step 3.10 base pack + Step 3.11 D18 pack) so the agent-reachable
23
+ * surface and the hydration surface stay in lock-step. Tenant + org
24
+ * scope is enforced by the tool handlers themselves; cross-tenant ids
25
+ * surface as `{ found: false }` / `missingIds`, which we translate to a
26
+ * silent null return (the runtime then proceeds without hydration).
27
+ *
28
+ * Error swallowing is required by the Step 3.2 runtime contract — a
29
+ * hydration fault MUST NEVER break the chat request.
30
+ */
31
+ import type { AwilixContainer } from 'awilix'
32
+ import catalogAiTools from './ai-tools'
33
+ import type {
34
+ CatalogAiToolDefinition,
35
+ CatalogToolContext,
36
+ } from './ai-tools/types'
37
+
38
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
39
+
40
+ const SELECTION_CAP = 10
41
+
42
+ function isUuid(value: unknown): value is string {
43
+ return typeof value === 'string' && UUID_REGEX.test(value)
44
+ }
45
+
46
+ function parseSelectionIds(raw: string): string[] {
47
+ if (!raw) return []
48
+ const unique = new Set<string>()
49
+ for (const token of raw.split(',')) {
50
+ const trimmed = token.trim()
51
+ if (isUuid(trimmed)) unique.add(trimmed)
52
+ if (unique.size >= SELECTION_CAP) break
53
+ }
54
+ return Array.from(unique)
55
+ }
56
+
57
+ function findTool(name: string): CatalogAiToolDefinition | null {
58
+ return (
59
+ (catalogAiTools as CatalogAiToolDefinition[]).find((tool) => tool.name === name) ?? null
60
+ )
61
+ }
62
+
63
+ function buildToolContext(
64
+ container: AwilixContainer,
65
+ tenantId: string,
66
+ organizationId: string | null,
67
+ ): CatalogToolContext {
68
+ return {
69
+ tenantId,
70
+ organizationId,
71
+ userId: null,
72
+ container,
73
+ userFeatures: [],
74
+ isSuperAdmin: true,
75
+ apiKeySecret: undefined,
76
+ sessionId: undefined,
77
+ }
78
+ }
79
+
80
+ function renderContextBlock(label: string, payload: unknown): string {
81
+ return `## Page context — ${label}\n${JSON.stringify(payload, null, 2)}`
82
+ }
83
+
84
+ export interface HydrateCatalogContextInput {
85
+ entityType: string
86
+ recordId: string
87
+ container: AwilixContainer
88
+ tenantId: string | null
89
+ organizationId: string | null
90
+ }
91
+
92
+ const SINGLE_PRODUCT_ENTITY_TYPES = new Set([
93
+ 'product',
94
+ 'catalog.product',
95
+ 'catalog:catalog_product',
96
+ ])
97
+
98
+ const PRODUCTS_LIST_ENTITY_TYPES = new Set([
99
+ 'catalog.products.list',
100
+ 'catalog.products.selection',
101
+ 'products.list',
102
+ 'products.selection',
103
+ ])
104
+
105
+ async function invokeTool(
106
+ toolName: string,
107
+ args: Record<string, unknown>,
108
+ toolContext: CatalogToolContext,
109
+ reasonPrefix: string,
110
+ ): Promise<unknown | null> {
111
+ const tool = findTool(toolName)
112
+ if (!tool) {
113
+ console.warn(`[${reasonPrefix}] resolvePageContext: tool "${toolName}" not registered`)
114
+ return null
115
+ }
116
+ try {
117
+ const result = await tool.handler(args as never, toolContext)
118
+ return result ?? null
119
+ } catch (error) {
120
+ console.warn(
121
+ `[${reasonPrefix}] resolvePageContext: tool "${toolName}" failed (reason="hydration_error"); skipping`,
122
+ error instanceof Error ? error.message : error,
123
+ )
124
+ return null
125
+ }
126
+ }
127
+
128
+ // -----------------------------------------------------------------------------
129
+ // catalog.catalog_assistant hydration
130
+ // -----------------------------------------------------------------------------
131
+
132
+ export async function hydrateCatalogAssistantContext(
133
+ input: HydrateCatalogContextInput,
134
+ ): Promise<string | null> {
135
+ const tenantId = input.tenantId
136
+ if (!tenantId) return null
137
+ const entityType = input.entityType.trim().toLowerCase()
138
+ if (!entityType) return null
139
+ const toolContext = buildToolContext(input.container, tenantId, input.organizationId)
140
+
141
+ if (SINGLE_PRODUCT_ENTITY_TYPES.has(entityType)) {
142
+ if (!isUuid(input.recordId)) return null
143
+ const result = await invokeTool(
144
+ 'catalog.get_product',
145
+ { productId: input.recordId },
146
+ toolContext,
147
+ 'catalog.catalog_assistant',
148
+ )
149
+ if (!result || typeof result !== 'object') return null
150
+ if ((result as { found?: boolean }).found === false) return null
151
+ return renderContextBlock(`Product ${input.recordId}`, result)
152
+ }
153
+
154
+ if (PRODUCTS_LIST_ENTITY_TYPES.has(entityType)) {
155
+ const ids = parseSelectionIds(input.recordId)
156
+ if (ids.length === 0) return null
157
+ // Reuse the D18 merchandising bundle tool — its result carries
158
+ // summaries inside full bundles. For the base catalog_assistant we
159
+ // keep the payload lightweight by projecting each bundle onto the
160
+ // summary subset the agent cares about.
161
+ const result = await invokeTool(
162
+ 'catalog.list_selected_products',
163
+ { productIds: ids },
164
+ toolContext,
165
+ 'catalog.catalog_assistant',
166
+ )
167
+ if (!result || typeof result !== 'object') return null
168
+ const { items, missingIds } = result as {
169
+ items?: Array<{ product?: unknown }>
170
+ missingIds?: string[]
171
+ }
172
+ const summaries = Array.isArray(items)
173
+ ? items
174
+ .map((item) => (item && typeof item === 'object' ? (item as { product?: unknown }).product ?? null : null))
175
+ .filter((value) => value !== null)
176
+ : []
177
+ if (summaries.length === 0) return null
178
+ return renderContextBlock(
179
+ `Products selection (${summaries.length} of ${ids.length})`,
180
+ { items: summaries, missingIds: missingIds ?? [] },
181
+ )
182
+ }
183
+
184
+ return null
185
+ }
186
+
187
+ // -----------------------------------------------------------------------------
188
+ // catalog.merchandising_assistant hydration
189
+ // -----------------------------------------------------------------------------
190
+
191
+ export async function hydrateMerchandisingAssistantContext(
192
+ input: HydrateCatalogContextInput,
193
+ ): Promise<string | null> {
194
+ const tenantId = input.tenantId
195
+ if (!tenantId) return null
196
+ const entityType = input.entityType.trim().toLowerCase()
197
+ if (!entityType) return null
198
+ const toolContext = buildToolContext(input.container, tenantId, input.organizationId)
199
+
200
+ if (SINGLE_PRODUCT_ENTITY_TYPES.has(entityType)) {
201
+ if (!isUuid(input.recordId)) return null
202
+ const result = await invokeTool(
203
+ 'catalog.get_product_bundle',
204
+ { productId: input.recordId },
205
+ toolContext,
206
+ 'catalog.merchandising_assistant',
207
+ )
208
+ if (!result || typeof result !== 'object') return null
209
+ if ((result as { found?: boolean }).found === false) return null
210
+ return renderContextBlock(`Product bundle ${input.recordId}`, result)
211
+ }
212
+
213
+ if (PRODUCTS_LIST_ENTITY_TYPES.has(entityType)) {
214
+ const ids = parseSelectionIds(input.recordId)
215
+ if (ids.length === 0) return null
216
+ const result = await invokeTool(
217
+ 'catalog.list_selected_products',
218
+ { productIds: ids },
219
+ toolContext,
220
+ 'catalog.merchandising_assistant',
221
+ )
222
+ if (!result || typeof result !== 'object') return null
223
+ const { items, missingIds } = result as {
224
+ items?: unknown[]
225
+ missingIds?: string[]
226
+ }
227
+ const bundles = Array.isArray(items) ? items : []
228
+ if (bundles.length === 0) return null
229
+ return renderContextBlock(
230
+ `Products selection bundles (${bundles.length} of ${ids.length})`,
231
+ {
232
+ items: bundles,
233
+ missingIds: missingIds ?? [],
234
+ },
235
+ )
236
+ }
237
+
238
+ return null
239
+ }