@open-mercato/ai-assistant 0.6.2-develop.3406.1.2b403f40da → 0.6.2-develop.3446.1.bd060c6017

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 (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +8 -1
  3. package/build.mjs +1 -0
  4. package/dist/frontend/components/AiChatButton.js +1 -1
  5. package/dist/frontend/components/AiChatButton.js.map +2 -2
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +16 -5
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +2 -2
  8. package/dist/modules/ai_assistant/ai-tools/meta-pack.js +58 -1
  9. package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +2 -2
  10. package/dist/modules/ai_assistant/api/ai/agents/route.js +2 -1
  11. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
  12. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  13. package/dist/modules/ai_assistant/i18n/de.json +7 -1
  14. package/dist/modules/ai_assistant/i18n/en.json +7 -1
  15. package/dist/modules/ai_assistant/i18n/es.json +7 -1
  16. package/dist/modules/ai_assistant/i18n/pl.json +7 -1
  17. package/dist/modules/ai_assistant/lib/agent-registry.js +26 -6
  18. package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
  19. package/dist/modules/ai_assistant/lib/agent-runtime.js +21 -8
  20. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
  21. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  22. package/dist/modules/ai_assistant/lib/pending-action-types.js.map +2 -2
  23. package/dist/modules/ai_assistant/lib/prepare-mutation.js +16 -6
  24. package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +2 -2
  25. package/dist/modules/ai_assistant/lib/task-plan-labels.js +95 -0
  26. package/dist/modules/ai_assistant/lib/task-plan-labels.js.map +7 -0
  27. package/dist/modules/ai_assistant/lib/task-plan-stream.js +349 -0
  28. package/dist/modules/ai_assistant/lib/task-plan-stream.js.map +7 -0
  29. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +3 -0
  30. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +2 -2
  31. package/package.json +6 -6
  32. package/src/frontend/components/AiChatButton.tsx +1 -1
  33. package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +20 -8
  34. package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +60 -4
  35. package/src/modules/ai_assistant/ai-tools/meta-pack.ts +79 -2
  36. package/src/modules/ai_assistant/api/ai/agents/route.ts +2 -1
  37. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1 -0
  38. package/src/modules/ai_assistant/i18n/de.json +7 -1
  39. package/src/modules/ai_assistant/i18n/en.json +7 -1
  40. package/src/modules/ai_assistant/i18n/es.json +7 -1
  41. package/src/modules/ai_assistant/i18n/pl.json +7 -1
  42. package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +60 -0
  43. package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +4 -0
  44. package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +43 -0
  45. package/src/modules/ai_assistant/lib/__tests__/task-plan-stream.test.ts +375 -0
  46. package/src/modules/ai_assistant/lib/agent-registry.ts +36 -5
  47. package/src/modules/ai_assistant/lib/agent-runtime.ts +26 -8
  48. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +14 -0
  49. package/src/modules/ai_assistant/lib/pending-action-types.ts +4 -1
  50. package/src/modules/ai_assistant/lib/prepare-mutation.ts +17 -5
  51. package/src/modules/ai_assistant/lib/task-plan-labels.ts +112 -0
  52. package/src/modules/ai_assistant/lib/task-plan-stream.ts +463 -0
  53. package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +3 -0
  54. package/src/modules/ai_assistant/lib/types.ts +16 -0
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Covers `list_agents` empty-registry graceful case, RBAC filtering,
5
5
  * super-admin bypass, `describe_agent` not-found / forbidden / happy,
6
- * and the `output.schema` JSON-Schema fallback.
6
+ * `update_task_plan` sanitization, and the `output.schema` JSON-Schema
7
+ * fallback.
7
8
  */
8
9
  import { z } from 'zod'
9
10
  import type { AiAgentDefinition } from '../../lib/ai-agent-definition'
@@ -148,6 +149,7 @@ describe('meta.describe_agent', () => {
148
149
  readOnly: true,
149
150
  mutationPolicy: 'read-only',
150
151
  acceptedMediaTypes: ['image', 'pdf'],
152
+ taskPlan: { enabled: true },
151
153
  maxSteps: 6,
152
154
  output: { schemaName: 'MerchProposal', schema, mode: 'generate' },
153
155
  keywords: ['catalog', 'merch'],
@@ -159,9 +161,14 @@ describe('meta.describe_agent', () => {
159
161
  const agent = result.agent as Record<string, unknown>
160
162
  expect(agent.id).toBe('catalog.merch')
161
163
  expect(agent.executionMode).toBe('object')
162
- expect(agent.allowedTools).toEqual(['search.hybrid_search', 'catalog.get_product_bundle'])
164
+ expect(agent.allowedTools).toEqual([
165
+ 'search.hybrid_search',
166
+ 'catalog.get_product_bundle',
167
+ 'meta.update_task_plan',
168
+ ])
163
169
  expect(agent.readOnly).toBe(true)
164
170
  expect(agent.acceptedMediaTypes).toEqual(['image', 'pdf'])
171
+ expect(agent.taskPlan).toEqual({ enabled: true })
165
172
  const output = agent.output as Record<string, unknown>
166
173
  expect(output.schemaName).toBe('MerchProposal')
167
174
  expect(output.jsonSchema).toBeDefined()
@@ -206,10 +213,59 @@ describe('meta.describe_agent', () => {
206
213
  })
207
214
  })
208
215
 
216
+ describe('meta.update_task_plan', () => {
217
+ const tool = findTool('meta.update_task_plan')
218
+
219
+ it('returns sanitized user-visible task labels', async () => {
220
+ const ctx = makeCtx()
221
+ const result = (await tool.handler(
222
+ {
223
+ tasks: [
224
+ {
225
+ id: ' step one ',
226
+ label: ' Search catalog products ',
227
+ detail: ' Use product search ',
228
+ toolName: 'catalog__search_products',
229
+ },
230
+ ],
231
+ },
232
+ ctx as any,
233
+ )) as Record<string, unknown>
234
+
235
+ expect(result.ok).toBe(true)
236
+ expect(result.accepted).toBe(1)
237
+ expect(result.tasks).toEqual([
238
+ {
239
+ id: 'step-one',
240
+ label: 'Search catalog products',
241
+ detail: 'Use product search',
242
+ toolName: 'catalog.search_products',
243
+ },
244
+ ])
245
+ })
246
+
247
+ it('rejects hidden-reasoning-like labels at schema validation', () => {
248
+ expect(() =>
249
+ tool.inputSchema.parse({
250
+ tasks: [
251
+ {
252
+ label: '<thinking>First I will inspect private chain of thought</thinking>',
253
+ },
254
+ ],
255
+ }),
256
+ ).toThrow(/private reasoning|Task-plan labels/i)
257
+ })
258
+
259
+ it('does not expose mutation capability', () => {
260
+ expect(tool.isMutation).not.toBe(true)
261
+ expect(tool.requiredFeatures).toEqual(['ai_assistant.view'])
262
+ })
263
+ })
264
+
209
265
  describe('meta-pack tool surface', () => {
210
- it('exports the two read-only meta tools', () => {
266
+ it('exports the read-only meta tools', () => {
211
267
  const names = metaAiTools.map((tool) => tool.name)
212
- expect(names).toEqual(['meta.list_agents', 'meta.describe_agent'])
268
+ expect(names).toEqual(['meta.list_agents', 'meta.describe_agent', 'meta.update_task_plan'])
213
269
  for (const tool of metaAiTools) {
214
270
  expect(tool.isMutation).not.toBe(true)
215
271
  expect(tool.requiredFeatures).toEqual(['ai_assistant.view'])
@@ -12,7 +12,15 @@ import type { AiAgentDefinition } from '../lib/ai-agent-definition'
12
12
  import { listAgents, getAgent } from '../lib/agent-registry'
13
13
  import { hasRequiredFeatures } from '../lib/auth'
14
14
  import { defineAiTool } from '../lib/ai-tool-definition'
15
- import type { AiToolDefinition } from '../lib/types'
15
+ import {
16
+ TASK_PLAN_DETAIL_MAX_CHARS,
17
+ TASK_PLAN_ID_MAX_CHARS,
18
+ TASK_PLAN_LABEL_MAX_CHARS,
19
+ TASK_PLAN_MAX_TASKS,
20
+ TASK_PLAN_TOOL_NAME_MAX_CHARS,
21
+ looksLikeHiddenReasoning,
22
+ sanitizeAgentTaskPlanInput,
23
+ } from '../lib/task-plan-labels'
16
24
 
17
25
  function summarizeAgent(agent: AiAgentDefinition): Record<string, unknown> {
18
26
  return {
@@ -30,6 +38,7 @@ function summarizeAgent(agent: AiAgentDefinition): Record<string, unknown> {
30
38
  domain: agent.domain ?? null,
31
39
  keywords: agent.keywords ?? [],
32
40
  suggestions: agent.suggestions ?? [],
41
+ taskPlan: agent.taskPlan ?? { enabled: false },
33
42
  dataCapabilities: agent.dataCapabilities ?? null,
34
43
  hasOutputSchema: Boolean(agent.output),
35
44
  hasPageContextResolver: typeof agent.resolvePageContext === 'function',
@@ -135,6 +144,74 @@ const describeAgentTool = defineAiTool({
135
144
  },
136
145
  })
137
146
 
138
- export const metaAiTools: AiToolDefinition<any, any>[] = [listAgentsTool, describeAgentTool]
147
+ const visibleTaskLabel = z
148
+ .string()
149
+ .min(1)
150
+ .max(TASK_PLAN_LABEL_MAX_CHARS)
151
+ .superRefine((value, ctx) => {
152
+ if (!looksLikeHiddenReasoning(value)) return
153
+ ctx.addIssue({
154
+ code: 'custom',
155
+ message: 'Task-plan labels are user-visible UI copy and must not include private reasoning.',
156
+ })
157
+ })
158
+
159
+ const visibleTaskDetail = z
160
+ .string()
161
+ .max(TASK_PLAN_DETAIL_MAX_CHARS)
162
+ .optional()
163
+ .superRefine((value, ctx) => {
164
+ if (!value || !looksLikeHiddenReasoning(value)) return
165
+ ctx.addIssue({
166
+ code: 'custom',
167
+ message: 'Task-plan details must not include private reasoning.',
168
+ })
169
+ })
170
+
171
+ const updateTaskPlanInput = z.object({
172
+ tasks: z
173
+ .array(
174
+ z.object({
175
+ id: z.string().min(1).max(TASK_PLAN_ID_MAX_CHARS).optional(),
176
+ label: visibleTaskLabel.describe('Concise user-visible step label. Do not include private reasoning.'),
177
+ detail: visibleTaskDetail.describe('Optional short visible detail, not private reasoning.'),
178
+ toolName: z
179
+ .string()
180
+ .min(1)
181
+ .max(TASK_PLAN_TOOL_NAME_MAX_CHARS)
182
+ .optional()
183
+ .describe('Optional whitelisted tool name that this planned step maps to.'),
184
+ }),
185
+ )
186
+ .min(1)
187
+ .max(TASK_PLAN_MAX_TASKS),
188
+ })
189
+
190
+ const updateTaskPlanTool = defineAiTool({
191
+ name: 'meta.update_task_plan',
192
+ displayName: 'Update task plan',
193
+ description:
194
+ 'Set the concise user-visible task plan for this assistant turn before calling domain tools. Labels are progress UI, not hidden reasoning.',
195
+ inputSchema: updateTaskPlanInput,
196
+ requiredFeatures: ['ai_assistant.view'],
197
+ tags: ['read', 'meta', 'task-plan'],
198
+ isMutation: false,
199
+ handler: async (rawInput) => {
200
+ const input = updateTaskPlanInput.parse(rawInput)
201
+ const sanitized = sanitizeAgentTaskPlanInput(input)
202
+ return {
203
+ ok: sanitized.tasks.length > 0,
204
+ tasks: sanitized.tasks,
205
+ accepted: sanitized.tasks.length,
206
+ dropped: input.tasks.length - sanitized.tasks.length,
207
+ }
208
+ },
209
+ })
210
+
211
+ export const metaAiTools = [
212
+ listAgentsTool,
213
+ describeAgentTool,
214
+ updateTaskPlanTool,
215
+ ]
139
216
 
140
217
  export default metaAiTools
@@ -4,7 +4,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
6
6
  import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
7
- import { listAgents, loadAgentRegistry } from '../../../lib/agent-registry'
7
+ import { isAgentTaskPlanEnabled, listAgents, loadAgentRegistry } from '../../../lib/agent-registry'
8
8
  import { hasRequiredFeatures } from '../../../lib/auth'
9
9
  import { toolRegistry } from '../../../lib/tool-registry'
10
10
  import type { AiToolDefinition } from '../../../lib/types'
@@ -90,6 +90,7 @@ export async function GET(req: NextRequest) {
90
90
  mutationPolicy: agent.mutationPolicy ?? 'read-only',
91
91
  readOnly: Boolean(agent.readOnly),
92
92
  maxSteps: agent.maxSteps ?? null,
93
+ taskPlan: { enabled: isAgentTaskPlanEnabled(agent) },
93
94
  allowedTools: agent.allowedTools,
94
95
  tools,
95
96
  requiredFeatures: agent.requiredFeatures ?? [],
@@ -75,6 +75,7 @@ type AgentSettings = {
75
75
  mutationPolicy: string
76
76
  readOnly: boolean
77
77
  maxSteps: number | null
78
+ taskPlan?: { enabled?: boolean }
78
79
  allowedTools: string[]
79
80
  tools: AgentTool[]
80
81
  requiredFeatures: string[]
@@ -146,7 +146,7 @@
146
146
  "ai_assistant.allowlist.save.success": "Liste gespeichert.",
147
147
  "ai_assistant.allowlist.subtitle": "Schränke Anbieter und Modelle ein, die für diesen Mandanten verwendet werden dürfen. Die ENV-Liste ist die äußere Beschränkung — die Mandantenauswahl engt sie weiter ein.",
148
148
  "ai_assistant.allowlist.title": "Anbieter- & Modell-Allowlist",
149
- "ai_assistant.chat.agentTasksTitle": "Agent-Aufgaben",
149
+ "ai_assistant.chat.agentTasksTitle": "Tool-Aufrufe",
150
150
  "ai_assistant.chat.assistantRoleLabel": "Assistent",
151
151
  "ai_assistant.chat.attachFile": "Attach file",
152
152
  "ai_assistant.chat.betaChip": "beta",
@@ -257,6 +257,12 @@
257
257
  "ai_assistant.chat.tabs.noSessions": "Keine Sitzungen",
258
258
  "ai_assistant.chat.tabs.recentSessions": "Letzte Sitzungen",
259
259
  "ai_assistant.chat.tabs.rename": "Umbenennen",
260
+ "ai_assistant.chat.taskDone": "fertig",
261
+ "ai_assistant.chat.taskFailed": "fehlgeschlagen",
262
+ "ai_assistant.chat.taskPending": "ausstehend",
263
+ "ai_assistant.chat.taskPlanTitle": "Plan",
264
+ "ai_assistant.chat.taskRunning": "läuft…",
265
+ "ai_assistant.chat.taskSkipped": "übersprungen",
260
266
  "ai_assistant.chat.thinking": "Denkt nach...",
261
267
  "ai_assistant.chat.toolDone": "done",
262
268
  "ai_assistant.chat.toolError": "failed",
@@ -146,7 +146,7 @@
146
146
  "ai_assistant.allowlist.save.success": "Allowlist saved.",
147
147
  "ai_assistant.allowlist.subtitle": "Limit which providers and models the runtime, settings, and chat picker may use for this tenant. The env allowlist is the outer constraint — tenant picks narrow it further.",
148
148
  "ai_assistant.allowlist.title": "AI provider & model allowlist",
149
- "ai_assistant.chat.agentTasksTitle": "Agent tasks",
149
+ "ai_assistant.chat.agentTasksTitle": "Tool calls",
150
150
  "ai_assistant.chat.assistantRoleLabel": "Assistant",
151
151
  "ai_assistant.chat.attachFile": "Attach file",
152
152
  "ai_assistant.chat.betaChip": "beta",
@@ -257,6 +257,12 @@
257
257
  "ai_assistant.chat.tabs.noSessions": "No sessions",
258
258
  "ai_assistant.chat.tabs.recentSessions": "Recent sessions",
259
259
  "ai_assistant.chat.tabs.rename": "Rename",
260
+ "ai_assistant.chat.taskDone": "done",
261
+ "ai_assistant.chat.taskFailed": "failed",
262
+ "ai_assistant.chat.taskPending": "pending",
263
+ "ai_assistant.chat.taskPlanTitle": "Plan",
264
+ "ai_assistant.chat.taskRunning": "running…",
265
+ "ai_assistant.chat.taskSkipped": "skipped",
260
266
  "ai_assistant.chat.thinking": "Thinking...",
261
267
  "ai_assistant.chat.toolDone": "done",
262
268
  "ai_assistant.chat.toolError": "failed",
@@ -146,7 +146,7 @@
146
146
  "ai_assistant.allowlist.save.success": "Lista guardada.",
147
147
  "ai_assistant.allowlist.subtitle": "Limita los proveedores y modelos que el runtime, los ajustes y el selector de chat pueden usar para este inquilino. La lista ENV es la restricción externa — las elecciones del inquilino la reducen.",
148
148
  "ai_assistant.allowlist.title": "Lista de proveedores y modelos AI",
149
- "ai_assistant.chat.agentTasksTitle": "Tareas del agente",
149
+ "ai_assistant.chat.agentTasksTitle": "Llamadas a herramientas",
150
150
  "ai_assistant.chat.assistantRoleLabel": "Asistente",
151
151
  "ai_assistant.chat.attachFile": "Attach file",
152
152
  "ai_assistant.chat.betaChip": "beta",
@@ -257,6 +257,12 @@
257
257
  "ai_assistant.chat.tabs.noSessions": "Sin sesiones",
258
258
  "ai_assistant.chat.tabs.recentSessions": "Sesiones recientes",
259
259
  "ai_assistant.chat.tabs.rename": "Renombrar",
260
+ "ai_assistant.chat.taskDone": "hecho",
261
+ "ai_assistant.chat.taskFailed": "fallido",
262
+ "ai_assistant.chat.taskPending": "pendiente",
263
+ "ai_assistant.chat.taskPlanTitle": "Plan",
264
+ "ai_assistant.chat.taskRunning": "en curso…",
265
+ "ai_assistant.chat.taskSkipped": "omitido",
260
266
  "ai_assistant.chat.thinking": "Pensando...",
261
267
  "ai_assistant.chat.toolDone": "done",
262
268
  "ai_assistant.chat.toolError": "failed",
@@ -146,7 +146,7 @@
146
146
  "ai_assistant.allowlist.save.success": "Lista zapisana.",
147
147
  "ai_assistant.allowlist.subtitle": "Ogranicz dostawców i modele dostępne dla tego najemcy. Lista ENV jest zewnętrznym ograniczeniem — wybory najemcy je zawężają.",
148
148
  "ai_assistant.allowlist.title": "Lista dozwolonych dostawców i modeli AI",
149
- "ai_assistant.chat.agentTasksTitle": "Zadania agenta",
149
+ "ai_assistant.chat.agentTasksTitle": "Wywołania narzędzi",
150
150
  "ai_assistant.chat.assistantRoleLabel": "Asystent",
151
151
  "ai_assistant.chat.attachFile": "Attach file",
152
152
  "ai_assistant.chat.betaChip": "beta",
@@ -257,6 +257,12 @@
257
257
  "ai_assistant.chat.tabs.noSessions": "Brak sesji",
258
258
  "ai_assistant.chat.tabs.recentSessions": "Ostatnie sesje",
259
259
  "ai_assistant.chat.tabs.rename": "Zmień nazwę",
260
+ "ai_assistant.chat.taskDone": "ukończono",
261
+ "ai_assistant.chat.taskFailed": "niepowodzenie",
262
+ "ai_assistant.chat.taskPending": "oczekuje",
263
+ "ai_assistant.chat.taskPlanTitle": "Plan",
264
+ "ai_assistant.chat.taskRunning": "w toku…",
265
+ "ai_assistant.chat.taskSkipped": "pominięto",
260
266
  "ai_assistant.chat.thinking": "Myślenie...",
261
267
  "ai_assistant.chat.toolDone": "done",
262
268
  "ai_assistant.chat.toolError": "failed",
@@ -187,6 +187,66 @@ describe('agent-registry', () => {
187
187
  })
188
188
  })
189
189
 
190
+ it('adds the internal task-plan tool only when taskPlan is enabled', () => {
191
+ seedAgentRegistryForTests([
192
+ makeAgent({
193
+ id: 'customers.account_assistant',
194
+ moduleId: 'customers',
195
+ allowedTools: ['customers.list_people'],
196
+ taskPlan: { enabled: true },
197
+ }),
198
+ makeAgent({
199
+ id: 'catalog.catalog_assistant',
200
+ moduleId: 'catalog',
201
+ allowedTools: ['catalog.list_products', 'meta.update_task_plan'],
202
+ }),
203
+ ])
204
+
205
+ expect(getAgent('customers.account_assistant')).toMatchObject({
206
+ taskPlan: { enabled: true },
207
+ allowedTools: ['customers.list_people', 'meta.update_task_plan'],
208
+ })
209
+ const catalogAgent = getAgent('catalog.catalog_assistant')
210
+ expect(catalogAgent?.taskPlan).toBeUndefined()
211
+ expect(catalogAgent?.allowedTools).toEqual(['catalog.list_products'])
212
+ })
213
+
214
+ it('lets extensions opt an existing agent in or out of visible task planning', () => {
215
+ seedAgentRegistryForTests([
216
+ makeAgent({
217
+ id: 'catalog.catalog_assistant',
218
+ moduleId: 'catalog',
219
+ allowedTools: ['catalog.list_products'],
220
+ }),
221
+ makeAgent({
222
+ id: 'customers.account_assistant',
223
+ moduleId: 'customers',
224
+ allowedTools: ['customers.list_people'],
225
+ taskPlan: { enabled: true },
226
+ }),
227
+ ])
228
+
229
+ applyAgentExtensionEntriesForTests([
230
+ {
231
+ targetAgentId: 'catalog.catalog_assistant',
232
+ taskPlan: { enabled: true },
233
+ },
234
+ {
235
+ targetAgentId: 'customers.account_assistant',
236
+ taskPlan: { enabled: false },
237
+ },
238
+ ])
239
+
240
+ expect(getAgent('catalog.catalog_assistant')).toMatchObject({
241
+ taskPlan: { enabled: true },
242
+ allowedTools: ['catalog.list_products', 'meta.update_task_plan'],
243
+ })
244
+ expect(getAgent('customers.account_assistant')).toMatchObject({
245
+ taskPlan: { enabled: false },
246
+ allowedTools: ['customers.list_people'],
247
+ })
248
+ })
249
+
190
250
  it('resetAgentRegistryForTests clears the cache so a subsequent seed sees fresh fixtures', () => {
191
251
  seedAgentRegistryForTests([
192
252
  makeAgent({ id: 'catalog.merchandiser', moduleId: 'catalog' }),
@@ -71,6 +71,7 @@ describe('defineAiAgentExtension', () => {
71
71
  replaceAllowedTools: ['catalog.list_products'],
72
72
  deleteAllowedTools: ['catalog.old_tool'],
73
73
  appendAllowedTools: ['example.catalog_stats'],
74
+ taskPlan: { enabled: true },
74
75
  replaceSystemPrompt: 'Replacement prompt.',
75
76
  appendSystemPrompt: 'Use example.catalog_stats for app-level catalog metrics.',
76
77
  replaceSuggestions: [
@@ -86,6 +87,7 @@ describe('defineAiAgentExtension', () => {
86
87
  expect(extension.replaceAllowedTools).toEqual(['catalog.list_products'])
87
88
  expect(extension.deleteAllowedTools).toEqual(['catalog.old_tool'])
88
89
  expect(extension.appendAllowedTools).toEqual(['example.catalog_stats'])
90
+ expect(extension.taskPlan).toEqual({ enabled: true })
89
91
  expect(extension.replaceSystemPrompt).toBe('Replacement prompt.')
90
92
  expect(extension.deleteSuggestions).toEqual(['Old prompt'])
91
93
  expect(extension.appendSuggestions).toEqual([
@@ -128,6 +130,7 @@ describe('defineAiAgent', () => {
128
130
  defaultModel: 'gpt-4o',
129
131
  acceptedMediaTypes: ['image', 'pdf', 'file'],
130
132
  requiredFeatures: ['catalog.products.view'],
133
+ taskPlan: { enabled: true },
131
134
  uiParts: ['mutation-preview-card'],
132
135
  readOnly: true,
133
136
  mutationPolicy: 'read-only',
@@ -163,6 +166,7 @@ describe('defineAiAgent', () => {
163
166
  expect(agent.domain).toBe('catalog')
164
167
  expect(agent.acceptedMediaTypes).toEqual(['image', 'pdf', 'file'])
165
168
  expect(agent.requiredFeatures).toEqual(['catalog.products.view'])
169
+ expect(agent.taskPlan).toEqual({ enabled: true })
166
170
  expect(agent.uiParts).toEqual(['mutation-preview-card'])
167
171
  expect(agent.readOnly).toBe(true)
168
172
  expect(agent.defaultModel).toBe('gpt-4o')
@@ -267,6 +267,49 @@ describe('prepareMutation', () => {
267
267
  expect(pendingAction.organizationId).toBe('org-alpha')
268
268
  })
269
269
 
270
+ it('uses resolver-provided after snapshots and display labels for operator previews', async () => {
271
+ const em = mockEm()
272
+ const container = makeContainer(em)
273
+ const tool = makeTool({
274
+ name: 'customers.update_deal_stage',
275
+ isMutation: true,
276
+ loadBeforeRecord: async () => ({
277
+ recordId: 'deal-1',
278
+ entityType: 'customers.deal',
279
+ recordVersion: 'v-1',
280
+ before: { pipelineStageId: 'stage-old' },
281
+ after: { pipelineStageId: 'stage-new' },
282
+ display: {
283
+ fieldLabels: { pipelineStageId: 'Pipeline stage' },
284
+ before: { pipelineStageId: 'Offering' },
285
+ after: { pipelineStageId: 'Lost' },
286
+ },
287
+ }),
288
+ })
289
+ const agent = makeAgent({ id: 'customers.deal_analyzer' })
290
+
291
+ const { uiPart, pendingAction } = await prepareMutation(
292
+ {
293
+ agent,
294
+ tool,
295
+ toolCallArgs: { dealId: 'deal-1', toPipelineStageId: 'stage-new' },
296
+ },
297
+ { ...baseCtx, container },
298
+ )
299
+
300
+ expect(uiPart.props.fieldDiff).toEqual([
301
+ {
302
+ field: 'pipelineStageId',
303
+ fieldLabel: 'Pipeline stage',
304
+ before: 'stage-old',
305
+ after: 'stage-new',
306
+ beforeDisplay: 'Offering',
307
+ afterDisplay: 'Lost',
308
+ },
309
+ ])
310
+ expect(pendingAction.fieldDiff).toEqual(uiPart.props.fieldDiff)
311
+ })
312
+
270
313
  it('batch happy path: populates records[] with per-record diffs (fieldDiff stays []) when isBulk=true', async () => {
271
314
  const em = mockEm()
272
315
  const container = makeContainer(em)