@open-mercato/ai-assistant 0.6.8-develop.6998.1.d2bc46c56c → 0.6.8-develop.7001.1.03608ab252

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.
@@ -136,7 +136,7 @@ function mergeFailedRecords(recheck, handler) {
136
136
  if (seen.size === 0) return null;
137
137
  return Array.from(seen.values());
138
138
  }
139
- function toToolHandlerContext(ctx, tool) {
139
+ function toToolHandlerContext(ctx, tool, approvedPendingActionId) {
140
140
  return {
141
141
  tenantId: ctx.tenantId,
142
142
  organizationId: ctx.organizationId,
@@ -144,7 +144,8 @@ function toToolHandlerContext(ctx, tool) {
144
144
  container: ctx.container,
145
145
  userFeatures: ctx.userFeatures,
146
146
  isSuperAdmin: ctx.isSuperAdmin,
147
- tool
147
+ tool,
148
+ approvedPendingActionId
148
149
  };
149
150
  }
150
151
  async function executePendingActionConfirm(input) {
@@ -184,7 +185,7 @@ async function executePendingActionConfirm(input) {
184
185
  const executingRow = await repo.setStatus(confirmedRow.id, "executing", scope, { now: clock });
185
186
  let handlerOutput;
186
187
  try {
187
- handlerOutput = await tool.handler(action.normalizedInput, toToolHandlerContext(ctx, tool));
188
+ handlerOutput = await tool.handler(action.normalizedInput, toToolHandlerContext(ctx, tool, action.id));
188
189
  } catch (error) {
189
190
  const failureResult = {
190
191
  error: buildHandlerErrorFromThrown(error, action.normalizedInput)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/pending-action-executor.ts"],
4
- "sourcesContent": ["/**\n * Pending-action executor (spec \u00A79.4, Step 5.8).\n *\n * Transitions an `AiPendingAction` from `pending \u2192 confirmed \u2192 executing`,\n * invokes the wrapped tool handler, and records the outcome. Isolated from\n * the HTTP route so the unit suite can exercise the state-machine +\n * event-emission + idempotency guarantees without constructing a\n * `NextRequest`.\n *\n * Atomicity:\n * - The `pending \u2192 confirmed` and `confirmed \u2192 executing` transitions go\n * through the repository's `em.transactional` boundary. If the process\n * crashes between steps, the row is left in an intermediate terminal\n * state (`executing` or `confirmed`) that the operator can recover \u2014\n * NEVER in a partially-applied state that hides the crash.\n * - The tool handler itself runs OUTSIDE the repo transaction so that a\n * long-running write does not hold an `ai_pending_actions` row lock.\n * The handler's own transaction boundary (typically a command) is the\n * unit of atomicity for the underlying data change.\n */\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { AiPendingActionRepository } from '../data/repositories/AiPendingActionRepository'\nimport type { AiPendingAction } from '../data/entities'\nimport { emitAiAssistantEvent } from '../events'\nimport type { AiActionConfirmedPayload } from '../events'\nimport type { AiAgentDefinition } from './ai-agent-definition'\nimport type { AiToolDefinition, McpToolContext } from './types'\nimport type {\n AiPendingActionExecutionResult,\n AiPendingActionFailedRecord,\n} from './pending-action-types'\n\nconst logger = createLogger('ai_assistant')\n\nexport interface PendingActionExecuteContext {\n tenantId: string\n organizationId: string | null\n userId: string\n userFeatures: string[]\n isSuperAdmin: boolean\n container: import('awilix').AwilixContainer\n}\n\nexport interface PendingActionExecuteInput {\n action: AiPendingAction\n agent: AiAgentDefinition\n tool: AiToolDefinition\n ctx: PendingActionExecuteContext\n /** Carried over from the re-check; written onto the row with status=confirmed. */\n failedRecords?: AiPendingActionFailedRecord[] | null\n repo?: AiPendingActionRepository\n /**\n * Injection seam for unit tests. When omitted, emission is routed via\n * the typed `emitAiAssistantEvent` helper (the normal production path).\n * When supplied, the raw bus is used directly \u2014 kept for legacy tests\n * that assert on the bus call surface.\n */\n emitEvent?: (\n eventId: 'ai.action.confirmed',\n payload: AiActionConfirmedPayload,\n ) => Promise<void>\n now?: Date\n}\n\nexport interface PendingActionExecuteOk {\n ok: true\n action: AiPendingAction\n executionResult: AiPendingActionExecutionResult\n}\n\nexport interface PendingActionExecuteFail {\n ok: false\n action: AiPendingAction\n executionResult: AiPendingActionExecutionResult\n /** The underlying error \u2014 the route translates into a 200 with `executionResult.error` set. */\n cause: unknown\n}\n\nexport type PendingActionExecuteResult = PendingActionExecuteOk | PendingActionExecuteFail\n\nconst CONFIRMED_EVENT_ID = 'ai.action.confirmed' as const\n\ntype ConfirmedEmitter = (\n eventId: 'ai.action.confirmed',\n payload: AiActionConfirmedPayload,\n) => Promise<void>\n\nconst defaultConfirmedEmitter: ConfirmedEmitter = async (eventId, payload) => {\n await emitAiAssistantEvent(eventId, payload as unknown as Record<string, unknown>, {\n persistent: true,\n })\n}\n\nasync function emitConfirmed(\n emitter: ConfirmedEmitter,\n payload: AiActionConfirmedPayload,\n): Promise<void> {\n try {\n await emitter(CONFIRMED_EVENT_ID, payload)\n } catch (error) {\n logger.warn('Failed to emit pending-action event', { eventId: CONFIRMED_EVENT_ID, err: error })\n }\n}\n\nfunction normalizeExecutionResult(\n raw: unknown,\n): AiPendingActionExecutionResult {\n if (!raw || typeof raw !== 'object') return {}\n const source = raw as Record<string, unknown>\n const result: AiPendingActionExecutionResult = {}\n if (typeof source.recordId === 'string') result.recordId = source.recordId\n if (typeof source.commandName === 'string') result.commandName = source.commandName\n return result\n}\n\n/**\n * Extract per-record handler failures from a bulk tool's return value so\n * they can be persisted onto the pending-action row's `failedRecords[]`.\n *\n * Bulk mutation tools (Step 5.14) return a result of the shape:\n * { commandName, records: [{ recordId, status, before, after, error? }],\n * failedRecordIds: string[], error? }\n *\n * We pull the entries whose `status !== 'updated'` AND carry an `error`\n * object, coerce them to the `AiPendingActionFailedRecord` shape, and\n * return them so the executor can merge with re-check-sourced failures\n * at the final `executing \u2192 confirmed` transition (spec \u00A79.8 line 746:\n * \"a failure inside the confirm handler ... is recorded per-record in\n * executionResult.failedRecords[] / row.failedRecords\").\n *\n * Returns an empty array when the handler output does not carry the\n * batch shape (single-record tools never populate this \u2014 their failures\n * either throw from the handler and land in `executionResult.error` or\n * succeed cleanly).\n */\n/**\n * Convert a thrown handler error into the structured shape we persist on\n * `executionResult.error`. The previous implementation only kept\n * `{ code: 'handler_error', message }` \u2014 for ZodError that flattened to\n * the literal string \"Invalid input\", which was not enough context for\n * the operator's \"Fix with AI\" retry to actually fix anything. This\n * version preserves the original error name, Zod issues / fieldErrors,\n * an `input` echo of the arguments the handler was called with, and any\n * structured `cause` so the model can self-correct.\n *\n * Stack traces are deliberately gated behind `OM_AI_INCLUDE_HANDLER_STACK=1`\n * \u2014 they're noise to the model and a leak risk in tenant-visible UI.\n */\nfunction buildHandlerErrorFromThrown(\n error: unknown,\n input: unknown,\n): NonNullable<AiPendingActionExecutionResult['error']> {\n const message = error instanceof Error ? error.message : String(error)\n const name = error instanceof Error ? error.name : undefined\n const out: NonNullable<AiPendingActionExecutionResult['error']> = {\n code: 'handler_error',\n message: message || 'Tool handler threw an error.',\n }\n if (name) out.name = name\n\n // Echo the input so the model can compare what it sent vs. what the\n // schema expected. `normalizedInput` has already been Zod-parsed at\n // prepareMutation time so the values are JSON-safe.\n if (input !== undefined) {\n try {\n out.input = JSON.parse(JSON.stringify(input))\n } catch {\n // ignore \u2014 non-serializable input is rare and the model can still\n // work from the message + Zod issues.\n }\n }\n\n const details: Record<string, unknown> = {}\n\n if (error && typeof error === 'object') {\n const err = error as Record<string, unknown>\n // ZodError: forward `issues[]` verbatim and a flat `fieldErrors`\n // form so the model can locate the failing field by name even when\n // the message has been collapsed to \"Invalid input\".\n const issues = err.issues\n if (Array.isArray(issues) && issues.length > 0) {\n details.issues = issues.map((issue) => {\n if (!issue || typeof issue !== 'object') return issue\n const obj = issue as Record<string, unknown>\n return {\n path: Array.isArray(obj.path) ? obj.path : undefined,\n message: typeof obj.message === 'string' ? obj.message : undefined,\n code: typeof obj.code === 'string' ? obj.code : undefined,\n ...(typeof obj.expected === 'string' ? { expected: obj.expected } : {}),\n ...(typeof obj.received === 'string' ? { received: obj.received } : {}),\n }\n })\n const fieldErrors: Record<string, string[]> = {}\n for (const issue of issues as Array<Record<string, unknown>>) {\n const path = Array.isArray(issue.path) ? issue.path.join('.') : ''\n const msg = typeof issue.message === 'string' ? issue.message : null\n if (!path || !msg) continue\n if (!fieldErrors[path]) fieldErrors[path] = []\n fieldErrors[path].push(msg)\n }\n if (Object.keys(fieldErrors).length > 0) {\n details.fieldErrors = fieldErrors\n }\n if (out.code === 'handler_error') out.code = 'validation_error'\n }\n // Forward a known `code` if the handler error carries one.\n if (typeof err.code === 'string' && err.code.length > 0) {\n out.code = err.code\n }\n // Carry the cause when it is JSON-serializable (string, number, plain object).\n if (err.cause !== undefined) {\n try {\n details.cause = JSON.parse(JSON.stringify(err.cause))\n } catch {\n if (err.cause instanceof Error) {\n details.cause = { message: err.cause.message, name: err.cause.name }\n }\n }\n }\n // Pull through any other plain-object enumerable own props the handler\n // attached (e.g. `expected`, `actual`, `target`).\n for (const key of Object.keys(err)) {\n if (key === 'issues' || key === 'cause' || key === 'code' || key === 'message' || key === 'name' || key === 'stack') continue\n const value = err[key]\n if (value === undefined) continue\n try {\n details[key] = JSON.parse(JSON.stringify(value))\n } catch {\n // skip non-serializable\n }\n }\n }\n\n if (Object.keys(details).length > 0) {\n out.details = details\n }\n\n if (process.env.OM_AI_INCLUDE_HANDLER_STACK === '1' && error instanceof Error && error.stack) {\n // Trim to the top frames so the persisted result stays bounded.\n const lines = error.stack.split('\\n').slice(0, 6)\n out.stack = lines.join('\\n')\n }\n\n return out\n}\n\nfunction extractHandlerFailedRecords(raw: unknown): AiPendingActionFailedRecord[] {\n if (!raw || typeof raw !== 'object') return []\n const source = raw as Record<string, unknown>\n const records = source.records\n if (!Array.isArray(records) || records.length === 0) return []\n const out: AiPendingActionFailedRecord[] = []\n for (const entry of records) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n if (typeof record.recordId !== 'string') continue\n const status = typeof record.status === 'string' ? record.status : null\n if (status === 'updated') continue\n const errorField = record.error\n if (!errorField || typeof errorField !== 'object') continue\n const error = errorField as Record<string, unknown>\n const code = typeof error.code === 'string' ? error.code : 'handler_error'\n const message = typeof error.message === 'string' ? error.message : 'Record update failed.'\n out.push({\n recordId: record.recordId,\n error: { code, message },\n })\n }\n return out\n}\n\nfunction mergeFailedRecords(\n recheck: AiPendingActionFailedRecord[] | null | undefined,\n handler: AiPendingActionFailedRecord[] | null | undefined,\n): AiPendingActionFailedRecord[] | null {\n const seen = new Map<string, AiPendingActionFailedRecord>()\n for (const entry of recheck ?? []) {\n if (entry && typeof entry.recordId === 'string' && !seen.has(entry.recordId)) {\n seen.set(entry.recordId, entry)\n }\n }\n for (const entry of handler ?? []) {\n if (entry && typeof entry.recordId === 'string' && !seen.has(entry.recordId)) {\n seen.set(entry.recordId, entry)\n }\n }\n if (seen.size === 0) return null\n return Array.from(seen.values())\n}\n\nfunction toToolHandlerContext(\n ctx: PendingActionExecuteContext,\n tool: AiToolDefinition,\n): McpToolContext {\n return {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n container: ctx.container,\n userFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n tool,\n }\n}\n\n/**\n * Idempotent entry point for the Step 5.8 confirm route.\n *\n * - If the action is already `confirmed` with a stored `executionResult`,\n * returns that prior result without re-invoking the handler (double-click /\n * retry contract).\n * - If the action is already `confirmed` without a stored `executionResult`\n * (shouldn't happen in practice), returns a synthesized empty result.\n * - If the action is still `pending`, runs the transitions and the handler.\n * - Any other status is rejected at the re-check layer before this helper\n * is ever called; this helper treats them as invariant violations.\n */\nexport async function executePendingActionConfirm(\n input: PendingActionExecuteInput,\n): Promise<PendingActionExecuteResult> {\n const { action, agent, tool, ctx, failedRecords, now } = input\n const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve('em'))\n const scope = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n const clock = now ?? new Date()\n const emitter: ConfirmedEmitter = input.emitEvent ?? defaultConfirmedEmitter\n\n if (action.status === 'confirmed') {\n const prior = (action.executionResult ?? {}) as AiPendingActionExecutionResult\n return { ok: true, action, executionResult: prior }\n }\n\n if (action.status === 'executing') {\n const prior = (action.executionResult ?? {}) as AiPendingActionExecutionResult\n return { ok: true, action, executionResult: prior }\n }\n\n if (action.status !== 'pending') {\n return {\n ok: false,\n action,\n executionResult: {\n error: { code: 'invalid_status', message: `Action is in status \"${action.status}\".` },\n },\n cause: new Error(`Action is in status \"${action.status}\"`),\n }\n }\n\n const partialFailedRecords =\n Array.isArray(failedRecords) && failedRecords.length > 0 ? failedRecords : null\n\n const confirmedRow = await repo.setStatus(action.id, 'confirmed', scope, {\n resolvedByUserId: ctx.userId,\n now: clock,\n ...(partialFailedRecords ? { failedRecords: partialFailedRecords } : {}),\n })\n const executingRow = await repo.setStatus(confirmedRow.id, 'executing', scope, { now: clock })\n\n let handlerOutput: unknown\n try {\n handlerOutput = await tool.handler(action.normalizedInput as never, toToolHandlerContext(ctx, tool))\n } catch (error) {\n const failureResult: AiPendingActionExecutionResult = {\n error: buildHandlerErrorFromThrown(error, action.normalizedInput),\n }\n const failedRow = await repo.setStatus(executingRow.id, 'failed', scope, {\n executionResult: failureResult,\n now: clock,\n })\n await emitConfirmed(emitter, {\n pendingActionId: failedRow.id,\n agentId: agent.id,\n toolName: tool.name,\n status: failedRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (failedRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult: failureResult,\n })\n return {\n ok: false,\n action: failedRow,\n executionResult: failureResult,\n cause: error,\n }\n }\n\n const successResult = normalizeExecutionResult(handlerOutput)\n const handlerFailedRecords = extractHandlerFailedRecords(handlerOutput)\n const mergedFailedRecords = mergeFailedRecords(partialFailedRecords, handlerFailedRecords)\n const confirmedExtra: Record<string, unknown> = {\n executionResult: successResult,\n now: clock,\n }\n // Always write `failedRecords` on the final transition so a batch that\n // had re-check-stale records but zero handler failures keeps the\n // original list, and a batch with handler failures merges both sets.\n // Explicit `null` collapses to \"no failures\" in the repository's\n // `normalizeFailedRecords` helper.\n confirmedExtra.failedRecords = mergedFailedRecords\n const confirmedFinal = await repo.setStatus(executingRow.id, 'confirmed', scope, confirmedExtra)\n const emitFailedRecordsPayload =\n Array.isArray(mergedFailedRecords) && mergedFailedRecords.length > 0\n ? mergedFailedRecords\n : null\n await emitConfirmed(emitter, {\n pendingActionId: confirmedFinal.id,\n agentId: agent.id,\n toolName: tool.name,\n status: confirmedFinal.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (confirmedFinal.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult: successResult,\n ...(emitFailedRecordsPayload ? { failedRecords: emitFailedRecordsPayload } : {}),\n })\n return { ok: true, action: confirmedFinal, executionResult: successResult }\n}\n\nexport const PENDING_ACTION_CONFIRMED_EVENT_ID = CONFIRMED_EVENT_ID\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Pending-action executor (spec \u00A79.4, Step 5.8).\n *\n * Transitions an `AiPendingAction` from `pending \u2192 confirmed \u2192 executing`,\n * invokes the wrapped tool handler, and records the outcome. Isolated from\n * the HTTP route so the unit suite can exercise the state-machine +\n * event-emission + idempotency guarantees without constructing a\n * `NextRequest`.\n *\n * Atomicity:\n * - The `pending \u2192 confirmed` and `confirmed \u2192 executing` transitions go\n * through the repository's `em.transactional` boundary. If the process\n * crashes between steps, the row is left in an intermediate terminal\n * state (`executing` or `confirmed`) that the operator can recover \u2014\n * NEVER in a partially-applied state that hides the crash.\n * - The tool handler itself runs OUTSIDE the repo transaction so that a\n * long-running write does not hold an `ai_pending_actions` row lock.\n * The handler's own transaction boundary (typically a command) is the\n * unit of atomicity for the underlying data change.\n */\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { AiPendingActionRepository } from '../data/repositories/AiPendingActionRepository'\nimport type { AiPendingAction } from '../data/entities'\nimport { emitAiAssistantEvent } from '../events'\nimport type { AiActionConfirmedPayload } from '../events'\nimport type { AiAgentDefinition } from './ai-agent-definition'\nimport type { AiToolDefinition, McpToolContext } from './types'\nimport type {\n AiPendingActionExecutionResult,\n AiPendingActionFailedRecord,\n} from './pending-action-types'\n\nconst logger = createLogger('ai_assistant')\n\nexport interface PendingActionExecuteContext {\n tenantId: string\n organizationId: string | null\n userId: string\n userFeatures: string[]\n isSuperAdmin: boolean\n container: import('awilix').AwilixContainer\n}\n\nexport interface PendingActionExecuteInput {\n action: AiPendingAction\n agent: AiAgentDefinition\n tool: AiToolDefinition\n ctx: PendingActionExecuteContext\n /** Carried over from the re-check; written onto the row with status=confirmed. */\n failedRecords?: AiPendingActionFailedRecord[] | null\n repo?: AiPendingActionRepository\n /**\n * Injection seam for unit tests. When omitted, emission is routed via\n * the typed `emitAiAssistantEvent` helper (the normal production path).\n * When supplied, the raw bus is used directly \u2014 kept for legacy tests\n * that assert on the bus call surface.\n */\n emitEvent?: (\n eventId: 'ai.action.confirmed',\n payload: AiActionConfirmedPayload,\n ) => Promise<void>\n now?: Date\n}\n\nexport interface PendingActionExecuteOk {\n ok: true\n action: AiPendingAction\n executionResult: AiPendingActionExecutionResult\n}\n\nexport interface PendingActionExecuteFail {\n ok: false\n action: AiPendingAction\n executionResult: AiPendingActionExecutionResult\n /** The underlying error \u2014 the route translates into a 200 with `executionResult.error` set. */\n cause: unknown\n}\n\nexport type PendingActionExecuteResult = PendingActionExecuteOk | PendingActionExecuteFail\n\nconst CONFIRMED_EVENT_ID = 'ai.action.confirmed' as const\n\ntype ConfirmedEmitter = (\n eventId: 'ai.action.confirmed',\n payload: AiActionConfirmedPayload,\n) => Promise<void>\n\nconst defaultConfirmedEmitter: ConfirmedEmitter = async (eventId, payload) => {\n await emitAiAssistantEvent(eventId, payload as unknown as Record<string, unknown>, {\n persistent: true,\n })\n}\n\nasync function emitConfirmed(\n emitter: ConfirmedEmitter,\n payload: AiActionConfirmedPayload,\n): Promise<void> {\n try {\n await emitter(CONFIRMED_EVENT_ID, payload)\n } catch (error) {\n logger.warn('Failed to emit pending-action event', { eventId: CONFIRMED_EVENT_ID, err: error })\n }\n}\n\nfunction normalizeExecutionResult(\n raw: unknown,\n): AiPendingActionExecutionResult {\n if (!raw || typeof raw !== 'object') return {}\n const source = raw as Record<string, unknown>\n const result: AiPendingActionExecutionResult = {}\n if (typeof source.recordId === 'string') result.recordId = source.recordId\n if (typeof source.commandName === 'string') result.commandName = source.commandName\n return result\n}\n\n/**\n * Extract per-record handler failures from a bulk tool's return value so\n * they can be persisted onto the pending-action row's `failedRecords[]`.\n *\n * Bulk mutation tools (Step 5.14) return a result of the shape:\n * { commandName, records: [{ recordId, status, before, after, error? }],\n * failedRecordIds: string[], error? }\n *\n * We pull the entries whose `status !== 'updated'` AND carry an `error`\n * object, coerce them to the `AiPendingActionFailedRecord` shape, and\n * return them so the executor can merge with re-check-sourced failures\n * at the final `executing \u2192 confirmed` transition (spec \u00A79.8 line 746:\n * \"a failure inside the confirm handler ... is recorded per-record in\n * executionResult.failedRecords[] / row.failedRecords\").\n *\n * Returns an empty array when the handler output does not carry the\n * batch shape (single-record tools never populate this \u2014 their failures\n * either throw from the handler and land in `executionResult.error` or\n * succeed cleanly).\n */\n/**\n * Convert a thrown handler error into the structured shape we persist on\n * `executionResult.error`. The previous implementation only kept\n * `{ code: 'handler_error', message }` \u2014 for ZodError that flattened to\n * the literal string \"Invalid input\", which was not enough context for\n * the operator's \"Fix with AI\" retry to actually fix anything. This\n * version preserves the original error name, Zod issues / fieldErrors,\n * an `input` echo of the arguments the handler was called with, and any\n * structured `cause` so the model can self-correct.\n *\n * Stack traces are deliberately gated behind `OM_AI_INCLUDE_HANDLER_STACK=1`\n * \u2014 they're noise to the model and a leak risk in tenant-visible UI.\n */\nfunction buildHandlerErrorFromThrown(\n error: unknown,\n input: unknown,\n): NonNullable<AiPendingActionExecutionResult['error']> {\n const message = error instanceof Error ? error.message : String(error)\n const name = error instanceof Error ? error.name : undefined\n const out: NonNullable<AiPendingActionExecutionResult['error']> = {\n code: 'handler_error',\n message: message || 'Tool handler threw an error.',\n }\n if (name) out.name = name\n\n // Echo the input so the model can compare what it sent vs. what the\n // schema expected. `normalizedInput` has already been Zod-parsed at\n // prepareMutation time so the values are JSON-safe.\n if (input !== undefined) {\n try {\n out.input = JSON.parse(JSON.stringify(input))\n } catch {\n // ignore \u2014 non-serializable input is rare and the model can still\n // work from the message + Zod issues.\n }\n }\n\n const details: Record<string, unknown> = {}\n\n if (error && typeof error === 'object') {\n const err = error as Record<string, unknown>\n // ZodError: forward `issues[]` verbatim and a flat `fieldErrors`\n // form so the model can locate the failing field by name even when\n // the message has been collapsed to \"Invalid input\".\n const issues = err.issues\n if (Array.isArray(issues) && issues.length > 0) {\n details.issues = issues.map((issue) => {\n if (!issue || typeof issue !== 'object') return issue\n const obj = issue as Record<string, unknown>\n return {\n path: Array.isArray(obj.path) ? obj.path : undefined,\n message: typeof obj.message === 'string' ? obj.message : undefined,\n code: typeof obj.code === 'string' ? obj.code : undefined,\n ...(typeof obj.expected === 'string' ? { expected: obj.expected } : {}),\n ...(typeof obj.received === 'string' ? { received: obj.received } : {}),\n }\n })\n const fieldErrors: Record<string, string[]> = {}\n for (const issue of issues as Array<Record<string, unknown>>) {\n const path = Array.isArray(issue.path) ? issue.path.join('.') : ''\n const msg = typeof issue.message === 'string' ? issue.message : null\n if (!path || !msg) continue\n if (!fieldErrors[path]) fieldErrors[path] = []\n fieldErrors[path].push(msg)\n }\n if (Object.keys(fieldErrors).length > 0) {\n details.fieldErrors = fieldErrors\n }\n if (out.code === 'handler_error') out.code = 'validation_error'\n }\n // Forward a known `code` if the handler error carries one.\n if (typeof err.code === 'string' && err.code.length > 0) {\n out.code = err.code\n }\n // Carry the cause when it is JSON-serializable (string, number, plain object).\n if (err.cause !== undefined) {\n try {\n details.cause = JSON.parse(JSON.stringify(err.cause))\n } catch {\n if (err.cause instanceof Error) {\n details.cause = { message: err.cause.message, name: err.cause.name }\n }\n }\n }\n // Pull through any other plain-object enumerable own props the handler\n // attached (e.g. `expected`, `actual`, `target`).\n for (const key of Object.keys(err)) {\n if (key === 'issues' || key === 'cause' || key === 'code' || key === 'message' || key === 'name' || key === 'stack') continue\n const value = err[key]\n if (value === undefined) continue\n try {\n details[key] = JSON.parse(JSON.stringify(value))\n } catch {\n // skip non-serializable\n }\n }\n }\n\n if (Object.keys(details).length > 0) {\n out.details = details\n }\n\n if (process.env.OM_AI_INCLUDE_HANDLER_STACK === '1' && error instanceof Error && error.stack) {\n // Trim to the top frames so the persisted result stays bounded.\n const lines = error.stack.split('\\n').slice(0, 6)\n out.stack = lines.join('\\n')\n }\n\n return out\n}\n\nfunction extractHandlerFailedRecords(raw: unknown): AiPendingActionFailedRecord[] {\n if (!raw || typeof raw !== 'object') return []\n const source = raw as Record<string, unknown>\n const records = source.records\n if (!Array.isArray(records) || records.length === 0) return []\n const out: AiPendingActionFailedRecord[] = []\n for (const entry of records) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n if (typeof record.recordId !== 'string') continue\n const status = typeof record.status === 'string' ? record.status : null\n if (status === 'updated') continue\n const errorField = record.error\n if (!errorField || typeof errorField !== 'object') continue\n const error = errorField as Record<string, unknown>\n const code = typeof error.code === 'string' ? error.code : 'handler_error'\n const message = typeof error.message === 'string' ? error.message : 'Record update failed.'\n out.push({\n recordId: record.recordId,\n error: { code, message },\n })\n }\n return out\n}\n\nfunction mergeFailedRecords(\n recheck: AiPendingActionFailedRecord[] | null | undefined,\n handler: AiPendingActionFailedRecord[] | null | undefined,\n): AiPendingActionFailedRecord[] | null {\n const seen = new Map<string, AiPendingActionFailedRecord>()\n for (const entry of recheck ?? []) {\n if (entry && typeof entry.recordId === 'string' && !seen.has(entry.recordId)) {\n seen.set(entry.recordId, entry)\n }\n }\n for (const entry of handler ?? []) {\n if (entry && typeof entry.recordId === 'string' && !seen.has(entry.recordId)) {\n seen.set(entry.recordId, entry)\n }\n }\n if (seen.size === 0) return null\n return Array.from(seen.values())\n}\n\nfunction toToolHandlerContext(\n ctx: PendingActionExecuteContext,\n tool: AiToolDefinition,\n approvedPendingActionId: string,\n): McpToolContext {\n return {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n container: ctx.container,\n userFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n tool,\n approvedPendingActionId,\n }\n}\n\n/**\n * Idempotent entry point for the Step 5.8 confirm route.\n *\n * - If the action is already `confirmed` with a stored `executionResult`,\n * returns that prior result without re-invoking the handler (double-click /\n * retry contract).\n * - If the action is already `confirmed` without a stored `executionResult`\n * (shouldn't happen in practice), returns a synthesized empty result.\n * - If the action is still `pending`, runs the transitions and the handler.\n * - Any other status is rejected at the re-check layer before this helper\n * is ever called; this helper treats them as invariant violations.\n */\nexport async function executePendingActionConfirm(\n input: PendingActionExecuteInput,\n): Promise<PendingActionExecuteResult> {\n const { action, agent, tool, ctx, failedRecords, now } = input\n const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve('em'))\n const scope = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n const clock = now ?? new Date()\n const emitter: ConfirmedEmitter = input.emitEvent ?? defaultConfirmedEmitter\n\n if (action.status === 'confirmed') {\n const prior = (action.executionResult ?? {}) as AiPendingActionExecutionResult\n return { ok: true, action, executionResult: prior }\n }\n\n if (action.status === 'executing') {\n const prior = (action.executionResult ?? {}) as AiPendingActionExecutionResult\n return { ok: true, action, executionResult: prior }\n }\n\n if (action.status !== 'pending') {\n return {\n ok: false,\n action,\n executionResult: {\n error: { code: 'invalid_status', message: `Action is in status \"${action.status}\".` },\n },\n cause: new Error(`Action is in status \"${action.status}\"`),\n }\n }\n\n const partialFailedRecords =\n Array.isArray(failedRecords) && failedRecords.length > 0 ? failedRecords : null\n\n const confirmedRow = await repo.setStatus(action.id, 'confirmed', scope, {\n resolvedByUserId: ctx.userId,\n now: clock,\n ...(partialFailedRecords ? { failedRecords: partialFailedRecords } : {}),\n })\n const executingRow = await repo.setStatus(confirmedRow.id, 'executing', scope, { now: clock })\n\n let handlerOutput: unknown\n try {\n handlerOutput = await tool.handler(action.normalizedInput as never, toToolHandlerContext(ctx, tool, action.id))\n } catch (error) {\n const failureResult: AiPendingActionExecutionResult = {\n error: buildHandlerErrorFromThrown(error, action.normalizedInput),\n }\n const failedRow = await repo.setStatus(executingRow.id, 'failed', scope, {\n executionResult: failureResult,\n now: clock,\n })\n await emitConfirmed(emitter, {\n pendingActionId: failedRow.id,\n agentId: agent.id,\n toolName: tool.name,\n status: failedRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (failedRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult: failureResult,\n })\n return {\n ok: false,\n action: failedRow,\n executionResult: failureResult,\n cause: error,\n }\n }\n\n const successResult = normalizeExecutionResult(handlerOutput)\n const handlerFailedRecords = extractHandlerFailedRecords(handlerOutput)\n const mergedFailedRecords = mergeFailedRecords(partialFailedRecords, handlerFailedRecords)\n const confirmedExtra: Record<string, unknown> = {\n executionResult: successResult,\n now: clock,\n }\n // Always write `failedRecords` on the final transition so a batch that\n // had re-check-stale records but zero handler failures keeps the\n // original list, and a batch with handler failures merges both sets.\n // Explicit `null` collapses to \"no failures\" in the repository's\n // `normalizeFailedRecords` helper.\n confirmedExtra.failedRecords = mergedFailedRecords\n const confirmedFinal = await repo.setStatus(executingRow.id, 'confirmed', scope, confirmedExtra)\n const emitFailedRecordsPayload =\n Array.isArray(mergedFailedRecords) && mergedFailedRecords.length > 0\n ? mergedFailedRecords\n : null\n await emitConfirmed(emitter, {\n pendingActionId: confirmedFinal.id,\n agentId: agent.id,\n toolName: tool.name,\n status: confirmedFinal.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (confirmedFinal.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult: successResult,\n ...(emitFailedRecordsPayload ? { failedRecords: emitFailedRecordsPayload } : {}),\n })\n return { ok: true, action: confirmedFinal, executionResult: successResult }\n}\n\nexport const PENDING_ACTION_CONFIRMED_EVENT_ID = CONFIRMED_EVENT_ID\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.8-develop.6998.1.d2bc46c56c",
3
+ "version": "0.6.8-develop.7001.1.03608ab252",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -99,16 +99,16 @@
99
99
  "zod-to-json-schema": "^3.25.2"
100
100
  },
101
101
  "peerDependencies": {
102
- "@open-mercato/shared": "0.6.8-develop.6998.1.d2bc46c56c",
103
- "@open-mercato/ui": "0.6.8-develop.6998.1.d2bc46c56c",
102
+ "@open-mercato/shared": "0.6.8-develop.7001.1.03608ab252",
103
+ "@open-mercato/ui": "0.6.8-develop.7001.1.03608ab252",
104
104
  "react": "^19.0.0",
105
105
  "react-dom": "^19.0.0",
106
106
  "zod": ">=3.23.0"
107
107
  },
108
108
  "devDependencies": {
109
- "@open-mercato/cli": "0.6.8-develop.6998.1.d2bc46c56c",
110
- "@open-mercato/shared": "0.6.8-develop.6998.1.d2bc46c56c",
111
- "@open-mercato/ui": "0.6.8-develop.6998.1.d2bc46c56c",
109
+ "@open-mercato/cli": "0.6.8-develop.7001.1.03608ab252",
110
+ "@open-mercato/shared": "0.6.8-develop.7001.1.03608ab252",
111
+ "@open-mercato/ui": "0.6.8-develop.7001.1.03608ab252",
112
112
  "@types/react": "^19.2.17",
113
113
  "@types/react-dom": "^19.2.3",
114
114
  "react": "19.2.8",
@@ -115,10 +115,11 @@ describe('executePendingActionConfirm', () => {
115
115
  it('transitions pending → confirmed → executing → confirmed on handler success and emits typed ai.action.confirmed', async () => {
116
116
  const repo = makeRepoStub(makeAction())
117
117
  const emitEvent = jest.fn().mockResolvedValue(undefined)
118
+ const handler = jest.fn(async () => ({ recordId: 'p-1', commandName: 'catalog.product.update' }))
118
119
  const result = await executePendingActionConfirm({
119
120
  action: makeAction(),
120
121
  agent: makeAgent(),
121
- tool: makeTool(),
122
+ tool: makeTool({ handler }),
122
123
  ctx: makeCtx(),
123
124
  repo: repo as unknown as never,
124
125
  emitEvent,
@@ -134,6 +135,10 @@ describe('executePendingActionConfirm', () => {
134
135
  recordId: 'p-1',
135
136
  commandName: 'catalog.product.update',
136
137
  })
138
+ expect(handler).toHaveBeenCalledWith(
139
+ expect.anything(),
140
+ expect.objectContaining({ approvedPendingActionId: 'pa_1' }),
141
+ )
137
142
  expect(emitEvent).toHaveBeenCalledTimes(1)
138
143
  const [emittedId, emittedPayload] = emitEvent.mock.calls[0] as [
139
144
  'ai.action.confirmed',
@@ -291,6 +291,7 @@ function mergeFailedRecords(
291
291
  function toToolHandlerContext(
292
292
  ctx: PendingActionExecuteContext,
293
293
  tool: AiToolDefinition,
294
+ approvedPendingActionId: string,
294
295
  ): McpToolContext {
295
296
  return {
296
297
  tenantId: ctx.tenantId,
@@ -300,6 +301,7 @@ function toToolHandlerContext(
300
301
  userFeatures: ctx.userFeatures,
301
302
  isSuperAdmin: ctx.isSuperAdmin,
302
303
  tool,
304
+ approvedPendingActionId,
303
305
  }
304
306
  }
305
307
 
@@ -361,7 +363,7 @@ export async function executePendingActionConfirm(
361
363
 
362
364
  let handlerOutput: unknown
363
365
  try {
364
- handlerOutput = await tool.handler(action.normalizedInput as never, toToolHandlerContext(ctx, tool))
366
+ handlerOutput = await tool.handler(action.normalizedInput as never, toToolHandlerContext(ctx, tool, action.id))
365
367
  } catch (error) {
366
368
  const failureResult: AiPendingActionExecutionResult = {
367
369
  error: buildHandlerErrorFromThrown(error, action.normalizedInput),
@@ -16,6 +16,8 @@ export interface McpToolContext {
16
16
  apiKeySecret?: string
17
17
  /** Session token for memory layer (deduplication of search/GET calls) */
18
18
  sessionId?: string
19
+ /** Pending action that the operator explicitly confirmed before this handler invocation. */
20
+ approvedPendingActionId?: string
19
21
  /**
20
22
  * Back-reference to the tool definition the handler is executing. Populated
21
23
  * by the runtime (`tool-executor`, `pending-action-executor`, the agent-tools