Files
CapaKraken/packages/api/src/router/assistant-tools.ts
T

2764 lines
86 KiB
TypeScript

/**
* AI Assistant Tool definitions for OpenAI Function Calling.
* Each tool has a JSON schema (for the AI) and an execute function (for the server).
*/
import { Prisma, VacationType } from "@capakraken/db";
import {
CreateAssignmentSchema,
AllocationStatus,
PermissionKey,
SystemRole,
} from "@capakraken/shared";
import type { WeekdayAvailability } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { ZodError } from "zod";
import { fmtEur } from "../lib/format-utils.js";
import { timelineRouter } from "./timeline.js";
import { logger } from "../lib/logger.js";
import { createCallerFactory, type TRPCContext } from "../trpc.js";
import { auditLogRouter } from "./audit-log.js";
import { chargeabilityReportRouter } from "./chargeability-report.js";
import { computationGraphRouter } from "./computation-graph.js";
import { dispoRouter } from "./dispo.js";
import { importExportRouter } from "./import-export.js";
import { resourceRouter } from "./resource.js";
import { settingsRouter } from "./settings.js";
import { systemRoleConfigRouter } from "./system-role-config.js";
import { userRouter } from "./user.js";
import { notificationRouter } from "./notification.js";
import { estimateRouter } from "./estimate.js";
import { webhookRouter } from "./webhook.js";
import { countryRouter } from "./country.js";
import { holidayCalendarRouter } from "./holiday-calendar.js";
import { blueprintRouter } from "./blueprint.js";
import { roleRouter } from "./role.js";
import { clientRouter } from "./client.js";
import { orgUnitRouter } from "./org-unit.js";
import { projectRouter } from "./project.js";
import { rateCardRouter } from "./rate-card.js";
import { reportRouter } from "./report.js";
import { vacationRouter } from "./vacation.js";
import { entitlementRouter } from "./entitlement.js";
import { commentRouter } from "./comment.js";
import { managementLevelRouter } from "./management-level.js";
import { utilizationCategoryRouter } from "./utilization-category.js";
import { calculationRuleRouter } from "./calculation-rules.js";
import { effortRuleRouter } from "./effort-rule.js";
import { experienceMultiplierRouter } from "./experience-multiplier.js";
import { dashboardRouter } from "./dashboard.js";
import { insightsRouter } from "./insights.js";
import { scenarioRouter } from "./scenario.js";
import { allocationRouter } from "./allocation.js";
import { staffingRouter } from "./staffing.js";
import { advancedTimelineToolDefinitions, createAdvancedTimelineExecutors } from "./assistant-tools/advanced-timeline.js";
import {
allocationPlanningMutationToolDefinitions,
allocationPlanningReadToolDefinitions,
createAllocationPlanningExecutors,
} from "./assistant-tools/allocation-planning.js";
import { settingsAdminToolDefinitions, createSettingsAdminExecutors } from "./assistant-tools/settings-admin.js";
import {
createVacationHolidayExecutors,
vacationHolidayMutationToolDefinitions,
vacationHolidayReadToolDefinitions,
} from "./assistant-tools/vacation-holidays.js";
import {
createRolesAnalyticsExecutors,
rolesAnalyticsMutationToolDefinitions,
rolesAnalyticsReadToolDefinitions,
} from "./assistant-tools/roles-analytics.js";
import {
clientMutationToolDefinitions,
createClientsOrgUnitsExecutors,
orgUnitMutationToolDefinitions,
} from "./assistant-tools/clients-org-units.js";
import {
chargeabilityComputationReadToolDefinitions,
createChargeabilityComputationExecutors,
} from "./assistant-tools/chargeability-computation.js";
import {
configReadmodelToolDefinitions,
createConfigReadmodelExecutors,
} from "./assistant-tools/config-readmodels.js";
import {
countryMetroAdminToolDefinitions,
createCountryMetroAdminExecutors,
} from "./assistant-tools/country-metro-admin.js";
import {
countryReadmodelToolDefinitions,
createCountryReadmodelExecutors,
} from "./assistant-tools/country-readmodels.js";
import {
createUserSelfServiceExecutors,
userSelfServiceToolDefinitions,
} from "./assistant-tools/user-self-service.js";
import {
createUserAdminExecutors,
userAdminToolDefinitions,
} from "./assistant-tools/user-admin.js";
import {
createNotificationsTasksExecutors,
notificationInboxToolDefinitions,
notificationTaskToolDefinitions,
} from "./assistant-tools/notifications-tasks.js";
import {
createEstimateExecutors,
estimateMutationToolDefinitions,
estimateReadToolDefinitions,
} from "./assistant-tools/estimates.js";
import {
createProjectExecutors,
projectMutationToolDefinitions,
projectReadToolDefinitions,
} from "./assistant-tools/projects.js";
import {
createStaffingDemandExecutors,
staffingDemandMutationToolDefinitions,
staffingDemandReadToolDefinitions,
} from "./assistant-tools/staffing-demand.js";
import {
createResourceExecutors,
resourceMutationToolDefinitions,
resourceReadToolDefinitions,
} from "./assistant-tools/resources.js";
import {
blueprintsRateCardsToolDefinitions,
createBlueprintsRateCardsExecutors,
} from "./assistant-tools/blueprints-rate-cards.js";
import {
createDashboardInsightsReportsExecutors,
dashboardInsightsReportsToolDefinitions,
} from "./assistant-tools/dashboard-insights-reports.js";
import {
createScenarioRateAnalysisExecutors,
scenarioRateAnalysisToolDefinitions,
} from "./assistant-tools/scenario-rate-analysis.js";
import {
createImportExportDispoExecutors,
importExportDispoToolDefinitions,
} from "./assistant-tools/import-export-dispo.js";
import {
commentMutationToolDefinitions,
commentReadToolDefinitions,
createCommentExecutors,
} from "./assistant-tools/comments.js";
import {
auditHistoryToolDefinitions,
createAuditHistoryExecutors,
} from "./assistant-tools/audit-history.js";
import {
createPlanningNavigationExecutors,
planningNavigationToolDefinitions,
} from "./assistant-tools/planning-navigation.js";
import {
withToolAccess,
type ToolAccessRequirements,
type ToolContext,
type ToolDef,
type ToolExecutor,
} from "./assistant-tools/shared.js";
export type { ToolContext } from "./assistant-tools/shared.js";
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
export const MUTATION_TOOLS = new Set([
"import_csv_data",
"update_system_settings",
"clear_stored_runtime_secrets",
"test_ai_connection",
"test_smtp_connection",
"test_gemini_connection",
"update_system_role_config",
"create_webhook",
"update_webhook",
"delete_webhook",
"test_webhook",
"stage_dispo_import_batch",
"cancel_dispo_import_batch",
"resolve_dispo_staged_record",
"commit_dispo_import_batch",
"create_allocation", "cancel_allocation", "update_allocation_status",
"update_timeline_allocation_inline", "apply_timeline_project_shift",
"quick_assign_timeline_resource", "batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"update_resource", "deactivate_resource", "create_resource",
"update_project", "create_project", "delete_project",
"create_vacation", "approve_vacation", "reject_vacation", "cancel_vacation",
"set_entitlement", "create_demand", "fill_demand",
"generate_project_cover", "remove_project_cover",
"create_role", "update_role", "delete_role",
"create_client", "update_client", "delete_client",
"create_org_unit", "update_org_unit",
"create_country", "update_country",
"create_metro_city", "update_metro_city", "delete_metro_city",
"create_holiday_calendar", "update_holiday_calendar", "delete_holiday_calendar",
"create_holiday_calendar_entry", "update_holiday_calendar_entry", "delete_holiday_calendar_entry",
"send_broadcast", "create_task_for_user", "create_reminder",
"update_task_status", "execute_task_action",
"create_comment", "resolve_comment", "mark_notification_read",
"save_dashboard_layout", "toggle_favorite_project",
"set_column_preferences", "generate_totp_secret", "verify_and_enable_totp",
"create_user", "set_user_password", "update_user_role", "update_user_name",
"link_user_resource", "auto_link_users_by_email", "set_user_permissions",
"reset_user_permissions", "disable_user_totp",
"create_notification", "update_reminder", "delete_reminder",
"delete_notification", "assign_task",
"clone_estimate", "update_estimate_draft", "submit_estimate_version",
"approve_estimate_version", "create_estimate_revision",
"create_estimate_export", "create_estimate_planning_handoff",
"generate_estimate_weekly_phasing", "update_estimate_commercial_terms",
]);
export const ADVANCED_ASSISTANT_TOOLS = new Set([
"find_best_project_resource",
"get_timeline_entries_view",
"get_timeline_holiday_overlays",
"get_project_timeline_context",
"preview_project_shift",
"update_timeline_allocation_inline",
"apply_timeline_project_shift",
"quick_assign_timeline_resource",
"batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"get_chargeability_report",
"get_resource_computation_graph",
"get_project_computation_graph",
]);
const createChargeabilityReportCaller = createCallerFactory(chargeabilityReportRouter);
const createComputationGraphCaller = createCallerFactory(computationGraphRouter);
const createTimelineCaller = createCallerFactory(timelineRouter);
const createAuditLogCaller = createCallerFactory(auditLogRouter);
const createImportExportCaller = createCallerFactory(importExportRouter);
const createDispoCaller = createCallerFactory(dispoRouter);
const createResourceCaller = createCallerFactory(resourceRouter);
const createSettingsCaller = createCallerFactory(settingsRouter);
const createSystemRoleConfigCaller = createCallerFactory(systemRoleConfigRouter);
const createUserCaller = createCallerFactory(userRouter);
const createNotificationCaller = createCallerFactory(notificationRouter);
const createEstimateCaller = createCallerFactory(estimateRouter);
const createWebhookCaller = createCallerFactory(webhookRouter);
const createCountryCaller = createCallerFactory(countryRouter);
const createHolidayCalendarCaller = createCallerFactory(holidayCalendarRouter);
const createBlueprintCaller = createCallerFactory(blueprintRouter);
const createRoleCaller = createCallerFactory(roleRouter);
const createClientCaller = createCallerFactory(clientRouter);
const createOrgUnitCaller = createCallerFactory(orgUnitRouter);
const createProjectCaller = createCallerFactory(projectRouter);
const createRateCardCaller = createCallerFactory(rateCardRouter);
const createReportCaller = createCallerFactory(reportRouter);
const createVacationCaller = createCallerFactory(vacationRouter);
const createEntitlementCaller = createCallerFactory(entitlementRouter);
const createCommentCaller = createCallerFactory(commentRouter);
const createManagementLevelCaller = createCallerFactory(managementLevelRouter);
const createUtilizationCategoryCaller = createCallerFactory(utilizationCategoryRouter);
const createCalculationRuleCaller = createCallerFactory(calculationRuleRouter);
const createEffortRuleCaller = createCallerFactory(effortRuleRouter);
const createExperienceMultiplierCaller = createCallerFactory(experienceMultiplierRouter);
const createDashboardCaller = createCallerFactory(dashboardRouter);
const createInsightsCaller = createCallerFactory(insightsRouter);
const createScenarioCaller = createCallerFactory(scenarioRouter);
const createAllocationCaller = createCallerFactory(allocationRouter);
const createStaffingCaller = createCallerFactory(staffingRouter);
// ─── Helpers ────────────────────────────────────────────────────────────────
function fmtDate(d: Date | null | undefined): string | null {
return d ? d.toISOString().slice(0, 10) : null;
}
class AssistantVisibleError extends Error {
constructor(message: string) {
super(message);
this.name = "AssistantVisibleError";
}
}
function assertPermission(ctx: ToolContext, perm: PermissionKey): void {
if (!ctx.permissions.has(perm)) {
throw new AssistantVisibleError(`Permission denied: you need the "${perm}" permission to perform this action.`);
}
}
function assertAdminRole(ctx: ToolContext): void {
if (ctx.userRole !== SystemRole.ADMIN) {
throw new AssistantVisibleError("Admin role required to perform this action.");
}
}
function formatHolidayCalendarEntry(entry: {
id: string;
date: Date;
name: string;
isRecurringAnnual?: boolean | null;
source?: string | null;
}) {
return {
id: entry.id,
date: fmtDate(entry.date),
name: entry.name,
isRecurringAnnual: entry.isRecurringAnnual ?? false,
source: entry.source ?? null,
};
}
function formatHolidayCalendar(calendar: {
id: string;
name: string;
scopeType: string;
stateCode?: string | null;
isActive?: boolean | null;
priority?: number | null;
country?: { id: string; code: string; name: string } | null;
metroCity?: { id: string; name: string } | null;
_count?: { entries?: number | null } | null;
entries?: Array<{
id: string;
date: Date;
name: string;
isRecurringAnnual?: boolean | null;
source?: string | null;
}> | null;
}) {
const entries = calendar.entries?.map(formatHolidayCalendarEntry) ?? [];
return {
id: calendar.id,
name: calendar.name,
scopeType: calendar.scopeType,
stateCode: calendar.stateCode ?? null,
isActive: calendar.isActive ?? true,
priority: calendar.priority ?? 0,
country: calendar.country
? {
id: calendar.country.id,
code: calendar.country.code,
name: calendar.country.name,
}
: null,
metroCity: calendar.metroCity
? {
id: calendar.metroCity.id,
name: calendar.metroCity.name,
}
: null,
entryCount: calendar._count?.entries ?? entries.length,
entries,
};
}
function formatCountry(country: {
id: string;
code: string;
name: string;
dailyWorkingHours: number;
scheduleRules?: Prisma.JsonValue | null;
isActive?: boolean | null;
metroCities?: Array<{ id: string; name: string }> | null;
_count?: { resources?: number | null } | null;
}) {
return {
id: country.id,
code: country.code,
name: country.name,
dailyWorkingHours: country.dailyWorkingHours,
scheduleRules: country.scheduleRules ?? null,
isActive: country.isActive ?? true,
resourceCount: country._count?.resources ?? null,
metroCities: (country.metroCities ?? []).map((city) => ({
id: city.id,
name: city.name,
})),
cities: (country.metroCities ?? []).map((city) => city.name),
};
}
function createUtcDate(year: number, monthIndex: number, day: number): Date {
return new Date(Date.UTC(year, monthIndex, day));
}
function resolveHolidayPeriod(input: {
year?: number;
periodStart?: string;
periodEnd?: string;
}): { year: number | null; periodStart: Date; periodEnd: Date } {
if (input.periodStart || input.periodEnd) {
if (!input.periodStart || !input.periodEnd) {
throw new AssistantVisibleError("periodStart and periodEnd must both be provided when using a custom holiday range.");
}
const periodStart = new Date(`${input.periodStart}T00:00:00.000Z`);
const periodEnd = new Date(`${input.periodEnd}T00:00:00.000Z`);
if (Number.isNaN(periodStart.getTime())) {
throw new AssistantVisibleError(`Invalid periodStart: ${input.periodStart}`);
}
if (Number.isNaN(periodEnd.getTime())) {
throw new AssistantVisibleError(`Invalid periodEnd: ${input.periodEnd}`);
}
if (periodEnd < periodStart) {
throw new AssistantVisibleError("periodEnd must be on or after periodStart.");
}
return { year: null, periodStart, periodEnd };
}
const year = input.year ?? new Date().getUTCFullYear();
return {
year,
periodStart: createUtcDate(year, 0, 1),
periodEnd: createUtcDate(year, 11, 31),
};
}
const CONTROLLER_ASSISTANT_ROLES = [
SystemRole.ADMIN,
SystemRole.MANAGER,
SystemRole.CONTROLLER,
] as const;
const MANAGER_ASSISTANT_ROLES = [
SystemRole.ADMIN,
SystemRole.MANAGER,
] as const;
const ADMIN_ASSISTANT_ROLES = [SystemRole.ADMIN] as const;
const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequirements>> = {
search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
get_project: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
update_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
approve_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
reject_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
};
const ASSISTANT_VACATION_REQUEST_TYPES = [
VacationType.ANNUAL,
VacationType.SICK,
VacationType.OTHER,
] as const;
function parseAssistantVacationRequestType(input: string): VacationType {
const normalized = input.trim().toUpperCase();
if (normalized === VacationType.PUBLIC_HOLIDAY) {
throw new AssistantVisibleError("PUBLIC_HOLIDAY requests cannot be created manually. Manage public holidays through holiday calendars instead.");
}
if ((ASSISTANT_VACATION_REQUEST_TYPES as readonly string[]).includes(normalized)) {
return normalized as VacationType;
}
throw new AssistantVisibleError(`Invalid vacation type: ${input}. Valid types: ${ASSISTANT_VACATION_REQUEST_TYPES.join(", ")}.`);
}
function parseIsoDate(value: string, fieldName: string): Date {
const parsed = new Date(`${value}T00:00:00.000Z`);
if (Number.isNaN(parsed.getTime())) {
throw new AssistantVisibleError(`Invalid ${fieldName}: ${value}`);
}
return parsed;
}
function parseOptionalIsoDate(
value: string | undefined,
fieldName: string,
): Date | undefined {
return value ? parseIsoDate(value, fieldName) : undefined;
}
function parseDateTime(value: string, fieldName: string): Date {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
throw new AssistantVisibleError(`Invalid ${fieldName}: ${value}`);
}
return parsed;
}
function parseOptionalDateTime(
value: string | undefined,
fieldName: string,
): Date | undefined {
return value ? parseDateTime(value, fieldName) : undefined;
}
function toDate(value: Date | string): Date {
return value instanceof Date ? value : new Date(value);
}
type AssistantToolErrorResult = { error: string };
type AssistantIndexedFieldErrorResult = AssistantToolErrorResult & {
field: string;
index: number;
};
type BatchQuickAssignmentInput = {
resourceId: string;
projectId: string;
startDate: Date;
endDate: Date;
hoursPerDay?: number;
role?: string;
status?: AllocationStatus;
};
function toAssistantNotFoundError(
error: unknown,
message: string,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: message };
}
return null;
}
function toAssistantAllocationNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "NOT_FOUND") {
return { error: "Allocation not found with the given criteria." };
}
if (error.message === "Record not found" || error.message.includes("Assignment not found")) {
return { error: "Allocation not found with the given criteria." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "Allocation not found with the given criteria." };
}
return null;
}
function toAssistantProjectNotFoundError(
error: unknown,
identifier: string,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
`Project not found: ${identifier}`,
);
}
function toAssistantTimelineMutationError(
error: unknown,
context: "updateInline" | "applyShift" | "quickAssign" | "batchShift",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "NOT_FOUND") {
if (error.message.includes("Resource not found")) {
return { error: "Resource not found with the given criteria." };
}
if (error.message.includes("Project not found")) {
return { error: "Project not found with the given criteria." };
}
if (error.message.includes("Demand requirement not found")) {
return { error: "Demand requirement not found with the given criteria." };
}
if (error.message.includes("No allocations found")) {
return { error: "Allocation not found with the given criteria." };
}
}
if (error.code === "BAD_REQUEST" || error.code === "CONFLICT") {
return { error: error.message };
}
}
const allocationNotFound = toAssistantAllocationNotFoundError(error);
if (allocationNotFound && (context === "updateInline" || context === "batchShift")) {
return allocationNotFound;
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
if (errorText.includes("demand")) {
return { error: "Demand requirement not found with the given criteria." };
}
if (prismaError.code === "P2025" && (context === "updateInline" || context === "batchShift")) {
return { error: "Allocation not found with the given criteria." };
}
return null;
}
function toAssistantVacationNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Vacation not found with the given criteria.",
);
}
function toAssistantVacationMutationError(
error: unknown,
action: "approve" | "reject" | "cancel",
): AssistantToolErrorResult | null {
const notFound = toAssistantVacationNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (action === "approve") {
return { error: "Vacation cannot be approved in its current status." };
}
if (action === "reject") {
return { error: "Vacation cannot be rejected in its current status." };
}
return { error: "Vacation cannot be cancelled in its current status." };
}
return null;
}
function toAssistantProjectCreationError(
error: unknown,
shortCode: string,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "CONFLICT") {
return { error: `A project with short code "${shortCode}" already exists.` };
}
if (error.code === "NOT_FOUND") {
if (error.message.includes("Blueprint")) {
return { error: "Blueprint not found with the given criteria." };
}
if (error.message.includes("Client")) {
return { error: "Client not found with the given criteria." };
}
}
if (error.code === "BAD_REQUEST" || error.code === "UNPROCESSABLE_CONTENT") {
return { error: error.message };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("blueprint")) {
return { error: "Blueprint not found with the given criteria." };
}
if (errorText.includes("client")) {
return { error: "Client not found with the given criteria." };
}
return null;
}
function toAssistantDemandNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Demand not found with the given criteria.",
);
}
function toAssistantDemandFillError(
error: unknown,
): AssistantToolErrorResult | null {
const notFound = toAssistantDemandNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Demand cannot be filled in its current status." };
}
return null;
}
function toAssistantEstimateNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("version")) {
return { error: "Estimate version not found with the given criteria." };
}
return { error: "Estimate not found with the given criteria." };
}
return null;
}
function toAssistantEstimateReadError(
error: unknown,
context: "weeklyPhasing" | "commercialTerms",
): AssistantToolErrorResult | null {
const notFound = toAssistantEstimateNotFoundError(error);
if (notFound) {
return notFound;
}
if (
context === "weeklyPhasing"
&& error instanceof TRPCError
&& error.code === "PRECONDITION_FAILED"
&& error.message === "Estimate has no versions"
) {
return { error: "Estimate version not found with the given criteria." };
}
return null;
}
function toAssistantHolidayCalendarNotFoundError(
error: unknown,
identifier?: string,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
identifier
? `Holiday calendar not found: ${identifier}`
: "Holiday calendar not found with the given criteria.",
);
}
function toAssistantHolidayCalendarMutationError(
error: unknown,
): AssistantToolErrorResult | null {
const notFound = toAssistantHolidayCalendarNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Holiday calendar scope is invalid." };
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A holiday calendar for this scope already exists." };
}
return null;
}
function toAssistantHolidayEntryNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Holiday calendar entry not found with the given criteria.",
);
}
function toAssistantHolidayEntryMutationError(
error: unknown,
): AssistantToolErrorResult | null {
const calendarNotFound = toAssistantHolidayCalendarNotFoundError(error);
if (calendarNotFound) {
return calendarNotFound;
}
const entryNotFound = toAssistantHolidayEntryNotFoundError(error);
if (entryNotFound) {
return entryNotFound;
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A holiday entry for this calendar and date already exists." };
}
return null;
}
function toAssistantRoleNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Role not found with the given criteria.",
);
}
function toAssistantRoleMutationError(
error: unknown,
action: "create" | "update" | "delete",
): AssistantToolErrorResult | null {
const notFound = toAssistantRoleNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A role with this name already exists." };
}
if (action === "delete" && error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
return { error: "Role cannot be deleted while it is still assigned. Deactivate it instead." };
}
return null;
}
function toAssistantClientMutationError(
error: unknown,
action: "create" | "update" | "delete" = "update",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Parent client")) {
return { error: "Parent client not found with the given criteria." };
}
return { error: "Client not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A client with this code already exists." };
}
if (action === "delete" && error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
if (error.message.includes("project")) {
return { error: "Client cannot be deleted while it still has projects. Deactivate it instead." };
}
if (error.message.includes("child client")) {
return { error: "Client cannot be deleted while it still has child clients. Remove or reassign them first." };
}
}
return null;
}
function toAssistantOrgUnitNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Parent org unit")) {
return { error: "Parent org unit not found with the given criteria." };
}
return { error: "Org unit not found with the given criteria." };
}
return null;
}
function toAssistantOrgUnitMutationError(
error: unknown,
): AssistantToolErrorResult | null {
const notFound = toAssistantOrgUnitNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (error.message.includes("must be greater than parent level")) {
return { error: "Org unit level must be greater than the parent org unit level." };
}
}
return null;
}
function toAssistantCountryNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Country not found with the given criteria." };
}
return null;
}
function toAssistantCountryMutationError(
error: unknown,
): AssistantToolErrorResult | null {
const notFound = toAssistantCountryNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "A country with this code already exists." };
}
return null;
}
function toAssistantResourceCreationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "CONFLICT") {
return { error: "A resource with this EID or email already exists." };
}
if (error.code === "BAD_REQUEST" || error.code === "UNPROCESSABLE_CONTENT") {
return { error: error.message };
}
if (error.code === "NOT_FOUND") {
if (error.message.includes("Role")) {
return { error: "Role not found with the given criteria." };
}
if (error.message.includes("Country")) {
return { error: "Country not found with the given criteria." };
}
if (error.message.includes("Org unit")) {
return { error: "Org unit not found with the given criteria." };
}
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("country")) {
return { error: "Country not found with the given criteria." };
}
if (errorText.includes("orgunit") || errorText.includes("org_unit") || errorText.includes("org unit")) {
return { error: "Org unit not found with the given criteria." };
}
return { error: "The selected role, country, or org unit no longer exists." };
}
function toAssistantResourceMutationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Resource not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("country")) {
return { error: "Country not found with the given criteria." };
}
if (errorText.includes("orgunit") || errorText.includes("org_unit") || errorText.includes("org unit")) {
return { error: "Org unit not found with the given criteria." };
}
return { error: "Resource not found with the given criteria." };
}
function toAssistantProjectMutationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Project not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Project not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("blueprint")) {
return { error: "Blueprint not found with the given criteria." };
}
if (errorText.includes("client")) {
return { error: "Client not found with the given criteria." };
}
return { error: "Project not found with the given criteria." };
}
function toAssistantMetroCityMutationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Country")) {
return { error: "Country not found with the given criteria." };
}
return { error: "Metro city not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
return { error: "Metro city cannot be deleted while it is still assigned to resources." };
}
return null;
}
function toAssistantDemandCreationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Role")) {
return { error: "Role not found with the given criteria." };
}
return { error: "Project not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
return null;
}
function toAssistantVacationCreationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "FORBIDDEN") {
return { error: "You can only create vacation requests for your own resource." };
}
if (error.code === "BAD_REQUEST") {
return { error: error.message };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
return { error: "Resource not found with the given criteria." };
}
function toAssistantEntitlementMutationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Resource not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code !== "P2003") {
return null;
}
return { error: "Resource not found with the given criteria." };
}
function toAssistantEstimateCreationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Project not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
if (errorText.includes("scopeitem") || errorText.includes("scope_item") || errorText.includes("scope item")) {
return { error: "Estimate scope item not found with the given criteria." };
}
return { error: "One of the referenced project, role, resource, or scope items no longer exists." };
}
function toAssistantEstimateMutationError(
error: unknown,
action:
| "clone"
| "updateDraft"
| "submitVersion"
| "approveVersion"
| "createRevision"
| "createExport"
| "createPlanningHandoff"
| "generateWeeklyPhasing"
| "updateCommercialTerms",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError) {
if (error.code === "NOT_FOUND") {
if (error.message.includes("Linked project")) {
return { error: "Project not found with the given criteria." };
}
if (action === "clone" && error.message === "Source estimate has no versions") {
return { error: "Source estimate has no versions and cannot be cloned." };
}
if (error.message.includes("version") || error.message.includes("versions")) {
return { error: "Estimate version not found with the given criteria." };
}
return { error: "Estimate not found with the given criteria." };
}
if (error.code === "PRECONDITION_FAILED") {
switch (error.message) {
case "Estimate has no working version":
return { error: "Estimate has no working version." };
case "Only working versions can be submitted":
return { error: "Only working versions can be submitted." };
case "Estimate has no submitted version":
return { error: "Estimate has no submitted version." };
case "Only submitted versions can be approved":
return { error: "Only submitted versions can be approved." };
case "Estimate already has a working version":
return { error: "Estimate already has a working version." };
case "Estimate has no locked version to revise":
return { error: "Estimate has no locked version to revise." };
case "Source version must be locked before creating a revision":
return { error: "Source version must be locked before creating a revision." };
case "Estimate has no approved version":
return { error: "Estimate has no approved version." };
case "Only approved versions can be handed off to planning":
return { error: "Only approved versions can be handed off to planning." };
case "Estimate must be linked to a project before planning handoff":
return { error: "Estimate must be linked to a project before planning handoff." };
case "Planning handoff already exists for this approved version":
return { error: "Planning handoff already exists for this approved version." };
case "Linked project has an invalid date range":
return { error: "The linked project has an invalid date range for planning handoff." };
case "Commercial terms can only be edited on working versions":
return { error: "Commercial terms can only be edited on working versions." };
default:
if (error.message.startsWith("Project window has no working days for demand line")) {
return { error: "The linked project window has no working days for at least one demand line." };
}
}
}
if (error.code === "BAD_REQUEST" && action === "updateCommercialTerms") {
return { error: "Commercial terms input is invalid." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code === "P2003") {
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("project")) {
return { error: "Project not found with the given criteria." };
}
if (errorText.includes("role")) {
return { error: "Role not found with the given criteria." };
}
if (errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
if (errorText.includes("scopeitem") || errorText.includes("scope_item") || errorText.includes("scope item")) {
return { error: "Estimate scope item not found with the given criteria." };
}
return { error: "One of the referenced project, role, resource, or scope items no longer exists." };
}
if (prismaError.code === "P2025") {
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("estimatedemandline") || errorText.includes("estimate_demand_line") || errorText.includes("estimate demand line")) {
return { error: "Estimate demand line not found with the given criteria." };
}
if (errorText.includes("estimateversion") || errorText.includes("estimate_version") || errorText.includes("estimate version")) {
return { error: "Estimate version not found with the given criteria." };
}
if (errorText.includes("estimate")) {
return { error: "Estimate not found with the given criteria." };
}
switch (action) {
case "generateWeeklyPhasing":
return { error: "Estimate demand line not found with the given criteria." };
case "updateCommercialTerms":
case "submitVersion":
case "approveVersion":
case "createRevision":
case "createExport":
return { error: "Estimate version not found with the given criteria." };
default:
return { error: "Estimate not found with the given criteria." };
}
}
return null;
}
function toAssistantUserMutationError(
error: unknown,
action: "create" | "update" | "password" = "update",
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "CONFLICT" && action === "create") {
return { error: "User with this email already exists." };
}
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "User not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
const validationIssues = getTrpcValidationIssues(error);
for (const issue of validationIssues) {
const field = issue.path[0];
if (field === "password" && issue.code === "too_small") {
return { error: "Password must be at least 8 characters." };
}
if (field === "name" && issue.code === "too_small") {
return { error: "Name is required." };
}
if (field === "name" && issue.code === "too_big") {
return { error: "Name must be at most 200 characters." };
}
}
if (error.message.includes("Password must be at least 8 characters")) {
return { error: "Password must be at least 8 characters." };
}
if (error.message.includes("Name is required")) {
return { error: "Name is required." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "User not found with the given criteria." };
}
return null;
}
function getTrpcValidationIssues(error: TRPCError): Array<{
code?: string;
path: string[];
}> {
if (error.cause instanceof ZodError) {
return error.cause.issues.map((issue) => ({
code: issue.code,
path: issue.path.map((segment) => String(segment)),
}));
}
try {
const parsed = JSON.parse(error.message);
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.filter((issue): issue is { code?: unknown; path?: unknown } => issue !== null && typeof issue === "object")
.map((issue) => (
typeof issue.code === "string"
? {
code: issue.code,
path: Array.isArray(issue.path) ? issue.path.map((segment) => String(segment)) : [],
}
: {
path: Array.isArray(issue.path) ? issue.path.map((segment) => String(segment)) : [],
}
));
} catch {
return [];
}
}
function toAssistantUserResourceLinkError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
if (error.message.includes("Resource")) {
return { error: "Resource not found with the given criteria." };
}
return { error: "User not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
const pointsToUser =
errorText.includes("userid")
|| errorText.includes("user_id")
|| errorText.includes(" user ");
const pointsToResource =
errorText.includes("resourceid")
|| errorText.includes("resource_id")
|| errorText.includes(" resource ");
if (prismaError.code === "P2025") {
return { error: "Resource not found with the given criteria." };
}
if (prismaError.code === "P2003") {
if (pointsToUser) {
return { error: "User not found with the given criteria." };
}
if (pointsToResource || errorText.includes("resource")) {
return { error: "Resource not found with the given criteria." };
}
return { error: "User not found with the given criteria." };
}
return null;
}
function toAssistantTotpEnableError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (error.message.includes("No TOTP secret generated")) {
return { error: "No TOTP secret generated. Call generate_totp_secret first." };
}
if (error.message.includes("already enabled")) {
return { error: "TOTP is already enabled." };
}
if (error.message.includes("Invalid TOTP token")) {
return { error: "Invalid TOTP token." };
}
}
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "User not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "User not found with the given criteria." };
}
return null;
}
function toAssistantWebhookNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Webhook not found with the given criteria.",
);
}
function toAssistantWebhookMutationError(
error: unknown,
action: "create" | "update" = "update",
): AssistantToolErrorResult | null {
const notFound = toAssistantWebhookNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return {
error: action === "create"
? "Webhook input is invalid."
: "Webhook update input is invalid.",
};
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "Webhook not found with the given criteria." };
}
return null;
}
function toAssistantAuditLogEntryNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Audit log entry not found with the given criteria.",
);
}
function toAssistantTaskNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Task not found with the given criteria.",
);
}
function toAssistantTaskActionError(
error: unknown,
): AssistantToolErrorResult | null {
const notFound = toAssistantTaskNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "PRECONDITION_FAILED") {
if (error.message.includes("no executable action")) {
return { error: "Task has no executable action." };
}
if (error.message.includes("already completed")) {
return { error: "Task is already completed." };
}
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (error.message.includes("Invalid taskAction format") || error.message.includes("Unknown action")) {
return { error: "Task action is invalid and cannot be executed." };
}
if (error.message === "Vacation not found") {
return { error: "Vacation not found with the given criteria." };
}
if (error.message.startsWith("Vacation is ") && error.message.includes(", not PENDING")) {
return { error: "Vacation is not pending and cannot be approved or rejected via this task action." };
}
if (error.message === "Assignment not found") {
return { error: "Assignment not found with the given criteria." };
}
if (error.message === "Assignment is already CONFIRMED") {
return { error: "Assignment is already confirmed." };
}
return { error: error.message };
}
if (error instanceof TRPCError && error.code === "FORBIDDEN") {
return { error: "You do not have permission to execute this task action." };
}
return null;
}
function toAssistantTaskAssignmentError(
error: unknown,
): AssistantToolErrorResult | null {
const notFound = toAssistantTaskNotFoundError(error);
if (notFound) {
return notFound;
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError && (prismaError.code === "P2003" || prismaError.code === "P2025")) {
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("assignee")) {
return { error: "Assignee user not found with the given criteria." };
}
}
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Only tasks and approvals can be assigned." };
}
return null;
}
function toAssistantBroadcastNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Broadcast not found with the given criteria.",
);
}
function toAssistantDispoImportBatchNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Import batch not found with the given criteria.",
);
}
function toAssistantReminderNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotFoundError(
error,
"Reminder not found with the given criteria.",
);
}
function toAssistantNotificationNotFoundError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Notification not found with the given criteria." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (prismaError?.code === "P2025") {
return { error: "Notification not found with the given criteria." };
}
return null;
}
function toAssistantNotificationReadError(
error: unknown,
): AssistantToolErrorResult | null {
return toAssistantNotificationNotFoundError(error);
}
function toAssistantNotificationDeletionError(
error: unknown,
): AssistantToolErrorResult | null {
const notFound = toAssistantNotificationNotFoundError(error);
if (notFound) {
return notFound;
}
if (error instanceof TRPCError && error.code === "FORBIDDEN") {
return { error: "Tasks created by other users cannot be deleted." };
}
return null;
}
function toAssistantReminderCreationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
return { error: "Reminder input is invalid." };
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("user")) {
return { error: "Authenticated user not found with the given criteria." };
}
return null;
}
function toAssistantCommentResolveError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: "Comment not found with the given criteria." };
}
if (error instanceof TRPCError && error.code === "FORBIDDEN") {
return { error: "Only the comment author or an admin can resolve comments." };
}
return null;
}
function toAssistantCommentCreationError(
error: unknown,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
if (error.message.includes("at least 1 character")) {
return { error: "Comment body is required." };
}
if (error.message.includes("at most 10000")) {
return { error: "Comment body must be at most 10000 characters." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("author") || errorText.includes("sender")) {
return { error: "Comment author not found with the given criteria." };
}
if (errorText.includes("user") || errorText.includes("recipient")) {
return { error: "Mentioned user not found with the given criteria." };
}
return null;
}
function getPrismaRequestErrorMetadata(error: unknown): {
code: string;
message: string;
metaText: string;
} | null {
const collectMetaStrings = (value: unknown): string[] => {
if (typeof value === "string") {
return [value];
}
if (Array.isArray(value)) {
return value.flatMap((entry) => collectMetaStrings(entry));
}
if (value && typeof value === "object") {
return Object.values(value).flatMap((entry) => collectMetaStrings(entry));
}
return [];
};
const queue: unknown[] = [error];
const visited = new Set<unknown>();
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined || current === null || visited.has(current)) {
continue;
}
visited.add(current);
if (current instanceof Prisma.PrismaClientKnownRequestError) {
const metaText = Object.values(current.meta ?? {})
.flatMap((value) => collectMetaStrings(value))
.join(" ");
return {
code: current.code,
message: current.message,
metaText,
};
}
if (typeof current !== "object") {
continue;
}
const candidate = current as {
code?: unknown;
message?: unknown;
meta?: Record<string, unknown>;
cause?: unknown;
};
if (typeof candidate.code === "string" && /^P\d{4}$/.test(candidate.code)) {
const metaText = Object.values(candidate.meta ?? {})
.flatMap((value) => collectMetaStrings(value))
.join(" ");
return {
code: candidate.code,
message: typeof candidate.message === "string" ? candidate.message : "",
metaText,
};
}
if ("cause" in candidate) {
queue.push(candidate.cause);
}
}
return null;
}
function getTrpcErrorMetadata(error: unknown): {
code: string;
message: string;
} | null {
const queue: unknown[] = [error];
const visited = new Set<unknown>();
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined || current === null || visited.has(current)) {
continue;
}
visited.add(current);
if (current instanceof TRPCError) {
return {
code: current.code,
message: current.message,
};
}
if (typeof current !== "object") {
continue;
}
const candidate = current as {
code?: unknown;
message?: unknown;
cause?: unknown;
data?: { code?: unknown };
shape?: { code?: unknown; message?: unknown; data?: { cause?: unknown } };
};
const candidateCode = typeof candidate.code === "string"
? candidate.code
: typeof candidate.data?.code === "string"
? candidate.data.code
: typeof candidate.shape?.code === "string"
? candidate.shape.code
: null;
const candidateMessage = typeof candidate.message === "string"
? candidate.message
: typeof candidate.shape?.message === "string"
? candidate.shape.message
: "";
if (candidateCode && /^[A-Z_]+$/.test(candidateCode)) {
return {
code: candidateCode,
message: candidateMessage,
};
}
if ("cause" in candidate) {
queue.push(candidate.cause);
}
if (candidate.shape?.data?.cause) {
queue.push(candidate.shape.data.cause);
}
}
return null;
}
function toAssistantNotificationCreationError(
error: unknown,
context: "notification" | "task" | "broadcast",
): AssistantToolErrorResult | null {
const trpcError = getTrpcErrorMetadata(error);
if (
context === "broadcast"
&& trpcError?.code === "BAD_REQUEST"
&& trpcError.message === "No recipients matched the broadcast target."
) {
return { error: "No recipients matched the broadcast target." };
}
if (trpcError?.code === "NOT_FOUND") {
if (trpcError.message.includes("broadcast")) {
return { error: "Broadcast not found with the given criteria." };
}
if (trpcError.message.includes("Sender user not found")) {
return { error: "Sender user not found with the given criteria." };
}
if (trpcError.message.includes("Assignee user not found")) {
return { error: "Assignee user not found with the given criteria." };
}
if (trpcError.message.includes("recipient")) {
return context === "broadcast"
? { error: "Broadcast recipient user not found with the given criteria." }
: context === "task"
? { error: "Task recipient user not found with the given criteria." }
: { error: "Notification recipient user not found with the given criteria." };
}
}
const prismaError = getPrismaRequestErrorMetadata(error);
if (!prismaError) {
return null;
}
if (prismaError.code !== "P2003" && prismaError.code !== "P2025") {
return null;
}
const errorText = `${prismaError.message} ${prismaError.metaText}`.toLowerCase();
if (errorText.includes("assignee")) {
return { error: "Assignee user not found with the given criteria." };
}
if (errorText.includes("sender")) {
return { error: "Sender user not found with the given criteria." };
}
if (context === "broadcast" && (errorText.includes("notificationbroadcast") || errorText.includes("broadcast"))) {
return { error: "Broadcast not found with the given criteria." };
}
if (context === "broadcast" && prismaError.code === "P2025") {
return { error: "Broadcast not found with the given criteria." };
}
if (context === "task") {
return { error: "Task recipient user not found with the given criteria." };
}
if (context === "broadcast") {
return { error: "Broadcast recipient user not found with the given criteria." };
}
return { error: "Notification recipient user not found with the given criteria." };
}
function normalizeAssistantExecutionError(
error: unknown,
): AssistantToolErrorResult {
if (error instanceof AssistantVisibleError) {
return { error: error.message };
}
const trpcError = getTrpcErrorMetadata(error);
if (trpcError) {
if (trpcError.code === "INTERNAL_SERVER_ERROR") {
return {
error: "The tool could not complete due to an internal error.",
};
}
if (trpcError.code === "UNAUTHORIZED") {
return {
error: "Authentication is required to use this tool.",
};
}
if (trpcError.code === "FORBIDDEN") {
return {
error: "You do not have permission to perform this action.",
};
}
return { error: "The tool could not complete due to a request error." };
}
if (error instanceof Error) {
return { error: "The tool could not complete due to an unexpected error." };
}
return { error: "The tool could not complete due to an unexpected error." };
}
function isAssistantToolErrorResult(
value: unknown,
): value is AssistantToolErrorResult {
return value !== null && typeof value === "object" && "error" in value;
}
function toAssistantIndexedFieldError(
index: number,
field: string,
message: string,
): AssistantIndexedFieldErrorResult {
return {
error: `assignments[${index}].${field}: ${message}`,
field: `assignments[${index}].${field}`,
index,
};
}
async function resolveEntityOrAssistantError<T>(
resolve: () => Promise<T>,
notFoundMessage: string,
): Promise<T | AssistantToolErrorResult> {
try {
return await resolve();
} catch (error) {
const mapped = toAssistantNotFoundError(error, notFoundMessage);
if (mapped) {
return mapped;
}
if (error instanceof TRPCError && error.code === "INTERNAL_SERVER_ERROR") {
return normalizeAssistantExecutionError(error);
}
throw error;
}
}
async function resolveProjectIdentifier(
ctx: ToolContext,
identifier: string,
) {
const caller = createProjectCaller(createScopedCallerContext(ctx));
return resolveEntityOrAssistantError(
() => caller.resolveByIdentifier({ identifier }),
`Project not found: ${identifier}`,
);
}
async function resolveResourceIdentifier(
ctx: ToolContext,
identifier: string,
) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
return resolveEntityOrAssistantError(
() => caller.resolveByIdentifier({ identifier }),
`Resource not found: ${identifier}`,
);
}
function createScopedCallerContext(ctx: ToolContext): TRPCContext {
if (!ctx.session?.user || !ctx.dbUser) {
throw new AssistantVisibleError("Authenticated assistant context is required for this tool.");
}
return {
session: ctx.session,
db: ctx.db,
dbUser: ctx.dbUser,
roleDefaults: ctx.roleDefaults ?? null,
};
}
function sanitizeWebhook<T extends { secret?: string | null }>(webhook: T) {
const { secret: _secret, ...rest } = webhook;
return {
...rest,
hasSecret: Boolean(webhook.secret),
};
}
function sanitizeWebhookList<T extends { secret?: string | null }>(webhooks: T[]) {
return webhooks.map((webhook) => sanitizeWebhook(webhook));
}
// ─── Tool Definitions ───────────────────────────────────────────────────────
export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
// ── READ TOOLS ──
...resourceReadToolDefinitions,
...projectReadToolDefinitions,
...advancedTimelineToolDefinitions,
...allocationPlanningReadToolDefinitions,
...vacationHolidayReadToolDefinitions,
...vacationHolidayMutationToolDefinitions,
...rolesAnalyticsReadToolDefinitions,
...chargeabilityComputationReadToolDefinitions,
...planningNavigationToolDefinitions,
// ── WRITE TOOLS ──
...allocationPlanningMutationToolDefinitions,
...resourceMutationToolDefinitions,
...projectMutationToolDefinitions,
// ── VACATION MANAGEMENT ──
{
type: "function",
function: {
name: "create_vacation",
description: "Create a vacation/leave request through the real vacation workflow. Any authenticated user can request leave for their own resource; manager/admin can create requests for others. Always confirm with the user.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
type: {
type: "string",
enum: ["ANNUAL", "SICK", "OTHER"],
description: "Vacation type. PUBLIC_HOLIDAY requests are managed through holiday calendars, not manual vacation requests.",
},
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
isHalfDay: { type: "boolean", description: "Half day? Default: false" },
halfDayPart: { type: "string", description: "MORNING or AFTERNOON (if half day)" },
note: { type: "string", description: "Optional note" },
},
required: ["resourceId", "type", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "approve_vacation",
description: "Approve a vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "reject_vacation",
description: "Reject a pending vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
reason: { type: "string", description: "Rejection reason" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "cancel_vacation",
description: "Cancel a vacation request through the real vacation workflow. Users can cancel their own requests; manager/admin can cancel any request. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "get_pending_vacation_approvals",
description: "List vacation requests awaiting approval.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_team_vacation_overlap",
description: "Check if team members have overlapping vacations in a date range.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID to check overlap for" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["resourceId", "startDate", "endDate"],
},
},
},
// ── ENTITLEMENT ──
{
type: "function",
function: {
name: "get_entitlement_summary",
description: "Get vacation entitlement year summary for all resources or a specific resource.",
parameters: {
type: "object",
properties: {
year: { type: "integer", description: "Year. Default: current year" },
resourceName: { type: "string", description: "Filter by resource name (optional)" },
},
},
},
},
{
type: "function",
function: {
name: "set_entitlement",
description: "Set the annual vacation entitlement for a resource/year through the real entitlement workflow. Manager or admin role required. Carryover is computed automatically. Always confirm first.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
year: { type: "integer", description: "Year" },
entitledDays: { type: "number", description: "Number of entitled vacation days" },
},
required: ["resourceId", "year", "entitledDays"],
},
},
},
// ── DEMAND / STAFFING ──
...staffingDemandReadToolDefinitions,
...staffingDemandMutationToolDefinitions,
// ── BLUEPRINT ──
...blueprintsRateCardsToolDefinitions,
// ── ESTIMATES ──
...estimateReadToolDefinitions,
...estimateMutationToolDefinitions,
// ── ROLES ──
...rolesAnalyticsMutationToolDefinitions,
// ── CLIENTS ──
...clientMutationToolDefinitions,
// ── ADMIN / CONFIG READ TOOLS ──
...countryReadmodelToolDefinitions,
...countryMetroAdminToolDefinitions,
...configReadmodelToolDefinitions,
...userAdminToolDefinitions,
...userSelfServiceToolDefinitions,
...notificationInboxToolDefinitions,
...dashboardInsightsReportsToolDefinitions,
// ── ORG UNIT MANAGEMENT ──
...orgUnitMutationToolDefinitions,
// ── TASK MANAGEMENT ──
...notificationTaskToolDefinitions,
...commentReadToolDefinitions,
...scenarioRateAnalysisToolDefinitions,
...commentMutationToolDefinitions,
...auditHistoryToolDefinitions,
...importExportDispoToolDefinitions,
...settingsAdminToolDefinitions,
], LEGACY_MONOLITHIC_TOOL_ACCESS);
// ─── Helpers ────────────────────────────────────────────────────────────────
/** Resolve a responsible person name against existing resources. Returns the exact displayName or an error object. */
async function resolveResponsiblePerson(
name: string,
ctx: ToolContext,
): Promise<{ displayName: string } | { error: string }> {
const caller = createResourceCaller(createScopedCallerContext(ctx));
const result = await caller.resolveResponsiblePersonName({ name });
if (result.status === "resolved") {
return { displayName: result.displayName };
}
if (result.status === "ambiguous" || result.status === "missing") {
return { error: result.message };
}
return { error: `Unable to resolve responsible person: ${name}` };
}
// ─── Tool Executors ─────────────────────────────────────────────────────────
const executors = {
...createResourceExecutors({
assertPermission,
createResourceCaller,
createRoleCaller,
createCountryCaller,
createOrgUnitCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
toAssistantResourceMutationError,
toAssistantResourceCreationError,
}),
...createProjectExecutors({
assertPermission,
createProjectCaller,
createBlueprintCaller,
createClientCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResponsiblePerson,
resolveEntityOrAssistantError,
toAssistantNotFoundError,
toAssistantProjectMutationError,
toAssistantProjectCreationError,
toAssistantProjectNotFoundError,
}),
...createStaffingDemandExecutors({
assertPermission,
createAllocationCaller,
createStaffingCaller,
createRoleCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
parseIsoDate,
parseOptionalIsoDate,
fmtDate,
toAssistantDemandCreationError,
toAssistantDemandFillError,
}),
...createAdvancedTimelineExecutors({
assertPermission,
createStaffingCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
parseIsoDate,
fmtDate,
isAssistantToolErrorResult,
toAssistantIndexedFieldError,
toAssistantTimelineMutationError,
}),
...createAllocationPlanningExecutors({
assertPermission,
createAllocationCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
parseIsoDate,
parseOptionalIsoDate,
fmtDate,
toAssistantAllocationNotFoundError,
}),
...createVacationHolidayExecutors({
createEntitlementCaller,
createVacationCaller,
createHolidayCalendarCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveHolidayPeriod,
resolveEntityOrAssistantError,
assertAdminRole,
fmtDate,
formatHolidayCalendar,
formatHolidayCalendarEntry,
toAssistantHolidayCalendarMutationError,
toAssistantHolidayCalendarNotFoundError,
toAssistantHolidayEntryMutationError,
toAssistantHolidayEntryNotFoundError,
}),
...createRolesAnalyticsExecutors({
createRoleCaller,
createResourceCaller,
createDashboardCaller,
createScopedCallerContext,
resolveResourceIdentifier,
toAssistantRoleMutationError,
}),
...createClientsOrgUnitsExecutors({
createClientCaller,
createOrgUnitCaller,
createScopedCallerContext,
toAssistantClientMutationError,
toAssistantOrgUnitMutationError,
}),
...createChargeabilityComputationExecutors({
assertPermission,
createChargeabilityReportCaller,
createComputationGraphCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveProjectIdentifier,
}),
...createBlueprintsRateCardsExecutors({
createBlueprintCaller,
createRateCardCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
parseOptionalIsoDate,
fmtDate,
}),
...createDashboardInsightsReportsExecutors({
assertPermission,
createDashboardCaller,
createInsightsCaller,
createReportCaller,
createScopedCallerContext,
}),
...createPlanningNavigationExecutors({
createEstimateCaller,
createClientCaller,
createOrgUnitCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
parseIsoDate,
}),
...createScenarioRateAnalysisExecutors({
assertPermission,
createRateCardCaller,
createScenarioCaller,
createInsightsCaller,
createScopedCallerContext,
fmtEur,
}),
...createCommentExecutors({
createCommentCaller,
createScopedCallerContext,
toAssistantCommentCreationError,
toAssistantCommentResolveError,
}),
...createAuditHistoryExecutors({
createAuditLogCaller,
createScopedCallerContext,
}),
// ── VACATION MANAGEMENT ──
async create_vacation(params: {
resourceId: string; type: string;
startDate: string; endDate: string;
isHalfDay?: boolean; halfDayPart?: string; note?: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createVacationCaller(createScopedCallerContext(ctx));
const type = parseAssistantVacationRequestType(params.type);
let vacation;
try {
vacation = await caller.create({
resourceId: resource.id,
type,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
...(params.isHalfDay !== undefined ? { isHalfDay: params.isHalfDay } : {}),
...(params.halfDayPart !== undefined ? { halfDayPart: params.halfDayPart as "MORNING" | "AFTERNOON" } : {}),
...(params.note !== undefined ? { note: params.note } : {}),
});
} catch (error) {
const mapped = toAssistantVacationCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
const effectiveDays = "effectiveDays" in vacation && typeof vacation.effectiveDays === "number"
? vacation.effectiveDays
: null;
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
message: `Created ${type} for ${resource.displayName}: ${params.startDate} to ${params.endDate} (status: ${vacation.status}${effectiveDays !== null ? `, deducted ${effectiveDays} day(s)` : ""})`,
vacationId: vacation.id,
vacation,
};
},
async approve_vacation(params: { vacationId: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "approve");
if (mapped) {
return mapped;
}
throw error;
}
let approved;
try {
approved = await caller.approve({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "approve");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
warnings: approved.warnings,
vacation: approved,
message: `Approved vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async reject_vacation(params: { vacationId: string; reason?: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "reject");
if (mapped) {
return mapped;
}
throw error;
}
let rejected;
try {
rejected = await caller.reject({
id: params.vacationId,
...(params.reason !== undefined ? { rejectionReason: params.reason } : {}),
});
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "reject");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: rejected,
message: `Rejected vacation for ${existing.resource?.displayName ?? params.vacationId}${params.reason ? `: ${params.reason}` : ""}`,
};
},
async cancel_vacation(params: { vacationId: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
let existing;
try {
existing = await caller.getById({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "cancel");
if (mapped) {
return mapped;
}
throw error;
}
let cancelled;
try {
cancelled = await caller.cancel({ id: params.vacationId });
} catch (error) {
const mapped = toAssistantVacationMutationError(error, "cancel");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: cancelled,
message: `Cancelled vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async get_pending_vacation_approvals(params: { limit?: number }, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const caller = createVacationCaller(createScopedCallerContext(ctx));
const vacations = await caller.getPendingApprovals();
return vacations.map((v) => ({
id: v.id,
resource: v.resource.displayName,
eid: v.resource.eid,
chapter: v.resource.chapter,
type: v.type,
start: fmtDate(v.startDate),
end: fmtDate(v.endDate),
isHalfDay: v.isHalfDay,
})).slice(0, limit);
},
async get_team_vacation_overlap(params: {
resourceId: string; startDate: string; endDate: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createVacationCaller(createScopedCallerContext(ctx));
return caller.getTeamOverlapDetail({
resourceId: resource.id,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
});
},
// ── ENTITLEMENT ──
async get_entitlement_summary(params: { year?: number; resourceName?: string }, ctx: ToolContext) {
const year = params.year ?? new Date().getFullYear();
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
return caller.getYearSummaryDetail({
year,
...(params.resourceName ? { resourceName: params.resourceName } : {}),
});
},
async set_entitlement(params: {
resourceId: string; year: number; entitledDays: number; carryoverDays?: number;
}, ctx: ToolContext) {
if (params.carryoverDays !== undefined) {
return {
error: "Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.",
};
}
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
let entitlement;
try {
entitlement = await caller.set({
resourceId: resource.id,
year: params.year,
entitledDays: params.entitledDays,
});
} catch (error) {
const mapped = toAssistantEntitlementMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
entitlement,
message: `Set entitlement for ${resource.displayName} (${params.year}): ${params.entitledDays} days`,
};
},
// ── ESTIMATES ──
...createEstimateExecutors({
assertPermission,
createEstimateCaller,
createScopedCallerContext,
resolveProjectIdentifier,
toAssistantEstimateNotFoundError,
toAssistantEstimateReadError,
toAssistantEstimateCreationError,
toAssistantEstimateMutationError,
}),
// ── ROLES ──
// ── CLIENTS ──
// ── ADMIN / CONFIG ──
...createCountryReadmodelExecutors({
createCountryCaller,
createScopedCallerContext,
formatCountry,
toAssistantCountryNotFoundError,
}),
...createCountryMetroAdminExecutors({
createCountryCaller,
createScopedCallerContext,
assertAdminRole,
formatCountry,
toAssistantCountryMutationError,
toAssistantMetroCityMutationError,
}),
...createConfigReadmodelExecutors({
createManagementLevelCaller,
createUtilizationCategoryCaller,
createCalculationRuleCaller,
createEffortRuleCaller,
createExperienceMultiplierCaller,
createScopedCallerContext,
}),
...createUserAdminExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantUserMutationError,
toAssistantUserResourceLinkError,
}),
...createUserSelfServiceExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantTotpEnableError,
}),
...createNotificationsTasksExecutors({
createNotificationCaller,
createScopedCallerContext,
parseDateTime,
parseOptionalDateTime,
toAssistantTaskNotFoundError,
toAssistantTaskActionError,
toAssistantTaskAssignmentError,
toAssistantBroadcastNotFoundError,
toAssistantReminderNotFoundError,
toAssistantNotificationReadError,
toAssistantNotificationDeletionError,
toAssistantReminderCreationError,
toAssistantNotificationCreationError,
}),
...createImportExportDispoExecutors({
assertPermission,
createImportExportCaller,
createDispoCaller,
createScopedCallerContext,
toAssistantDispoImportBatchNotFoundError,
}),
...createSettingsAdminExecutors({
createSettingsCaller,
createSystemRoleConfigCaller,
createWebhookCaller,
createAuditLogCaller,
createProjectCaller,
createScopedCallerContext,
parseIsoDate,
resolveProjectIdentifier,
sanitizeWebhook,
sanitizeWebhookList,
toAssistantWebhookNotFoundError,
toAssistantWebhookMutationError,
toAssistantAuditLogEntryNotFoundError,
}),
};
// ─── Executor ───────────────────────────────────────────────────────────────
export interface ToolAction {
type: string;
url?: string;
scope?: string[];
description?: string;
}
export interface ToolResult {
content: string;
action?: ToolAction;
data?: unknown;
}
export async function executeTool(
name: string,
args: string,
ctx: ToolContext,
): Promise<ToolResult> {
const executor = executors[name as keyof typeof executors];
if (!executor) return { content: JSON.stringify({ error: `Unknown tool: ${name}` }) };
try {
const params = JSON.parse(args);
// Audit-log all mutation tool executions (EGAI 4.1.3.1 / IAAI 3.6.26)
if (MUTATION_TOOLS.has(name)) {
logger.info(
{ tool: name, params, userId: ctx.userId, userRole: ctx.userRole },
"AI assistant mutation tool executed",
);
}
const result = await executor(params, ctx);
// Detect action payloads (e.g. navigation, invalidation)
if (result && typeof result === "object" && "__action" in (result as Record<string, unknown>)) {
const actionResult = result as Record<string, unknown>;
const actionType = actionResult.__action as string;
if (actionType === "navigate") {
const url = actionResult.url as string;
const desc = (actionResult.description as string | undefined) ?? url;
return {
content: JSON.stringify({ description: desc }),
data: { description: desc },
action: { type: "navigate", url, description: desc },
};
}
if (actionType === "invalidate") {
const scope = actionResult.scope as string[];
// Strip __action, scope, and large data from the result sent back to the AI
const { __action: _, scope: _s, coverImageUrl: _img, ...rest } = actionResult;
const content = JSON.stringify(rest);
return {
content: content.length > 4000 ? content.slice(0, 4000) + '..."' : content,
data: rest,
action: { type: "invalidate", scope },
};
}
}
// Cap tool result size to prevent oversized OpenAI conversation payloads
const content = typeof result === "string" ? result : JSON.stringify(result);
return {
content: content.length > 8000 ? content.slice(0, 8000) + '..."' : content,
...(typeof result === "string" ? {} : { data: result }),
};
} catch (err) {
const normalizedError = normalizeAssistantExecutionError(err);
logger.error(
{
tool: name,
userId: ctx.userId,
userRole: ctx.userRole,
error: err instanceof Error ? { message: err.message, stack: err.stack } : err,
},
"AI assistant tool execution failed",
);
return { content: JSON.stringify(normalizedError), data: normalizedError };
}
}