@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.
- package/dist/modules/ai_assistant/lib/pending-action-executor.js +4 -3
- package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +6 -1
- package/src/modules/ai_assistant/lib/pending-action-executor.ts +3 -1
- package/src/modules/ai_assistant/lib/types.ts +2 -0
|
@@ -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.
|
|
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.
|
|
103
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
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.
|
|
110
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
111
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
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
|