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

7061 lines
240 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, ImportBatchStatus, StagedRecordStatus, DispoStagedRecordType, VacationType } from "@capakraken/db";
import {
CreateAssignmentSchema,
type CreateEstimateInput,
CreateProjectSchema,
CreateResourceSchema,
AllocationStatus,
EstimateExportFormat,
EstimateStatus,
type CommentEntityType,
COMMENT_ENTITY_TYPE_VALUES,
PermissionKey,
SystemRole,
type UpdateEstimateDraftInput,
UpdateProjectSchema,
UpdateResourceSchema,
} 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 {
countryMetroAdminToolDefinitions,
createCountryMetroAdminExecutors,
} from "./assistant-tools/country-metro-admin.js";
import type { ToolContext, ToolDef, ToolExecutor } from "./assistant-tools/shared.js";
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../lib/comment-entity-registry.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 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[] = [
// ── READ TOOLS ──
{
type: "function",
function: {
name: "search_resources",
description: "Search for resources (employees) by name, employee ID, chapter, country, metro city, org unit, or role. Resource overview access required. Returns a list of matching resources with key details.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search term (matches displayName, eid, chapter)" },
country: { type: "string", description: "Filter by country name or code (e.g. 'Spain', 'ES', 'Deutschland', 'DE')" },
metroCity: { type: "string", description: "Filter by metro city name (e.g. 'Madrid', 'München')" },
orgUnit: { type: "string", description: "Filter by org unit name (partial match)" },
roleName: { type: "string", description: "Filter by role name (partial match)" },
isActive: { type: "boolean", description: "Filter by active status. Default: true" },
limit: { type: "integer", description: "Max results. Default: 50" },
},
},
},
},
{
type: "function",
function: {
name: "get_resource",
description: "Get detailed information about a single resource by ID, employee ID (eid), or name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Resource ID, employee ID (eid like EMP-001), or display name" },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "search_projects",
description: "Search for projects by name, short code, status, or client.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search term (matches name, shortCode)" },
status: { type: "string", description: "Filter by status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_project",
description: "Get detailed information about a single project by ID or short code, including top allocations.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Project ID or short code (e.g. Z033T593)" },
},
required: ["identifier"],
},
},
},
...advancedTimelineToolDefinitions,
...allocationPlanningReadToolDefinitions,
...vacationHolidayReadToolDefinitions,
...vacationHolidayMutationToolDefinitions,
...rolesAnalyticsReadToolDefinitions,
...chargeabilityComputationReadToolDefinitions,
{
type: "function",
function: {
name: "search_estimates",
description: "Search for estimates (cost/effort estimates) by project or name. Returns estimate name, status, version count.",
parameters: {
type: "object",
properties: {
projectCode: { type: "string", description: "Project short code to filter by" },
query: { type: "string", description: "Search term (matches estimate name)" },
status: { type: "string", description: "Filter by status: DRAFT, IN_REVIEW, APPROVED, ARCHIVED" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "list_clients",
description: "List clients/customers. Can search by name or code.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search term (matches name or code)" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "list_org_units",
description: "List organizational units (departments, teams) with their hierarchy.",
parameters: {
type: "object",
properties: {
level: { type: "integer", description: "Filter by org level (5, 6, or 7)" },
},
},
},
},
// ── NAVIGATION TOOLS ──
{
type: "function",
function: {
name: "get_my_timeline_entries_view",
description: "Get the caller's own self-service timeline entries view for a date range using the real timeline self-service endpoint. Returns only data for the caller's linked resource.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
resourceIds: { type: "array", items: { type: "string" }, description: "Optional filters are accepted but will be scoped to the caller's own linked resource." },
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to narrow the caller's own timeline view." },
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to narrow the caller's own timeline view." },
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters. Self-service scoping still applies." },
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs. Self-service scoping still applies." },
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes. Self-service scoping still applies." },
},
required: ["startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "get_my_timeline_holiday_overlays",
description: "Get the caller's own self-service holiday overlays for a date range using the real timeline self-service endpoint. Returns only holiday overlays for the caller's linked resource.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
resourceIds: { type: "array", items: { type: "string" }, description: "Optional filters are accepted but will be scoped to the caller's own linked resource." },
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to narrow the caller's own holiday overlay view." },
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to narrow the caller's own holiday overlay view." },
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters. Self-service scoping still applies." },
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs. Self-service scoping still applies." },
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes. Self-service scoping still applies." },
},
required: ["startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "navigate_to_page",
description: "Navigate the user to a specific page in CapaKraken, optionally with filters. Use this when the user wants to see data on a specific page (e.g. 'show me on the timeline', 'open the resources page').",
parameters: {
type: "object",
properties: {
page: {
type: "string",
description: "Page name: timeline, dashboard, resources, projects, allocations, staffing, estimates, vacations, my-vacations, roles, skills-analytics, chargeability, computation-graph",
},
eids: { type: "string", description: "Comma-separated employee IDs to filter (for timeline)" },
chapters: { type: "string", description: "Comma-separated chapters to filter (for timeline)" },
projectIds: { type: "string", description: "Comma-separated project IDs to filter (for timeline)" },
clientIds: { type: "string", description: "Comma-separated client IDs to filter (for timeline)" },
countryCodes: { type: "string", description: "Comma-separated country codes to filter (e.g. 'ES,DE' for Spain and Germany, for timeline)" },
startDate: { type: "string", description: "Start date YYYY-MM-DD (for timeline)" },
days: { type: "integer", description: "Number of days to show (for timeline)" },
},
required: ["page"],
},
},
},
// ── WRITE TOOLS ──
...allocationPlanningMutationToolDefinitions,
{
type: "function",
function: {
name: "update_resource",
description: "Update a resource's details. Requires manageResources permission. Always confirm with the user before calling this.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Resource ID, EID, or display name" },
displayName: { type: "string", description: "New display name" },
fte: { type: "number", description: "New FTE (0.0-1.0)" },
lcrCents: { type: "integer", description: "New LCR in cents (e.g. 8500 = 85.00 EUR/h)" },
chapter: { type: "string", description: "New chapter" },
chargeabilityTarget: { type: "number", description: "New chargeability target (0-100)" },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "update_project",
description: "Update a project's details. Requires manageProjects permission. Always confirm with the user before calling this.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Project ID, short code, or project name" },
name: { type: "string", description: "New project name" },
budgetCents: { type: "integer", description: "New budget in cents (e.g. 10000000 = 100,000 EUR)" },
winProbability: { type: "integer", description: "Win probability 0-100" },
status: { type: "string", description: "New status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_project",
description: "Create a new project. Requires manageProjects permission. Always confirm with the user before calling this. The project is created in DRAFT status by default.",
parameters: {
type: "object",
properties: {
shortCode: { type: "string", description: "Unique project code, uppercase alphanumeric with hyphens/underscores (e.g. 'PROJ-001')" },
name: { type: "string", description: "Project name" },
orderType: { type: "string", description: "Order type: BD, CHARGEABLE, INTERNAL, OVERHEAD" },
allocationType: { type: "string", description: "Allocation type: INT or EXT. Default: INT" },
budgetCents: { type: "integer", description: "Budget in cents (e.g. 10000000 = 100,000 EUR)" },
startDate: { type: "string", description: "Start date (YYYY-MM-DD)" },
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
winProbability: { type: "integer", description: "Win probability 0-100. Default: 100" },
status: { type: "string", description: "Initial status: DRAFT, ACTIVE, ON_HOLD. Default: DRAFT" },
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
color: { type: "string", description: "Hex color (e.g. '#3b82f6')" },
blueprintName: { type: "string", description: "Blueprint name to look up and attach (partial match)" },
clientName: { type: "string", description: "Client name to look up and attach (partial match)" },
},
required: ["shortCode", "name", "orderType", "budgetCents", "startDate", "endDate", "responsiblePerson"],
},
},
},
// ── RESOURCE MANAGEMENT ──
{
type: "function",
function: {
name: "create_resource",
description: "Create a new resource (employee). Requires manageResources permission. Always confirm with the user before calling.",
parameters: {
type: "object",
properties: {
eid: { type: "string", description: "Employee ID (e.g. EMP-042)" },
displayName: { type: "string", description: "Full name" },
email: { type: "string", description: "Email address" },
fte: { type: "number", description: "FTE 0.0-1.0. Default: 1" },
lcrCents: { type: "integer", description: "Loaded cost rate in cents (e.g. 8500 = 85 EUR/h)" },
ucrCents: { type: "integer", description: "Unloaded cost rate in cents" },
chapter: { type: "string", description: "Chapter/team name" },
chargeabilityTarget: { type: "number", description: "Chargeability target 0-100. Default: 80" },
roleName: { type: "string", description: "Role name (partial match)" },
countryCode: { type: "string", description: "Country code (e.g. DE, ES)" },
orgUnitName: { type: "string", description: "Org unit name (partial match)" },
postalCode: { type: "string", description: "Postal code" },
},
required: ["eid", "displayName", "email", "lcrCents"],
},
},
},
{
type: "function",
function: {
name: "deactivate_resource",
description: "Deactivate a resource (soft delete). Requires manageResources permission. Always confirm first.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Resource ID, eid, or name" },
},
required: ["identifier"],
},
},
},
// ── 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 ──
{
type: "function",
function: {
name: "list_demands",
description: "List staffing demand requirements for projects. Shows open positions that need to be filled.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Filter by project ID or short code" },
status: { type: "string", description: "Filter by status: OPEN, PARTIALLY_FILLED, FILLED, CANCELLED" },
limit: { type: "integer", description: "Max results. Default: 30" },
},
},
},
},
{
type: "function",
function: {
name: "create_demand",
description: "Create a staffing demand requirement on a project. Requires manageAllocations permission. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
roleName: { type: "string", description: "Role name for the demand" },
headcount: { type: "integer", description: "Number of people needed. Default: 1" },
hoursPerDay: { type: "number", description: "Hours per day required" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["projectId", "roleName", "hoursPerDay", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "fill_demand",
description: "Fill/assign a resource to an open demand requirement. Requires manageAllocations permission. Always confirm first.",
parameters: {
type: "object",
properties: {
demandId: { type: "string", description: "Demand requirement ID" },
resourceId: { type: "string", description: "Resource ID or name to assign" },
},
required: ["demandId", "resourceId"],
},
},
},
{
type: "function",
function: {
name: "check_resource_availability",
description: "Check if a resource is available in a given date range (no conflicting allocations or vacations).",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, eid, or name" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["resourceId", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "get_staffing_suggestions",
description: "Get AI-powered staffing suggestions for a project based on required skills, availability, and cost.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
roleName: { type: "string", description: "Role to find candidates for" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
limit: { type: "integer", description: "Max suggestions. Default: 5" },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "find_capacity",
description: "Find resources with available capacity in a date range.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
minHoursPerDay: { type: "number", description: "Minimum available hours/day. Default: 4" },
roleName: { type: "string", description: "Filter by role name" },
chapter: { type: "string", description: "Filter by chapter" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
required: ["startDate", "endDate"],
},
},
},
// ── BLUEPRINT ──
{
type: "function",
function: {
name: "list_blueprints",
description: "List available project blueprints with their field definitions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_blueprint",
description: "Get detailed blueprint with all field definitions and role presets.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
},
required: ["identifier"],
},
},
},
// ── RATE CARDS ──
{
type: "function",
function: {
name: "list_rate_cards",
description: "List rate cards with their effective dates and line items.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search by name" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "resolve_rate",
description: "Look up the applicable rate for a resource, role, or management level from rate cards.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID or name" },
roleName: { type: "string", description: "Role name" },
date: { type: "string", description: "Date to check rate for (YYYY-MM-DD). Default: today" },
},
},
},
},
// ── ESTIMATES ──
{
type: "function",
function: {
name: "get_estimate_detail",
description: "Get one estimate via the real estimate router, including versions, demand lines, metrics, assumptions, and linked project data. Controller/manager/admin access and viewCosts required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID" },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "list_estimate_versions",
description: "List estimate versions via the real estimate router, including status, timestamps, and artifact counts. Controller/manager/admin access required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID" },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "get_estimate_version_snapshot",
description: "Get an estimate version snapshot via the real estimate router, including totals, breakdowns, exports, and resource snapshots. Controller/manager/admin access and viewCosts required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID" },
versionId: { type: "string", description: "Optional explicit version ID. Defaults to the latest version." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "create_estimate",
description: "Create a new estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Optional project ID." },
projectCode: { type: "string", description: "Optional project short code convenience alias." },
name: { type: "string", description: "Estimate name." },
opportunityId: { type: "string", description: "Optional opportunity/reference ID." },
baseCurrency: { type: "string", description: "Base currency, e.g. EUR." },
status: { type: "string", enum: ["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"] },
versionLabel: { type: "string", description: "Optional working version label." },
versionNotes: { type: "string", description: "Optional working version notes." },
assumptions: { type: "array", items: { type: "object" }, description: "Estimate assumptions." },
scopeItems: { type: "array", items: { type: "object" }, description: "Estimate scope items." },
demandLines: { type: "array", items: { type: "object" }, description: "Estimate demand lines." },
resourceSnapshots: { type: "array", items: { type: "object" }, description: "Resource cost snapshots." },
metrics: { type: "array", items: { type: "object" }, description: "Optional metric overrides." },
},
required: ["name"],
},
},
},
{
type: "function",
function: {
name: "clone_estimate",
description: "Clone an existing estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
sourceEstimateId: { type: "string", description: "Source estimate ID." },
name: { type: "string", description: "Optional cloned estimate name." },
projectId: { type: "string", description: "Optional target project ID." },
projectCode: { type: "string", description: "Optional target project short code convenience alias." },
},
required: ["sourceEstimateId"],
},
},
},
{
type: "function",
function: {
name: "update_estimate_draft",
description: "Update the working draft of an estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Estimate ID." },
projectId: { type: "string", description: "Optional linked project ID." },
projectCode: { type: "string", description: "Optional linked project short code convenience alias." },
name: { type: "string" },
opportunityId: { type: "string" },
baseCurrency: { type: "string" },
status: { type: "string", enum: ["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"] },
versionLabel: { type: "string" },
versionNotes: { type: "string" },
assumptions: { type: "array", items: { type: "object" } },
scopeItems: { type: "array", items: { type: "object" } },
demandLines: { type: "array", items: { type: "object" } },
resourceSnapshots: { type: "array", items: { type: "object" } },
metrics: { type: "array", items: { type: "object" } },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "submit_estimate_version",
description: "Submit an estimate working version for review via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "approve_estimate_version",
description: "Approve a submitted estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "create_estimate_revision",
description: "Create a new working revision from the latest locked estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
sourceVersionId: { type: "string", description: "Optional source version ID." },
label: { type: "string", description: "Optional revision label." },
notes: { type: "string", description: "Optional revision notes." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "create_estimate_export",
description: "Create an estimate export artifact via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
format: { type: "string", enum: ["XLSX", "CSV", "JSON", "SAP", "MMP"], description: "Export format." },
},
required: ["estimateId", "format"],
},
},
},
{
type: "function",
function: {
name: "create_estimate_planning_handoff",
description: "Create planning allocations from an approved estimate version via the real estimate router. Manager/admin role and manageAllocations permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit approved version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "generate_estimate_weekly_phasing",
description: "Generate weekly phasing for the working estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
pattern: { type: "string", enum: ["even", "front_loaded", "back_loaded", "custom"], description: "Distribution pattern." },
},
required: ["estimateId", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "get_estimate_weekly_phasing",
description: "Get generated weekly phasing for an estimate via the real estimate router. Controller/manager/admin access required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "get_estimate_commercial_terms",
description: "Get estimate commercial terms via the real estimate router. Controller/manager/admin access required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "update_estimate_commercial_terms",
description: "Update estimate commercial terms on a working version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
terms: { type: "object", description: "Commercial terms payload." },
},
required: ["estimateId", "terms"],
},
},
},
// ── ROLES ──
...rolesAnalyticsMutationToolDefinitions,
// ── CLIENTS ──
...clientMutationToolDefinitions,
// ── ADMIN / CONFIG READ TOOLS ──
{
type: "function",
function: {
name: "list_countries",
description: "List countries including working hours, schedule rules, active flag, and metro cities.",
parameters: {
type: "object",
properties: {
includeInactive: { type: "boolean", description: "Include inactive countries. Default: false." },
search: { type: "string", description: "Optional country code or name search." },
},
},
},
},
{
type: "function",
function: {
name: "get_country",
description: "Get one country with schedule rules, active flag, metro cities, and resource count. Accepts ID, code, or name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Country ID, code, or name." },
},
required: ["identifier"],
},
},
},
...countryMetroAdminToolDefinitions,
{
type: "function",
function: {
name: "list_management_levels",
description: "List management level groups and their levels with target percentages.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_utilization_categories",
description: "List utilization categories (cost classification for projects).",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_calculation_rules",
description: "List calculation rules for cost attribution and chargeability.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_effort_rules",
description: "List effort estimation rules with their formulas and conditions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_experience_multipliers",
description: "List experience multipliers that adjust effort estimates based on seniority.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_users",
description: "List all system users via the admin user router, including role and MFA state. Admin role required.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 50" },
},
},
},
},
{
type: "function",
function: {
name: "list_assignable_users",
description: "List lightweight users available for assignment workflows. Manager or admin role required.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_current_user",
description: "Get the authenticated user's own profile, role, and permission overrides.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_dashboard_layout",
description: "Get the authenticated user's saved dashboard widget layout and last update timestamp.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "save_dashboard_layout",
description: "Save the authenticated user's dashboard layout. Always confirm first.",
parameters: {
type: "object",
properties: {
layout: {
type: "array",
description: "Dashboard layout items as stored by the user router.",
items: { type: "object" },
},
},
required: ["layout"],
},
},
},
{
type: "function",
function: {
name: "get_favorite_project_ids",
description: "Get the authenticated user's favorite project IDs.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "toggle_favorite_project",
description: "Add or remove a project from the authenticated user's favorites. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID." },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "get_column_preferences",
description: "Get the authenticated user's saved table column preferences for all supported views.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "set_column_preferences",
description: "Update the authenticated user's table column preferences for one view. Always confirm first.",
parameters: {
type: "object",
properties: {
view: {
type: "string",
enum: ["resources", "projects", "allocations", "vacations", "roles", "users", "blueprints"],
description: "View key to update.",
},
visible: {
type: "array",
items: { type: "string" },
description: "Visible column IDs.",
},
sort: {
type: ["object", "null"],
properties: {
field: { type: "string" },
dir: { type: "string", enum: ["asc", "desc"] },
},
description: "Sort state. Use null to clear it.",
},
rowOrder: {
type: ["array", "null"],
items: { type: "string" },
description: "Optional row order. Use null to clear it.",
},
},
required: ["view"],
},
},
},
{
type: "function",
function: {
name: "generate_totp_secret",
description: "Generate a new MFA TOTP secret and provisioning URI for the authenticated user. Always confirm first. The secret is sensitive.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "verify_and_enable_totp",
description: "Verify a 6-digit MFA TOTP token and enable MFA for the authenticated user. Always confirm first.",
parameters: {
type: "object",
properties: {
token: { type: "string", description: "6-digit TOTP token." },
},
required: ["token"],
},
},
},
{
type: "function",
function: {
name: "get_mfa_status",
description: "Get the authenticated user's MFA status.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_active_user_count",
description: "Get the number of users active in the last five minutes. Admin role required.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "create_user",
description: "Create a new system user and auto-link a matching resource by email when possible. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
email: { type: "string", description: "User email address." },
name: { type: "string", description: "Display name." },
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"], description: "Initial system role." },
password: { type: "string", description: "Initial password, minimum 8 characters." },
},
required: ["email", "name", "password"],
},
},
},
{
type: "function",
function: {
name: "set_user_password",
description: "Reset a user's password. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
password: { type: "string", description: "New password, minimum 8 characters." },
},
required: ["userId", "password"],
},
},
},
{
type: "function",
function: {
name: "update_user_role",
description: "Change a user's system role. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "User ID." },
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
},
required: ["id", "systemRole"],
},
},
},
{
type: "function",
function: {
name: "update_user_name",
description: "Rename a user. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "User ID." },
name: { type: "string", description: "New display name." },
},
required: ["id", "name"],
},
},
},
{
type: "function",
function: {
name: "link_user_resource",
description: "Link or unlink a user to a resource. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
resourceId: { type: ["string", "null"], description: "Resource ID or null to unlink." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "auto_link_users_by_email",
description: "Auto-link all users without a resource to matching resources by email. Admin role required. Always confirm first.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "set_user_permissions",
description: "Set explicit permission overrides for a user. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
overrides: {
type: ["object", "null"],
properties: {
granted: { type: "array", items: { type: "string" } },
denied: { type: "array", items: { type: "string" } },
chapterIds: { type: "array", items: { type: "string" } },
},
description: "Permission override object or null to clear.",
},
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "reset_user_permissions",
description: "Reset a user's permission overrides back to role defaults. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "get_effective_user_permissions",
description: "Get a user's resolved permissions, role, and explicit overrides. Admin role required.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "disable_user_totp",
description: "Disable MFA TOTP for a user as an admin override. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "list_notifications",
description: "List recent notifications for the current user.",
parameters: {
type: "object",
properties: {
unreadOnly: { type: "boolean", description: "Only show unread. Default: false" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "mark_notification_read",
description: "Mark one notification as read, or all unread notifications when no notificationId is provided. Always confirm first.",
parameters: {
type: "object",
properties: {
notificationId: { type: "string", description: "Notification ID. Omit to mark all unread notifications as read." },
},
},
},
},
{
type: "function",
function: {
name: "get_unread_notification_count",
description: "Count unread notifications for the current user.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "create_notification",
description: "Create a notification or task-style notification for a specific user. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "Target user ID." },
type: { type: "string", description: "Notification type code." },
title: { type: "string", description: "Title." },
body: { type: "string", description: "Optional body text." },
entityId: { type: "string", description: "Optional linked entity ID." },
entityType: { type: "string", description: "Optional linked entity type." },
category: { type: "string", enum: ["NOTIFICATION", "REMINDER", "TASK", "APPROVAL"] },
priority: { type: "string", enum: ["LOW", "NORMAL", "HIGH", "URGENT"] },
link: { type: "string", description: "Optional deep link." },
taskStatus: { type: "string", enum: ["OPEN", "IN_PROGRESS", "DONE", "DISMISSED"] },
taskAction: { type: "string", description: "Optional machine-readable task action." },
assigneeId: { type: "string", description: "Optional assignee user ID." },
dueDate: { type: "string", format: "date-time", description: "Optional due date." },
channel: { type: "string", enum: ["in_app", "email", "both"] },
senderId: { type: "string", description: "Optional sender override." },
},
required: ["userId", "type", "title"],
},
},
},
// ── DASHBOARD DETAIL ──
{
type: "function",
function: {
name: "get_dashboard_detail",
description: "Get detailed dashboard data: peak allocation times, top-value resources, demand pipeline, chargeability overview.",
parameters: {
type: "object",
properties: {
section: {
type: "string",
description: "Which section: peak_times, top_resources, demand_pipeline, chargeability_overview, or all",
},
},
},
},
},
// ── PROJECT MANAGEMENT ──
{
type: "function",
function: {
name: "delete_project",
description: "Delete a project. Only DRAFT projects can be deleted. Requires manageProjects permission. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
},
required: ["projectId"],
},
},
},
// ── ORG UNIT MANAGEMENT ──
...orgUnitMutationToolDefinitions,
// ── COVER ART ──
{
type: "function",
function: {
name: "generate_project_cover",
description: "Generate an AI cover art image for a project. Uses the configured image provider (DALL-E or Google Gemini). The image will be stored as the project's cover. Requires manageProjects permission.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
prompt: { type: "string", description: "Optional custom prompt for the AI image generation (e.g. 'futuristic car in neon cityscape'). If not provided, a default automotive/CGI prompt is used based on the project name." },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "remove_project_cover",
description: "Remove the cover art image from a project. Requires manageProjects permission.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
},
required: ["projectId"],
},
},
},
// ── TASK MANAGEMENT ──
{
type: "function",
function: {
name: "list_tasks",
description: "List tasks and approvals for the current user via the real notification router, optionally including tasks assigned to them.",
parameters: {
type: "object",
properties: {
status: { type: "string", enum: ["OPEN", "IN_PROGRESS", "DONE", "DISMISSED"], description: "Optional status filter." },
includeAssigned: { type: "boolean", description: "Include tasks where the current user is assignee as well as owner. Default: true." },
limit: { type: "integer", description: "Max results. Default: 20." },
},
},
},
},
{
type: "function",
function: {
name: "get_task_counts",
description: "Get open, in-progress, done, dismissed, and overdue task counts for the current user.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_task_detail",
description: "Get details of a specific task/notification including linked entity information.",
parameters: {
type: "object",
properties: {
taskId: { type: "string", description: "Notification/task ID" },
},
required: ["taskId"],
},
},
},
{
type: "function",
function: {
name: "update_task_status",
description: "Update the status of a task. Mark as IN_PROGRESS, DONE, or DISMISSED.",
parameters: {
type: "object",
properties: {
taskId: { type: "string", description: "Task/notification ID" },
status: { type: "string", enum: ["OPEN", "IN_PROGRESS", "DONE", "DISMISSED"], description: "New status" },
},
required: ["taskId", "status"],
},
},
},
{
type: "function",
function: {
name: "execute_task_action",
description: "Execute the machine-readable action associated with a task. For example: approve a vacation, confirm an assignment, etc. The action is encoded in the task's taskAction field.",
parameters: {
type: "object",
properties: {
taskId: { type: "string", description: "Task/notification ID containing the action to execute" },
},
required: ["taskId"],
},
},
},
{
type: "function",
function: {
name: "create_reminder",
description: "Create a personal reminder for the current user via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
title: { type: "string", description: "Reminder title" },
body: { type: "string", description: "Optional details" },
remindAt: { type: "string", format: "date-time", description: "When to remind (ISO 8601 datetime)" },
recurrence: { type: "string", enum: ["daily", "weekly", "monthly"], description: "Optional recurrence pattern" },
entityId: { type: "string", description: "Optional: linked entity ID (project, resource, etc.)" },
entityType: { type: "string", description: "Optional: entity type (project, resource, vacation, etc.)" },
link: { type: "string", description: "Optional deep link." },
},
required: ["title", "remindAt"],
},
},
},
{
type: "function",
function: {
name: "list_reminders",
description: "List personal reminders for the current user via the real notification router.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20." },
},
},
},
},
{
type: "function",
function: {
name: "update_reminder",
description: "Update a personal reminder via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Reminder notification ID." },
title: { type: "string", description: "Optional reminder title." },
body: { type: "string", description: "Optional reminder body." },
remindAt: { type: "string", format: "date-time", description: "Optional reminder timestamp." },
recurrence: { type: ["string", "null"], enum: ["daily", "weekly", "monthly", null], description: "Optional recurrence update. Use null to clear recurrence." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "delete_reminder",
description: "Delete a personal reminder via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Reminder notification ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_task_for_user",
description: "Create a task for a specific user via the real notification router. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "Target user ID" },
title: { type: "string", description: "Task title" },
body: { type: "string", description: "Task description" },
priority: { type: "string", enum: ["LOW", "NORMAL", "HIGH", "URGENT"], description: "Priority (default NORMAL)" },
dueDate: { type: "string", format: "date-time", description: "Optional due date (ISO 8601)" },
taskAction: { type: "string", description: "Optional machine-readable action (format: action_name:entity_id)" },
entityId: { type: "string", description: "Optional linked entity ID" },
entityType: { type: "string", description: "Optional entity type" },
link: { type: "string", description: "Optional deep link." },
channel: { type: "string", enum: ["in_app", "email", "both"], description: "Delivery channel. Default: in_app." },
},
required: ["userId", "title"],
},
},
},
{
type: "function",
function: {
name: "assign_task",
description: "Assign or reassign a task to another user via the real notification router. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Task notification ID." },
assigneeId: { type: "string", description: "User ID to assign." },
},
required: ["id", "assigneeId"],
},
},
},
{
type: "function",
function: {
name: "send_broadcast",
description: "Create and send a broadcast notification via the real notification router. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
title: { type: "string", description: "Notification title" },
body: { type: "string", description: "Notification body" },
targetType: { type: "string", enum: ["user", "role", "project", "orgUnit", "all"], description: "Target audience type" },
targetValue: { type: "string", description: "Target value: user ID, role name (ADMIN/MANAGER/CONTROLLER/USER/VIEWER), project ID, or org unit ID" },
category: { type: "string", enum: ["NOTIFICATION", "REMINDER", "TASK", "APPROVAL"], description: "Broadcast category. Default: NOTIFICATION." },
priority: { type: "string", enum: ["LOW", "NORMAL", "HIGH", "URGENT"], description: "Priority (default NORMAL)" },
channel: { type: "string", enum: ["in_app", "email", "both"], description: "Delivery channel (default in_app)" },
link: { type: "string", description: "Optional deep-link URL" },
scheduledAt: { type: "string", format: "date-time", description: "Optional scheduled send timestamp." },
taskAction: { type: "string", description: "Optional machine-readable task action for task-like broadcasts." },
dueDate: { type: "string", format: "date-time", description: "Optional due date for task-like broadcasts." },
},
required: ["title", "targetType"],
},
},
},
{
type: "function",
function: {
name: "list_broadcasts",
description: "List notification broadcasts via the real notification router. Manager or admin role required.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20." },
},
},
},
},
{
type: "function",
function: {
name: "get_broadcast_detail",
description: "Get one notification broadcast via the real notification router. Manager or admin role required.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Broadcast ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "delete_notification",
description: "Delete one of the current user's own notifications via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Notification ID." },
},
required: ["id"],
},
},
},
// ── INSIGHTS & ANOMALIES ──
{
type: "function",
function: {
name: "detect_anomalies",
description: "Detect anomalies across all active projects: budget burn rate issues, staffing gaps, utilization outliers, and timeline overruns.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_skill_gaps",
description: "Analyze skill supply vs demand across all active projects. Returns which skills are in short supply relative to demand requirements.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_project_health",
description: "Get health scores for all active projects based on budget utilization, staffing completeness, and timeline status.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_budget_forecast",
description: "Get budget utilization and burn rate per active project. Shows total budget, spent, remaining, and whether burn is ahead or behind schedule.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_insights_summary",
description: "Get a summary of anomaly counts by category (budget, staffing, timeline, utilization) plus critical count.",
parameters: { type: "object", properties: {} },
},
},
// ── REPORTS & COMMENTS ──
{
type: "function",
function: {
name: "run_report",
description: "Run a dynamic report query on resources, projects, assignments, or resource-month rows with flexible column selection and filtering.",
parameters: {
type: "object",
properties: {
entity: {
type: "string",
enum: ["resource", "project", "assignment", "resource_month"],
description: "Entity type to query",
},
columns: {
type: "array",
items: { type: "string" },
description: "Column keys to include (e.g. 'displayName', 'chapter', 'country.name')",
},
filters: {
type: "array",
items: {
type: "object",
properties: {
field: { type: "string", description: "Field to filter on" },
op: { type: "string", enum: ["eq", "neq", "gt", "lt", "gte", "lte", "contains", "in"], description: "Filter operator" },
value: { type: "string", description: "Filter value (string)" },
},
required: ["field", "op", "value"],
},
description: "Filters to apply",
},
periodMonth: {
type: "string",
description: "Required for resource_month reports. Format: YYYY-MM",
},
groupBy: {
type: "string",
description: "Optional scalar field used to group result rows into labeled sections.",
},
sortBy: {
type: "string",
description: "Optional scalar field used to sort rows within the grouped result.",
},
sortDir: {
type: "string",
enum: ["asc", "desc"],
description: "Sort direction for sortBy. Default: asc",
},
limit: { type: "integer", description: "Max results. Default: 50" },
},
required: ["entity", "columns"],
},
},
},
{
type: "function",
function: {
name: "list_comments",
description: `List comments (with replies) for a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required.`,
parameters: {
type: "object",
properties: {
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
entityId: { type: "string", description: "Entity ID" },
},
required: ["entityType", "entityId"],
},
},
},
{
type: "function",
function: {
name: "lookup_rate",
description: "Find the best matching rate card line for given criteria (client, chapter, management level, role, seniority).",
parameters: {
type: "object",
properties: {
clientId: { type: "string", description: "Client ID to find rate card for" },
chapter: { type: "string", description: "Chapter to match" },
managementLevelId: { type: "string", description: "Management level ID to match" },
roleName: { type: "string", description: "Role name to match" },
seniority: { type: "string", description: "Seniority level to match" },
},
},
},
},
// ── SCENARIO & AI ──
{
type: "function",
function: {
name: "simulate_scenario",
description: "Run a read-only what-if staffing simulation for a project. Shows cost/hours/utilization impact of adding, removing, or changing resource assignments without persisting changes.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
changes: {
type: "array",
items: {
type: "object",
properties: {
assignmentId: { type: "string", description: "Existing assignment ID to modify (omit for new)" },
resourceId: { type: "string", description: "Resource ID" },
roleId: { type: "string", description: "Role ID" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
hoursPerDay: { type: "number", description: "Hours per day" },
remove: { type: "boolean", description: "Set true to remove an existing assignment" },
},
required: ["startDate", "endDate", "hoursPerDay"],
},
description: "Array of staffing changes to simulate",
},
},
required: ["projectId", "changes"],
},
},
},
{
type: "function",
function: {
name: "generate_project_narrative",
description: "Generate an AI-powered executive narrative for a project covering budget, staffing, timeline risk, and action items. Requires AI to be configured.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "create_comment",
description: `Add a comment to a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required. Supports @mentions. Always confirm with the user first.`,
parameters: {
type: "object",
properties: {
entityType: { type: "string", enum: [...COMMENT_ENTITY_TYPE_VALUES], description: getCommentToolEntityDescription() },
entityId: { type: "string", description: "Entity ID" },
body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." },
},
required: ["entityType", "entityId", "body"],
},
},
},
{
type: "function",
function: {
name: "resolve_comment",
description: `Mark a comment as resolved (or unresolve it) on a supported comment-enabled entity. ${getCommentToolScopeSentence()} Entity visibility is required, and only the comment author or an admin can change resolution.`,
parameters: {
type: "object",
properties: {
commentId: { type: "string", description: "Comment ID to resolve" },
resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" },
},
required: ["commentId"],
},
},
},
{
type: "function",
function: {
name: "query_change_history",
description: "Search the audit history for changes to projects, resources, allocations, vacations, or any entity. Reuses the real audit log list API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Filter by entity type (e.g. 'Project', 'Resource', 'Allocation', 'Vacation', 'Role', 'Estimate')" },
search: { type: "string", description: "Search in entity name or summary text" },
userId: { type: "string", description: "Filter by user ID who made the change" },
daysBack: { type: "integer", description: "How many days back to search. Default: 7" },
action: { type: "string", description: "Filter by action type: CREATE, UPDATE, DELETE, SHIFT, IMPORT" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_entity_timeline",
description: "Get the audit history for a specific entity (project, resource, etc.) via the real audit API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'Project', 'Resource', 'Allocation')" },
entityId: { type: "string", description: "Entity ID" },
limit: { type: "integer", description: "Max results. Default: 50" },
},
required: ["entityType", "entityId"],
},
},
},
{
type: "function",
function: {
name: "export_resources_csv",
description: "Export the current active resource list as CSV via the real import/export router. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "export_projects_csv",
description: "Export the current project list as CSV via the real import/export router. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "import_csv_data",
description: "Import CSV-style row data for resources, projects, or allocations via the real import/export router. Requires manager/admin, importData permission, and confirmation.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", enum: ["resources", "projects", "allocations"], description: "Import target entity type." },
rows: {
type: "array",
description: "CSV rows already parsed to key/value objects.",
items: {
type: "object",
additionalProperties: { type: "string" },
},
},
dryRun: { type: "boolean", description: "Validate only without persisting changes. Default: true." },
},
required: ["entityType", "rows"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_import_batches",
description: "List Dispo import batches with pagination and optional status filter via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
status: { type: "string", description: "Optional batch status filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
},
},
},
{
type: "function",
function: {
name: "get_dispo_import_batch",
description: "Get one Dispo import batch including staged record counters via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Import batch ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "stage_dispo_import_batch",
description: "Stage a Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." },
referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." },
chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." },
costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." },
rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." },
notes: { type: "string", description: "Optional import notes." },
},
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
},
},
},
{
type: "function",
function: {
name: "validate_dispo_import_batch",
description: "Validate a Dispo import batch readiness check via the real dispo router without committing anything. Admin role required.",
parameters: {
type: "object",
properties: {
planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." },
referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." },
chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." },
costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." },
rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." },
importBatchId: { type: "string", description: "Optional existing staged import batch ID." },
notes: { type: "string", description: "Optional import notes." },
},
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
},
},
},
{
type: "function",
function: {
name: "cancel_dispo_import_batch",
description: "Cancel a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Import batch ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_resources",
description: "List staged Dispo resources for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
status: { type: "string", description: "Optional staged record status filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_projects",
description: "List staged Dispo projects for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
status: { type: "string", description: "Optional staged record status filter." },
isTbd: { type: "boolean", description: "Optional TBD-project filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_assignments",
description: "List staged Dispo assignments for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
status: { type: "string", description: "Optional staged record status filter." },
resourceExternalId: { type: "string", description: "Optional resource external ID filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_vacations",
description: "List staged Dispo vacations for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
resourceExternalId: { type: "string", description: "Optional resource external ID filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_unresolved_records",
description: "List staged unresolved Dispo records for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
recordType: { type: "string", description: "Optional unresolved record type filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "resolve_dispo_staged_record",
description: "Resolve one staged Dispo record via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Staged record ID." },
recordType: { type: "string", description: "Staged record type." },
action: { type: "string", enum: ["APPROVE", "REJECT", "SKIP"], description: "Resolution action." },
},
required: ["id", "recordType", "action"],
},
},
},
{
type: "function",
function: {
name: "commit_dispo_import_batch",
description: "Commit a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
allowTbdUnresolved: { type: "boolean", description: "Allow unresolved TBD projects during commit." },
importTbdProjects: { type: "boolean", description: "Whether TBD projects should be imported." },
},
required: ["importBatchId"],
},
},
},
...settingsAdminToolDefinitions,
];
// ─── 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 = {
async search_resources(params: {
query?: string; country?: string; metroCity?: string;
orgUnit?: string; roleName?: string;
isActive?: boolean; limit?: number;
}, ctx: ToolContext) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
return caller.listSummariesDetail({
search: params.query,
country: params.country,
metroCity: params.metroCity,
orgUnit: params.orgUnit,
roleName: params.roleName,
isActive: params.isActive ?? true,
limit: Math.min(params.limit ?? 50, 100),
});
},
async get_resource(params: { identifier: string }, ctx: ToolContext) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
return caller.getByIdentifierDetail({ identifier: params.identifier });
},
async search_projects(params: { query?: string; status?: string; limit?: number }, ctx: ToolContext) {
const caller = createProjectCaller(createScopedCallerContext(ctx));
return caller.searchSummariesDetail({
search: params.query,
status: params.status as import("@capakraken/shared").ProjectStatus | undefined,
limit: Math.min(params.limit ?? 20, 50),
});
},
async get_project(params: { identifier: string }, ctx: ToolContext) {
const caller = createProjectCaller(createScopedCallerContext(ctx));
let project;
try {
project = await caller.getByIdentifierDetail({ identifier: params.identifier });
} catch (error) {
const mapped = toAssistantNotFoundError(
error,
`Project not found: ${params.identifier}`,
);
if (mapped) {
return mapped;
}
throw error;
}
return project;
},
...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,
}),
async search_estimates(params: {
projectCode?: string; query?: string; status?: string; limit?: number;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId: string | undefined;
if (params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
return caller.list({
...(params.query ? { query: params.query } : {}),
...(params.status ? { status: params.status as EstimateStatus } : {}),
...(projectId ? { projectId } : {}),
});
},
async list_clients(params: { query?: string; limit?: number }, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const caller = createClientCaller(createScopedCallerContext(ctx));
const clients = await caller.list({
isActive: true,
...(params.query ? { search: params.query } : {}),
});
return clients.slice(0, limit).map((c) => ({
id: c.id,
name: c.name,
code: c.code,
projectCount: c._count.projects,
}));
},
async list_org_units(params: { level?: number }, ctx: ToolContext) {
const caller = createOrgUnitCaller(createScopedCallerContext(ctx));
const units = await caller.list({
isActive: true,
...(params.level !== undefined ? { level: params.level } : {}),
});
const details = await Promise.all(units.map((unit) => caller.getById({ id: unit.id })));
return details.map((u) => ({
id: u.id,
name: u.name,
shortName: u.shortName,
level: u.level,
parent: u.parent?.name ?? null,
resourceCount: u._count.resources,
}));
},
// ── NAVIGATION TOOLS ──
async get_my_timeline_entries_view(params: {
startDate: string;
endDate: string;
resourceIds?: string[];
projectIds?: string[];
clientIds?: string[];
chapters?: string[];
eids?: string[];
countryCodes?: string[];
}, ctx: ToolContext) {
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getMyEntriesView({
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
...(params.resourceIds ? { resourceIds: params.resourceIds } : {}),
...(params.projectIds ? { projectIds: params.projectIds } : {}),
...(params.clientIds ? { clientIds: params.clientIds } : {}),
...(params.chapters ? { chapters: params.chapters } : {}),
...(params.eids ? { eids: params.eids } : {}),
...(params.countryCodes ? { countryCodes: params.countryCodes } : {}),
});
},
async get_my_timeline_holiday_overlays(params: {
startDate: string;
endDate: string;
resourceIds?: string[];
projectIds?: string[];
clientIds?: string[];
chapters?: string[];
eids?: string[];
countryCodes?: string[];
}, ctx: ToolContext) {
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getMyHolidayOverlays({
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
...(params.resourceIds ? { resourceIds: params.resourceIds } : {}),
...(params.projectIds ? { projectIds: params.projectIds } : {}),
...(params.clientIds ? { clientIds: params.clientIds } : {}),
...(params.chapters ? { chapters: params.chapters } : {}),
...(params.eids ? { eids: params.eids } : {}),
...(params.countryCodes ? { countryCodes: params.countryCodes } : {}),
});
},
async navigate_to_page(params: {
page: string;
eids?: string; chapters?: string; projectIds?: string; clientIds?: string;
countryCodes?: string; startDate?: string; days?: number;
}, _ctx: ToolContext) {
const pageMap: Record<string, string> = {
timeline: "/timeline",
dashboard: "/dashboard",
resources: "/resources",
projects: "/projects",
allocations: "/allocations",
staffing: "/staffing",
estimates: "/estimates",
vacations: "/vacations",
"my-vacations": "/vacations/my",
roles: "/roles",
"skills-analytics": "/analytics/skills",
chargeability: "/reports/chargeability",
"computation-graph": "/analytics/computation-graph",
};
const path = pageMap[params.page];
if (!path) return { error: `Unknown page: ${params.page}. Available: ${Object.keys(pageMap).join(", ")}` };
// Build query params for pages that support them
const queryParts: string[] = [];
if (params.eids) queryParts.push(`eids=${encodeURIComponent(params.eids)}`);
if (params.chapters) queryParts.push(`chapters=${encodeURIComponent(params.chapters)}`);
if (params.projectIds) queryParts.push(`projectIds=${encodeURIComponent(params.projectIds)}`);
if (params.clientIds) queryParts.push(`clientIds=${encodeURIComponent(params.clientIds)}`);
if (params.countryCodes) queryParts.push(`countryCodes=${encodeURIComponent(params.countryCodes)}`);
if (params.startDate) queryParts.push(`startDate=${encodeURIComponent(params.startDate)}`);
if (params.days) queryParts.push(`days=${params.days}`);
const url = queryParts.length > 0 ? `${path}?${queryParts.join("&")}` : path;
return {
__action: "navigate",
url,
description: `Navigiere zu ${path}`,
};
},
// ── WRITE TOOLS ──
async update_resource(params: {
id: string; displayName?: string; fte?: number;
lcrCents?: number; chapter?: string; chargeabilityTarget?: number;
}, ctx: ToolContext) {
assertPermission(ctx, "manageResources" as PermissionKey);
const resource = await resolveResourceIdentifier(ctx, params.id);
if ("error" in resource) return resource;
const caller = createResourceCaller(createScopedCallerContext(ctx));
const data = UpdateResourceSchema.parse({
...(params.displayName !== undefined ? { displayName: params.displayName } : {}),
...(params.fte !== undefined ? { fte: params.fte } : {}),
...(params.lcrCents !== undefined ? { lcrCents: params.lcrCents } : {}),
...(params.chapter !== undefined ? { chapter: params.chapter } : {}),
...(params.chargeabilityTarget !== undefined ? { chargeabilityTarget: params.chargeabilityTarget } : {}),
});
const updatedFields = Object.keys(data);
if (updatedFields.length === 0) return { error: "No fields to update" };
let updated;
try {
updated = await caller.update({ id: resource.id, data });
} catch (error) {
const mapped = toAssistantResourceMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["resource"],
success: true,
message: `Updated resource ${updated.displayName} (${updated.eid})`,
updatedFields,
};
},
async update_project(params: {
id: string; name?: string; budgetCents?: number;
winProbability?: number; status?: string;
responsiblePerson?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const project = await resolveProjectIdentifier(ctx, params.id);
if ("error" in project) return project;
const data: Record<string, unknown> = {};
if (params.name !== undefined) data.name = params.name;
if (params.budgetCents !== undefined) data.budgetCents = params.budgetCents;
if (params.winProbability !== undefined) data.winProbability = params.winProbability;
if (params.status !== undefined) data.status = params.status;
// Validate responsible person against existing resources
if (params.responsiblePerson !== undefined) {
const result = await resolveResponsiblePerson(params.responsiblePerson, ctx);
if ("error" in result) return { error: result.error };
data.responsiblePerson = result.displayName;
}
const parsedData = UpdateProjectSchema.parse(data);
const updatedFields = Object.keys(parsedData);
if (updatedFields.length === 0) return { error: "No fields to update" };
const caller = createProjectCaller(createScopedCallerContext(ctx));
let updated;
try {
updated = await caller.update({ id: project.id, data: parsedData });
} catch (error) {
const mapped = toAssistantProjectMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Updated project ${updated.name} (${updated.shortCode})`,
updatedFields,
};
},
async create_project(params: {
shortCode: string; name: string; orderType: string;
allocationType?: string; budgetCents: number;
startDate: string; endDate: string;
winProbability?: number; status?: string;
responsiblePerson?: string; color?: string;
blueprintName?: string; clientName?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
if (!params.responsiblePerson?.trim()) {
return { error: "responsiblePerson is required to create a project." };
}
// Validate responsible person against existing resources
const responsible = await resolveResponsiblePerson(params.responsiblePerson, ctx);
if ("error" in responsible) return { error: responsible.error };
const blueprintCaller = createBlueprintCaller(createScopedCallerContext(ctx));
const clientCaller = createClientCaller(createScopedCallerContext(ctx));
let blueprintId: string | undefined;
if (params.blueprintName) {
const blueprint = await resolveEntityOrAssistantError(
() => blueprintCaller.resolveByIdentifier({ identifier: params.blueprintName! }),
`Blueprint not found: "${params.blueprintName}"`,
);
if ("error" in blueprint) {
return blueprint;
}
blueprintId = blueprint.id;
}
let clientId: string | undefined;
if (params.clientName) {
const client = await resolveEntityOrAssistantError(
() => clientCaller.resolveByIdentifier({ identifier: params.clientName! }),
`Client not found: "${params.clientName}"`,
);
if ("error" in client) {
return client;
}
clientId = client.id;
}
const input = CreateProjectSchema.parse({
shortCode: params.shortCode,
name: params.name,
orderType: params.orderType,
allocationType: params.allocationType ?? "INT",
budgetCents: params.budgetCents,
startDate: params.startDate,
endDate: params.endDate,
winProbability: params.winProbability ?? 100,
status: params.status ?? "DRAFT",
responsiblePerson: responsible.displayName,
...(params.color ? { color: params.color } : {}),
...(blueprintId ? { blueprintId } : {}),
...(clientId ? { clientId } : {}),
staffingReqs: [],
dynamicFields: {},
});
const caller = createProjectCaller(createScopedCallerContext(ctx));
let project;
try {
project = await caller.create(input);
} catch (error) {
const mapped = toAssistantProjectCreationError(error, input.shortCode);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Created project: ${project.name} (${project.shortCode}), budget ${fmtEur(params.budgetCents)}, ${params.startDate} to ${params.endDate}, status: ${project.status}`,
projectId: project.id,
shortCode: project.shortCode,
};
},
// ── RESOURCE MANAGEMENT ──
async create_resource(params: {
eid: string; displayName: string; email?: string;
fte?: number; lcrCents: number; ucrCents?: number;
chapter?: string; chargeabilityTarget?: number;
roleName?: string; countryCode?: string; orgUnitName?: string;
postalCode?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageResources" as PermissionKey);
if (!params.email?.trim()) {
return { error: "email is required to create a resource." };
}
const roleCaller = createRoleCaller(createScopedCallerContext(ctx));
const countryCaller = createCountryCaller(createScopedCallerContext(ctx));
const orgUnitCaller = createOrgUnitCaller(createScopedCallerContext(ctx));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: Record<string, any> = {
eid: params.eid,
displayName: params.displayName,
email: params.email,
lcrCents: params.lcrCents,
ucrCents: params.ucrCents ?? Math.round(params.lcrCents * 0.7),
chargeabilityTarget: params.chargeabilityTarget ?? 80,
currency: "EUR",
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
skills: [],
dynamicFields: {},
...(params.fte !== undefined ? { fte: params.fte } : {}),
};
if (params.chapter) data.chapter = params.chapter;
if (params.postalCode) data.postalCode = params.postalCode;
if (params.roleName) {
const role = await resolveEntityOrAssistantError(
() => roleCaller.resolveByIdentifier({ identifier: params.roleName! }),
`Role not found: "${params.roleName}"`,
);
if ("error" in role) {
return role;
}
data.roleId = role.id;
}
if (params.countryCode) {
const country = await resolveEntityOrAssistantError(
() => countryCaller.resolveByIdentifier({ identifier: params.countryCode! }),
`Country not found: "${params.countryCode}"`,
);
if ("error" in country) {
return country;
}
data.countryId = country.id;
}
if (params.orgUnitName) {
const orgUnit = await resolveEntityOrAssistantError(
() => orgUnitCaller.resolveByIdentifier({ identifier: params.orgUnitName! }),
`Org unit not found: "${params.orgUnitName}"`,
);
if ("error" in orgUnit) {
return orgUnit;
}
data.orgUnitId = orgUnit.id;
}
const input = CreateResourceSchema.parse(data);
const caller = createResourceCaller(createScopedCallerContext(ctx));
let resource;
try {
resource = await caller.create(input);
} catch (error) {
const mapped = toAssistantResourceCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["resource"],
success: true,
message: `Created resource: ${resource.displayName} (${resource.eid})`,
resourceId: resource.id,
};
},
async deactivate_resource(params: { identifier: string }, ctx: ToolContext) {
assertPermission(ctx, "manageResources" as PermissionKey);
const resource = await resolveResourceIdentifier(ctx, params.identifier);
if ("error" in resource) return resource;
const caller = createResourceCaller(createScopedCallerContext(ctx));
try {
await caller.deactivate({ id: resource.id });
} catch (error) {
const mapped = toAssistantResourceMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["resource"],
success: true,
message: `Deactivated resource: ${resource.displayName} (${resource.eid})`,
};
},
// ── 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`,
};
},
// ── DEMAND / STAFFING ──
async list_demands(params: { projectId?: string; status?: string; limit?: number }, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 30, 50);
const caller = createAllocationCaller(createScopedCallerContext(ctx));
const resolvedProject = params.projectId
? await resolveProjectIdentifier(ctx, params.projectId)
: null;
if (resolvedProject && "error" in resolvedProject) {
return resolvedProject;
}
const demands = await caller.listDemands({
...(resolvedProject ? { projectId: resolvedProject.id } : {}),
...(params.status ? { status: params.status as AllocationStatus } : {}),
});
return demands.map((d) => ({
id: d.id,
project: d.project.name,
projectCode: d.project.shortCode,
role: d.roleEntity?.name ?? d.role ?? "Unspecified",
status: d.status,
headcount: d.headcount,
filled: d.assignments.length,
remaining: d.headcount - d.assignments.length,
hoursPerDay: d.hoursPerDay,
start: fmtDate(d.startDate),
end: fmtDate(d.endDate),
})).slice(0, limit);
},
async create_demand(params: {
projectId: string; roleName: string; headcount?: number;
hoursPerDay: number; startDate: string; endDate: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const roleCaller = createRoleCaller(createScopedCallerContext(ctx));
const [project, role] = await Promise.all([
resolveProjectIdentifier(ctx, params.projectId),
resolveEntityOrAssistantError(
() => roleCaller.resolveByIdentifier({ identifier: params.roleName }),
`Role not found: ${params.roleName}`,
),
]);
if ("error" in project) {
return project;
}
if ("error" in role) {
return role;
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
let demand;
try {
demand = await caller.createDemand({
projectId: project.id,
roleId: role.id,
role: role.name,
headcount: params.headcount ?? 1,
hoursPerDay: params.hoursPerDay,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
});
} catch (error) {
const mapped = toAssistantDemandCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation"],
success: true,
message: `Created demand: ${role.name} × ${params.headcount ?? 1} for ${project.name} (${project.shortCode}), ${params.hoursPerDay}h/day, ${params.startDate} to ${params.endDate}`,
demandId: demand.id,
};
},
async fill_demand(params: { demandId: string; resourceId: string }, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const allocationCaller = createAllocationCaller(createScopedCallerContext(ctx));
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
let result;
try {
result = await allocationCaller.assignResourceToDemand({
demandRequirementId: params.demandId,
resourceId: resource.id,
});
} catch (error) {
const mapped = toAssistantDemandFillError(error);
if (mapped) {
return mapped;
}
throw error;
}
const roleName = result.demandRequirement.roleEntity?.name ?? result.demandRequirement.role ?? null;
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `Assigned ${resource.displayName} to ${roleName ?? "demand"} on ${result.demandRequirement.project.name} (${result.demandRequirement.project.shortCode})`,
assignmentId: result.assignment.id,
};
},
async check_resource_availability(params: {
resourceId: string; startDate: string; endDate: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
return caller.getResourceAvailabilitySummary({
resourceId: resource.id,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
});
},
async get_staffing_suggestions(params: {
projectId: string; roleName?: string;
startDate?: string; endDate?: string; limit?: number;
}, ctx: ToolContext) {
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const caller = createStaffingCaller(createScopedCallerContext(ctx));
const startDate = parseOptionalIsoDate(params.startDate, "startDate");
const endDate = parseOptionalIsoDate(params.endDate, "endDate");
return caller.getProjectStaffingSuggestions({
projectId: project.id,
...(params.roleName ? { roleName: params.roleName } : {}),
...(startDate ? { startDate } : {}),
...(endDate ? { endDate } : {}),
...(params.limit ? { limit: params.limit } : {}),
});
},
async find_capacity(params: {
startDate: string; endDate: string;
minHoursPerDay?: number; roleName?: string;
chapter?: string; limit?: number;
}, ctx: ToolContext) {
const caller = createStaffingCaller(createScopedCallerContext(ctx));
return caller.searchCapacity({
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
minHoursPerDay: params.minHoursPerDay ?? 4,
...(params.roleName ? { roleName: params.roleName } : {}),
...(params.chapter ? { chapter: params.chapter } : {}),
...(params.limit ? { limit: params.limit } : {}),
});
},
// ── BLUEPRINT ──
async list_blueprints(_params: Record<string, never>, ctx: ToolContext) {
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
const blueprints = await caller.listSummaries();
return blueprints.map((b) => ({
id: b.id,
name: b.name,
projectCount: b._count.projects,
}));
},
async get_blueprint(params: { identifier: string }, ctx: ToolContext) {
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
const bp = await resolveEntityOrAssistantError(
() => caller.getByIdentifier({ identifier: params.identifier }),
`Blueprint not found: ${params.identifier}`,
);
if ("error" in bp) {
return bp;
}
return {
id: bp.id,
name: bp.name,
fieldDefs: bp.fieldDefs,
rolePresets: bp.rolePresets,
};
},
// ── RATE CARDS ──
async list_rate_cards(params: { query?: string; limit?: number }, ctx: ToolContext) {
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const cards = await caller.list({
isActive: true,
...(params.query ? { search: params.query } : {}),
});
return cards.map((c) => ({
id: c.id,
name: c.name,
effectiveFrom: fmtDate(c.effectiveFrom),
effectiveTo: fmtDate(c.effectiveTo),
lineCount: c._count.lines,
})).slice(0, Math.min(params.limit ?? 20, 50));
},
async resolve_rate(params: { resourceId?: string; roleName?: string; date?: string }, ctx: ToolContext) {
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const date = parseOptionalIsoDate(params.date, "date");
if (params.resourceId) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
return caller.resolveBestRate({
resourceId: resource.id,
...(date ? { date } : {}),
});
}
return caller.resolveBestRate({
...(params.roleName ? { roleName: params.roleName } : {}),
...(date ? { date } : {}),
});
},
// ── ESTIMATES ──
async get_estimate_detail(params: { estimateId: string }, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getById({ id: params.estimateId });
} catch (error) {
const mapped = toAssistantEstimateNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async list_estimate_versions(params: { estimateId: string }, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.listVersions({ estimateId: params.estimateId });
} catch (error) {
const mapped = toAssistantEstimateNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async get_estimate_version_snapshot(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getVersionSnapshot({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async create_estimate(params: {
projectId?: string;
projectCode?: string;
name: string;
opportunityId?: string;
baseCurrency?: string;
status?: EstimateStatus;
versionLabel?: string;
versionNotes?: string;
assumptions?: CreateEstimateInput["assumptions"];
scopeItems?: CreateEstimateInput["scopeItems"];
demandLines?: CreateEstimateInput["demandLines"];
resourceSnapshots?: CreateEstimateInput["resourceSnapshots"];
metrics?: CreateEstimateInput["metrics"];
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
let estimate;
try {
estimate = await caller.create({
name: params.name,
...(projectId ? { projectId } : {}),
...(params.opportunityId !== undefined ? { opportunityId: params.opportunityId } : {}),
...(params.baseCurrency !== undefined ? { baseCurrency: params.baseCurrency } : {}),
...(params.status !== undefined ? { status: params.status } : {}),
...(params.versionLabel !== undefined ? { versionLabel: params.versionLabel } : {}),
...(params.versionNotes !== undefined ? { versionNotes: params.versionNotes } : {}),
...(params.assumptions !== undefined ? { assumptions: params.assumptions } : {}),
...(params.scopeItems !== undefined ? { scopeItems: params.scopeItems } : {}),
...(params.demandLines !== undefined ? { demandLines: params.demandLines } : {}),
...(params.resourceSnapshots !== undefined ? { resourceSnapshots: params.resourceSnapshots } : {}),
...(params.metrics !== undefined ? { metrics: params.metrics } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Created estimate "${estimate.name}".`,
};
},
async clone_estimate(params: {
sourceEstimateId: string;
name?: string;
projectId?: string;
projectCode?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
let estimate;
try {
estimate = await caller.clone({
sourceEstimateId: params.sourceEstimateId,
...(params.name !== undefined ? { name: params.name } : {}),
...(projectId ? { projectId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "clone");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Cloned estimate "${estimate.name}".`,
};
},
async update_estimate_draft(params: {
id: string;
projectId?: string;
projectCode?: string;
name?: string;
opportunityId?: string;
baseCurrency?: string;
status?: EstimateStatus;
versionLabel?: string;
versionNotes?: string;
assumptions?: UpdateEstimateDraftInput["assumptions"];
scopeItems?: UpdateEstimateDraftInput["scopeItems"];
demandLines?: UpdateEstimateDraftInput["demandLines"];
resourceSnapshots?: UpdateEstimateDraftInput["resourceSnapshots"];
metrics?: UpdateEstimateDraftInput["metrics"];
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
let estimate;
try {
estimate = await caller.updateDraft({
id: params.id,
...(projectId ? { projectId } : {}),
...(params.name !== undefined ? { name: params.name } : {}),
...(params.opportunityId !== undefined ? { opportunityId: params.opportunityId } : {}),
...(params.baseCurrency !== undefined ? { baseCurrency: params.baseCurrency } : {}),
...(params.status !== undefined ? { status: params.status } : {}),
...(params.versionLabel !== undefined ? { versionLabel: params.versionLabel } : {}),
...(params.versionNotes !== undefined ? { versionNotes: params.versionNotes } : {}),
...(params.assumptions !== undefined ? { assumptions: params.assumptions } : {}),
...(params.scopeItems !== undefined ? { scopeItems: params.scopeItems } : {}),
...(params.demandLines !== undefined ? { demandLines: params.demandLines } : {}),
...(params.resourceSnapshots !== undefined ? { resourceSnapshots: params.resourceSnapshots } : {}),
...(params.metrics !== undefined ? { metrics: params.metrics } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "updateDraft");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Updated estimate draft "${estimate.name}".`,
};
},
async submit_estimate_version(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let estimate;
try {
estimate = await caller.submitVersion({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "submitVersion");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Submitted estimate version for "${estimate.name}".`,
};
},
async approve_estimate_version(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let estimate;
try {
estimate = await caller.approveVersion({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "approveVersion");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Approved estimate version for "${estimate.name}".`,
};
},
async create_estimate_revision(params: {
estimateId: string;
sourceVersionId?: string;
label?: string;
notes?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let estimate;
try {
estimate = await caller.createRevision({
estimateId: params.estimateId,
...(params.sourceVersionId !== undefined ? { sourceVersionId: params.sourceVersionId } : {}),
...(params.label !== undefined ? { label: params.label } : {}),
...(params.notes !== undefined ? { notes: params.notes } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "createRevision");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Created a new estimate revision for "${estimate.name}".`,
};
},
async create_estimate_export(params: {
estimateId: string;
versionId?: string;
format: EstimateExportFormat;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let estimate;
try {
estimate = await caller.createExport({
estimateId: params.estimateId,
format: params.format,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "createExport");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Created ${params.format} export for estimate "${estimate.name}".`,
};
},
async create_estimate_planning_handoff(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.createPlanningHandoff({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "createPlanningHandoff");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate", "allocation", "timeline"],
success: true,
...result,
message: `Created planning handoff for estimate ${params.estimateId}.`,
};
},
async generate_estimate_weekly_phasing(params: {
estimateId: string;
startDate: string;
endDate: string;
pattern?: "even" | "front_loaded" | "back_loaded" | "custom";
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.generateWeeklyPhasing({
estimateId: params.estimateId,
startDate: params.startDate,
endDate: params.endDate,
...(params.pattern !== undefined ? { pattern: params.pattern } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "generateWeeklyPhasing");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
...result,
message: `Generated weekly phasing for estimate ${params.estimateId}.`,
};
},
async get_estimate_weekly_phasing(params: {
estimateId: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getWeeklyPhasing({ estimateId: params.estimateId });
} catch (error) {
const mapped = toAssistantEstimateReadError(error, "weeklyPhasing");
if (mapped) {
return mapped;
}
throw error;
}
},
async get_estimate_commercial_terms(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
try {
return await caller.getCommercialTerms({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateReadError(error, "commercialTerms");
if (mapped) {
return mapped;
}
throw error;
}
},
async update_estimate_commercial_terms(params: {
estimateId: string;
versionId?: string;
terms: Record<string, unknown>;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.updateCommercialTerms({
estimateId: params.estimateId,
terms: params.terms,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
} catch (error) {
const mapped = toAssistantEstimateMutationError(error, "updateCommercialTerms");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
...result,
message: `Updated commercial terms for estimate ${params.estimateId}.`,
};
},
// ── ROLES ──
// ── CLIENTS ──
// ── ADMIN / CONFIG ──
async list_countries(params: { includeInactive?: boolean; search?: string }, ctx: ToolContext) {
const caller = createCountryCaller(createScopedCallerContext(ctx));
const countries = await caller.list(
params.includeInactive
? undefined
: { isActive: true },
);
const normalizedSearch = params.search?.trim().toLowerCase();
const filteredCountries = normalizedSearch
? countries.filter((country) =>
country.code.toLowerCase().includes(normalizedSearch)
|| country.name.toLowerCase().includes(normalizedSearch))
: countries;
return {
count: filteredCountries.length,
countries: filteredCountries.map(formatCountry),
};
},
async get_country(params: { identifier: string }, ctx: ToolContext) {
const caller = createCountryCaller(createScopedCallerContext(ctx));
let country;
try {
country = await caller.getByIdentifier({ identifier: params.identifier });
} catch (error) {
const mapped = toAssistantCountryNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return formatCountry(country);
},
...createCountryMetroAdminExecutors({
createCountryCaller,
createScopedCallerContext,
assertAdminRole,
formatCountry,
toAssistantCountryMutationError,
toAssistantMetroCityMutationError,
}),
async list_management_levels(_params: Record<string, never>, ctx: ToolContext) {
const caller = createManagementLevelCaller(createScopedCallerContext(ctx));
const groups = await caller.listGroups();
return groups.map((g) => ({
id: g.id,
name: g.name,
target: g.targetPercentage ? `${g.targetPercentage}%` : null,
levels: g.levels.map((l: { id: string; name: string }) => ({ id: l.id, name: l.name })),
}));
},
async list_utilization_categories(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUtilizationCategoryCaller(createScopedCallerContext(ctx));
const categories = await caller.list();
const categoriesWithCounts = await Promise.all(
categories.map(async (category) => {
const detailed = await caller.getById({ id: category.id });
return {
category,
projectCount: detailed._count.projects,
};
}),
);
return categoriesWithCounts.map(({ category, projectCount }) => ({
id: category.id,
code: category.code,
name: category.name,
description: category.description,
projectCount,
}));
},
async list_calculation_rules(_params: Record<string, never>, ctx: ToolContext) {
const caller = createCalculationRuleCaller(createScopedCallerContext(ctx));
const rules = await caller.list();
return rules.map((rule) => ({
id: rule.id,
name: rule.name,
description: rule.description,
isActive: rule.isActive,
triggerType: rule.triggerType,
orderType: rule.orderType,
costEffect: rule.costEffect,
costReductionPercent: rule.costReductionPercent,
chargeabilityEffect: rule.chargeabilityEffect,
priority: rule.priority,
project: rule.project
? {
id: rule.project.id,
name: rule.project.name,
shortCode: rule.project.shortCode,
}
: null,
}));
},
async list_effort_rules(_params: Record<string, never>, ctx: ToolContext) {
const caller = createEffortRuleCaller(createScopedCallerContext(ctx));
const ruleSets = await caller.list();
return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
scopeType: rule.scopeType,
discipline: rule.discipline,
chapter: rule.chapter,
unitMode: rule.unitMode,
hoursPerUnit: rule.hoursPerUnit,
sortOrder: rule.sortOrder,
ruleSet: {
name: ruleSet.name,
isDefault: ruleSet.isDefault,
},
})));
},
async list_experience_multipliers(_params: Record<string, never>, ctx: ToolContext) {
const caller = createExperienceMultiplierCaller(createScopedCallerContext(ctx));
const multiplierSets = await caller.list();
return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
chapter: rule.chapter,
location: rule.location,
level: rule.level,
costMultiplier: rule.costMultiplier,
billMultiplier: rule.billMultiplier,
shoringRatio: rule.shoringRatio,
additionalEffortRatio: rule.additionalEffortRatio,
sortOrder: rule.sortOrder,
multiplierSet: {
name: multiplierSet.name,
isDefault: multiplierSet.isDefault,
},
})));
},
async list_users(params: { limit?: number }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const users = await caller.list();
return users.slice(0, Math.min(params.limit ?? 50, 100));
},
async list_assignable_users(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.listAssignable();
},
async get_current_user(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.me();
},
async get_dashboard_layout(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getDashboardLayout();
},
async save_dashboard_layout(params: { layout: unknown[] }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.saveDashboardLayout({ layout: params.layout });
return {
__action: "invalidate",
scope: ["dashboard"],
success: true,
...result,
message: "Saved dashboard layout.",
};
},
async get_favorite_project_ids(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getFavoriteProjectIds();
},
async toggle_favorite_project(params: { projectId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.toggleFavoriteProject({ projectId: params.projectId });
return {
__action: "invalidate",
scope: ["project"],
success: true,
...result,
message: result.added ? "Added project to favorites." : "Removed project from favorites.",
};
},
async get_column_preferences(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getColumnPreferences();
},
async set_column_preferences(params: {
view: "resources" | "projects" | "allocations" | "vacations" | "roles" | "users" | "blueprints";
visible?: string[];
sort?: { field: string; dir: "asc" | "desc" } | null;
rowOrder?: string[] | null;
}, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.setColumnPreferences({
view: params.view,
...(params.visible !== undefined ? { visible: params.visible } : {}),
...(params.sort !== undefined ? { sort: params.sort } : {}),
...(params.rowOrder !== undefined ? { rowOrder: params.rowOrder } : {}),
});
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: `Updated column preferences for ${params.view}.`,
};
},
async generate_totp_secret(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.generateTotpSecret();
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: "Generated a new MFA TOTP secret.",
};
},
async verify_and_enable_totp(params: { token: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.verifyAndEnableTotp({ token: params.token });
} catch (error) {
const mapped = toAssistantTotpEnableError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: "Enabled MFA TOTP.",
};
},
async get_mfa_status(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getMfaStatus();
},
async get_active_user_count(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.activeCount();
},
async create_user(params: {
email: string;
name: string;
systemRole?: SystemRole;
password: string;
}, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.create({
email: params.email,
name: params.name,
password: params.password,
...(params.systemRole !== undefined ? { systemRole: params.systemRole } : {}),
});
} catch (error) {
const mapped = toAssistantUserMutationError(error, "create");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user", "resource"],
success: true,
user,
userId: user.id,
message: `Created user ${user.name}.`,
};
},
async set_user_password(params: { userId: string; password: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.setPassword(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error, "password");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
...result,
message: `Reset password for user ${params.userId}.`,
};
},
async update_user_role(params: { id: string; systemRole: SystemRole }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.updateRole(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: `Updated role for ${user.name} to ${user.systemRole}.`,
};
},
async update_user_name(params: { id: string; name: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.updateName(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: `Updated user name to ${user.name}.`,
};
},
async link_user_resource(params: { userId: string; resourceId?: string | null }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.linkResource({
userId: params.userId,
resourceId: params.resourceId ?? null,
});
} catch (error) {
const mapped = toAssistantUserResourceLinkError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user", "resource"],
...result,
message: params.resourceId ? "Linked user to resource." : "Unlinked user resource.",
};
},
async auto_link_users_by_email(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.autoLinkAllByEmail();
return {
__action: "invalidate",
scope: ["user", "resource"],
success: true,
...result,
message: `Auto-linked ${result.linked} user(s) by email.`,
};
},
async set_user_permissions(params: {
userId: string;
overrides?: {
granted?: string[];
denied?: string[];
chapterIds?: string[];
} | null;
}, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.setPermissions({
userId: params.userId,
overrides: params.overrides ?? null,
});
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: params.overrides ? "Updated user permission overrides." : "Cleared user permission overrides.",
};
},
async reset_user_permissions(params: { userId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.resetPermissions(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: "Reset user permissions to role defaults.",
};
},
async get_effective_user_permissions(params: { userId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
try {
return await caller.getEffectivePermissions(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async disable_user_totp(params: { userId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.disableTotp(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: `Disabled TOTP for user ${params.userId}.`,
};
},
async list_notifications(params: { unreadOnly?: boolean; limit?: number }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.list({
...(params.unreadOnly !== undefined ? { unreadOnly: params.unreadOnly } : {}),
...(params.limit !== undefined ? { limit: Math.min(params.limit, 100) } : {}),
});
},
async mark_notification_read(params: { notificationId?: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
try {
await caller.markRead({
...(params.notificationId !== undefined ? { id: params.notificationId } : {}),
});
} catch (error) {
const mapped = toAssistantNotificationReadError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
message: params.notificationId ? "Notification marked as read." : "All unread notifications marked as read.",
};
},
async get_unread_notification_count(_params: Record<string, never>, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const count = await caller.unreadCount();
return { count };
},
async create_notification(params: {
userId: string;
type: string;
title: string;
body?: string;
entityId?: string;
entityType?: string;
category?: "NOTIFICATION" | "REMINDER" | "TASK" | "APPROVAL";
priority?: "LOW" | "NORMAL" | "HIGH" | "URGENT";
link?: string;
taskStatus?: "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED";
taskAction?: string;
assigneeId?: string;
dueDate?: string;
channel?: "in_app" | "email" | "both";
senderId?: string;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const dueDate = parseOptionalDateTime(params.dueDate, "dueDate");
let notification;
try {
notification = await caller.create({
userId: params.userId,
type: params.type,
title: params.title,
...(params.body !== undefined ? { body: params.body } : {}),
...(params.entityId !== undefined ? { entityId: params.entityId } : {}),
...(params.entityType !== undefined ? { entityType: params.entityType } : {}),
...(params.category !== undefined ? { category: params.category } : {}),
...(params.priority !== undefined ? { priority: params.priority } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
...(params.taskStatus !== undefined ? { taskStatus: params.taskStatus } : {}),
...(params.taskAction !== undefined ? { taskAction: params.taskAction } : {}),
...(params.assigneeId !== undefined ? { assigneeId: params.assigneeId } : {}),
...(dueDate ? { dueDate } : {}),
...(params.channel !== undefined ? { channel: params.channel } : {}),
...(params.senderId !== undefined ? { senderId: params.senderId } : {}),
});
} catch (error) {
const mapped = toAssistantNotificationCreationError(error, "notification");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
notification,
notificationId: notification?.id ?? null,
message: `Created notification "${params.title}".`,
};
},
// ── DASHBOARD DETAIL ──
async get_dashboard_detail(params: { section?: string }, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getDetail({ ...(params.section ? { section: params.section } : {}) });
},
// ── PROJECT MANAGEMENT ──
async delete_project(params: { projectId: string }, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) return project;
const caller = createProjectCaller(createScopedCallerContext(ctx));
try {
await caller.delete({ id: project.id });
} catch (error) {
const mapped = toAssistantProjectNotFoundError(error, params.projectId);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Deleted project: ${project.name} (${project.shortCode})`,
};
},
// ── ORG UNIT MANAGEMENT ──
// ─── Cover Art ───────────────────────────────────────────────────────────
async generate_project_cover(params: { projectId: string; prompt?: string }, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const caller = createProjectCaller(createScopedCallerContext(ctx));
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const { coverImageUrl } = await caller.generateCover({
projectId: project.id,
...(params.prompt !== undefined ? { prompt: params.prompt } : {}),
});
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Generated cover art for project "${project.name}"`,
coverImageUrl: coverImageUrl.slice(0, 100) + "...[truncated]",
};
},
async remove_project_cover(params: { projectId: string }, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const caller = createProjectCaller(createScopedCallerContext(ctx));
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
await caller.removeCover({ projectId: project.id });
return { __action: "invalidate", scope: ["project"], success: true, message: `Removed cover art from project "${project.name}"` };
},
// ── TASK MANAGEMENT ──
async list_tasks(params: {
status?: "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED";
includeAssigned?: boolean;
limit?: number;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.listTasks({
...(params.status !== undefined ? { status: params.status } : {}),
...(params.includeAssigned !== undefined ? { includeAssigned: params.includeAssigned } : {}),
...(params.limit !== undefined ? { limit: Math.min(params.limit, 100) } : {}),
});
},
async get_task_counts(_params: Record<string, never>, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.taskCounts();
},
async get_task_detail(params: { taskId: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
try {
return await caller.getTaskDetail({ id: params.taskId });
} catch (error) {
const mapped = toAssistantTaskNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async update_task_status(params: { taskId: string; status: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
let task;
try {
task = await caller.updateTaskStatus({
id: params.taskId,
status: params.status as "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED",
});
} catch (error) {
const mapped = toAssistantTaskNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task,
message: `Task status updated to ${task.taskStatus ?? params.status}.`,
};
},
async execute_task_action(params: { taskId: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.executeTaskAction({ id: params.taskId });
} catch (error) {
const mapped = toAssistantTaskActionError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task: result.task,
message: result.actionResult.message,
};
},
async create_reminder(params: {
title: string;
body?: string;
remindAt: string;
recurrence?: string;
entityId?: string;
entityType?: string;
link?: string;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
if (!params.title.trim()) {
return { error: "Reminder title is required." };
}
if (params.title.length > 200) {
return { error: "Reminder title must be at most 200 characters." };
}
if (params.body !== undefined && params.body.length > 2000) {
return { error: "Reminder body must be at most 2000 characters." };
}
if (
params.recurrence !== undefined
&& !["daily", "weekly", "monthly"].includes(params.recurrence)
) {
return {
error: `Invalid recurrence: ${params.recurrence}. Valid values: daily, weekly, monthly.`,
};
}
const remindAt = parseDateTime(params.remindAt, "remindAt");
let reminder;
try {
reminder = await caller.createReminder({
title: params.title,
remindAt,
...(params.body !== undefined ? { body: params.body } : {}),
...(params.recurrence !== undefined ? { recurrence: params.recurrence as "daily" | "weekly" | "monthly" } : {}),
...(params.entityId !== undefined ? { entityId: params.entityId } : {}),
...(params.entityType !== undefined ? { entityType: params.entityType } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
});
} catch (error) {
const mapped = toAssistantReminderCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
reminder,
reminderId: reminder.id,
message: `Reminder "${params.title}" created.`,
};
},
async list_reminders(params: { limit?: number }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.listReminders({
...(params.limit !== undefined ? { limit: Math.min(params.limit, 100) } : {}),
});
},
async update_reminder(params: {
id: string;
title?: string;
body?: string;
remindAt?: string;
recurrence?: "daily" | "weekly" | "monthly" | null;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const remindAt = parseOptionalDateTime(params.remindAt, "remindAt");
let reminder;
try {
reminder = await caller.updateReminder({
id: params.id,
...(params.title !== undefined ? { title: params.title } : {}),
...(params.body !== undefined ? { body: params.body } : {}),
...(remindAt ? { remindAt } : {}),
...(params.recurrence !== undefined ? { recurrence: params.recurrence } : {}),
});
} catch (error) {
const mapped = toAssistantReminderNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
reminder,
reminderId: reminder.id,
message: `Updated reminder ${params.id}.`,
};
},
async delete_reminder(params: { id: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
try {
await caller.deleteReminder({ id: params.id });
} catch (error) {
const mapped = toAssistantReminderNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
id: params.id,
message: `Deleted reminder ${params.id}.`,
};
},
async create_task_for_user(params: {
userId: string;
title: string;
body?: string;
priority?: string;
dueDate?: string;
taskAction?: string;
entityId?: string;
entityType?: string;
link?: string;
channel?: "in_app" | "email" | "both";
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const dueDate = parseOptionalDateTime(params.dueDate, "dueDate");
let task;
try {
task = await caller.createTask({
userId: params.userId,
title: params.title,
...(params.body !== undefined ? { body: params.body } : {}),
...(params.priority !== undefined ? { priority: params.priority as "LOW" | "NORMAL" | "HIGH" | "URGENT" } : {}),
...(dueDate ? { dueDate } : {}),
...(params.taskAction !== undefined ? { taskAction: params.taskAction } : {}),
...(params.entityId !== undefined ? { entityId: params.entityId } : {}),
...(params.entityType !== undefined ? { entityType: params.entityType } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
...(params.channel !== undefined ? { channel: params.channel } : {}),
});
} catch (error) {
const mapped = toAssistantNotificationCreationError(error, "task");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task,
taskId: task?.id ?? null,
message: `Created task "${params.title}" for ${params.userId}.`,
};
},
async assign_task(params: { id: string; assigneeId: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
let task;
try {
task = await caller.assignTask(params);
} catch (error) {
const mapped = toAssistantTaskAssignmentError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task,
taskId: task.id,
message: `Assigned task ${params.id} to ${params.assigneeId}.`,
};
},
async send_broadcast(params: {
title: string;
body?: string;
targetType: string;
targetValue?: string;
category?: "NOTIFICATION" | "REMINDER" | "TASK" | "APPROVAL";
priority?: string;
channel?: string;
link?: string;
scheduledAt?: string;
taskAction?: string;
dueDate?: string;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const scheduledAt = parseOptionalDateTime(params.scheduledAt, "scheduledAt");
const dueDate = parseOptionalDateTime(params.dueDate, "dueDate");
let broadcast;
try {
broadcast = await caller.createBroadcast({
title: params.title,
targetType: params.targetType as "user" | "role" | "project" | "orgUnit" | "all",
...(params.body !== undefined ? { body: params.body } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
...(params.category !== undefined ? { category: params.category } : {}),
...(params.priority !== undefined ? { priority: params.priority as "LOW" | "NORMAL" | "HIGH" | "URGENT" } : {}),
...(params.channel !== undefined ? { channel: params.channel as "in_app" | "email" | "both" } : {}),
...(params.targetValue !== undefined ? { targetValue: params.targetValue } : {}),
...(scheduledAt ? { scheduledAt } : {}),
...(params.taskAction !== undefined ? { taskAction: params.taskAction } : {}),
...(dueDate ? { dueDate } : {}),
});
} catch (error) {
const mapped = toAssistantNotificationCreationError(error, "broadcast");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
broadcast,
broadcastId: broadcast.id,
recipientCount: broadcast.recipientCount ?? 0,
message: `Broadcast "${params.title}" created.`,
};
},
async list_broadcasts(params: { limit?: number }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.listBroadcasts({
...(params.limit !== undefined ? { limit: Math.min(params.limit, 50) } : {}),
});
},
async get_broadcast_detail(params: { id: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
try {
return await caller.getBroadcastById({ id: params.id });
} catch (error) {
const mapped = toAssistantBroadcastNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async delete_notification(params: { id: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
try {
await caller.delete({ id: params.id });
} catch (error) {
const mapped = toAssistantNotificationDeletionError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["notification"],
success: true,
id: params.id,
message: `Deleted notification ${params.id}.`,
};
},
// ── INSIGHTS & ANOMALIES ──────────────────────────────────────────────────
async detect_anomalies(_params: Record<string, never>, ctx: ToolContext) {
const caller = createInsightsCaller(createScopedCallerContext(ctx));
return caller.getAnomalyDetail();
},
async get_skill_gaps(_params: Record<string, never>, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getSkillGapSummary();
},
async get_project_health(_params: Record<string, never>, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getProjectHealthDetail();
},
async get_budget_forecast(_params: Record<string, never>, ctx: ToolContext) {
assertPermission(ctx, "viewCosts" as PermissionKey);
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getBudgetForecastDetail();
},
async get_insights_summary(_params: Record<string, never>, ctx: ToolContext) {
const caller = createInsightsCaller(createScopedCallerContext(ctx));
return caller.getInsightsSummary();
},
async run_report(params: {
entity: string;
columns: string[];
filters?: Array<{
field: string;
op: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains" | "in";
value: string;
}>;
periodMonth?: string;
groupBy?: string;
sortBy?: string;
sortDir?: "asc" | "desc";
limit?: number;
}, ctx: ToolContext) {
const entity = params.entity as "resource" | "project" | "assignment" | "resource_month";
if (!["resource", "project", "assignment", "resource_month"].includes(entity)) {
return {
error:
`Unknown entity: ${params.entity}. Use resource, project, assignment, or resource_month.`,
};
}
const caller = createReportCaller(createScopedCallerContext(ctx));
const result = await caller.getReportData({
entity,
columns: params.columns,
filters: params.filters ?? [],
periodMonth: params.periodMonth,
groupBy: params.groupBy,
sortBy: params.sortBy,
sortDir: params.sortDir ?? "asc",
limit: Math.min(params.limit ?? 50, 200),
offset: 0,
});
return {
rows: result.rows,
rowCount: result.rows.length,
totalCount: result.totalCount,
columns: result.columns,
groups: result.groups,
};
},
async list_comments(params: { entityType: CommentEntityType; entityId: string }, ctx: ToolContext) {
const caller = createCommentCaller(createScopedCallerContext(ctx));
const comments = await caller.list({
entityType: params.entityType,
entityId: params.entityId,
});
return comments.map((c) => ({
id: c.id,
author: c.author.name ?? c.author.email,
body: c.body,
resolved: c.resolved,
createdAt: c.createdAt.toISOString(),
replyCount: c.replies.length,
replies: c.replies.map((r) => ({
id: r.id,
author: r.author.name ?? r.author.email,
body: r.body,
resolved: r.resolved,
createdAt: r.createdAt.toISOString(),
})),
}));
},
async lookup_rate(params: {
clientId?: string;
chapter?: string;
managementLevelId?: string;
roleName?: string;
seniority?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "viewCosts" as PermissionKey);
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const result = await caller.lookupBestMatch(params);
return {
...(result.message ? { message: result.message } : {}),
bestMatch: result.bestMatch
? {
...result.bestMatch,
costRate: fmtEur(result.bestMatch.costRateCents),
billRate: result.bestMatch.billRateCents ? fmtEur(result.bestMatch.billRateCents) : null,
}
: null,
alternatives: result.alternatives.map((alternative) => ({
...alternative,
costRate: fmtEur(alternative.costRateCents),
billRate: alternative.billRateCents ? fmtEur(alternative.billRateCents) : null,
})),
totalCandidates: result.totalCandidates,
};
},
// ── SCENARIO & AI ─────────────────────────────────────────────────────────
async simulate_scenario(params: {
projectId: string;
changes: Array<{
assignmentId?: string;
resourceId?: string;
roleId?: string;
startDate: string;
endDate: string;
hoursPerDay: number;
remove?: boolean;
}>;
}, ctx: ToolContext) {
const caller = createScenarioCaller(createScopedCallerContext(ctx));
const result = await caller.simulate({
projectId: params.projectId,
changes: params.changes.map((change) => ({
...change,
startDate: new Date(change.startDate),
endDate: new Date(change.endDate),
})),
});
return {
baseline: {
...result.baseline,
totalCost: fmtEur(result.baseline.totalCostCents),
},
scenario: {
...result.scenario,
totalCost: fmtEur(result.scenario.totalCostCents),
},
delta: {
...result.delta,
cost: fmtEur(result.delta.costCents),
},
resourceImpacts: result.resourceImpacts,
warnings: result.warnings,
budgetCents: result.budgetCents,
};
},
async generate_project_narrative(params: { projectId: string }, ctx: ToolContext) {
const caller = createInsightsCaller(createScopedCallerContext(ctx));
return caller.generateProjectNarrative({ projectId: params.projectId });
},
async create_comment(params: {
entityType: CommentEntityType;
entityId: string;
body: string;
}, ctx: ToolContext) {
if (params.body.length === 0) {
return { error: "Comment body is required." };
}
if (params.body.length > 10_000) {
return { error: "Comment body must be at most 10000 characters." };
}
const caller = createCommentCaller(createScopedCallerContext(ctx));
let comment;
try {
comment = await caller.create({
entityType: params.entityType,
entityId: params.entityId,
body: params.body,
});
} catch (error) {
const mapped = toAssistantCommentCreationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["comment"],
id: comment.id,
author: comment.author.name ?? comment.author.email,
body: comment.body,
createdAt: comment.createdAt.toISOString(),
};
},
async resolve_comment(params: {
commentId: string;
resolved?: boolean;
}, ctx: ToolContext) {
const caller = createCommentCaller(createScopedCallerContext(ctx));
let updated;
try {
updated = await caller.resolve({
id: params.commentId,
resolved: params.resolved !== false,
});
} catch (error) {
const mapped = toAssistantCommentResolveError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["comment"],
id: updated.id,
resolved: updated.resolved,
author: updated.author.name ?? updated.author.email,
body: updated.body.slice(0, 100),
};
},
async query_change_history(params: {
entityType?: string;
search?: string;
userId?: string;
daysBack?: number;
action?: string;
limit?: number;
}, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const daysBack = params.daysBack ?? 7;
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
const result = await caller.listDetail({
...(params.entityType ? { entityType: params.entityType } : {}),
...(params.userId ? { userId: params.userId } : {}),
...(params.action ? { action: params.action } : {}),
...(params.search ? { search: params.search } : {}),
startDate,
limit,
});
return {
filters: {
entityType: params.entityType ?? null,
userId: params.userId ?? null,
action: params.action ?? null,
search: params.search ?? null,
daysBack,
},
itemCount: result.items.length,
nextCursor: result.nextCursor ?? null,
items: result.items,
};
},
async get_entity_timeline(params: {
entityType: string;
entityId: string;
limit?: number;
}, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 50, 200);
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getByEntityDetail({
entityType: params.entityType,
entityId: params.entityId,
limit,
});
},
async export_resources_csv(_params: Record<string, never>, ctx: ToolContext) {
const caller = createImportExportCaller(createScopedCallerContext(ctx));
const csv = await caller.exportResourcesCSV();
return {
format: "csv",
lineCount: csv.length === 0 ? 0 : csv.split("\n").length,
csv,
};
},
async export_projects_csv(_params: Record<string, never>, ctx: ToolContext) {
const caller = createImportExportCaller(createScopedCallerContext(ctx));
const csv = await caller.exportProjectsCSV();
return {
format: "csv",
lineCount: csv.length === 0 ? 0 : csv.split("\n").length,
csv,
};
},
async import_csv_data(params: {
entityType: "resources" | "projects" | "allocations";
rows: Array<Record<string, string>>;
dryRun?: boolean;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.IMPORT_DATA);
const caller = createImportExportCaller(createScopedCallerContext(ctx));
return caller.importCSV({
entityType: params.entityType,
rows: params.rows,
dryRun: params.dryRun ?? true,
});
},
async list_dispo_import_batches(params: {
status?: ImportBatchStatus;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listImportBatches({
...(params.status ? { status: params.status } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async get_dispo_import_batch(params: {
id: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
try {
return await caller.getImportBatch({ id: params.id });
} catch (error) {
const mapped = toAssistantDispoImportBatchNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async stage_dispo_import_batch(params: {
chargeabilityWorkbookPath: string;
costWorkbookPath?: string;
notes?: string | null;
planningWorkbookPath: string;
referenceWorkbookPath: string;
rosterWorkbookPath?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.stageImportBatch({
chargeabilityWorkbookPath: params.chargeabilityWorkbookPath,
planningWorkbookPath: params.planningWorkbookPath,
referenceWorkbookPath: params.referenceWorkbookPath,
...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}),
...(params.notes !== undefined ? { notes: params.notes } : {}),
...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}),
});
},
async validate_dispo_import_batch(params: {
chargeabilityWorkbookPath: string;
costWorkbookPath?: string;
importBatchId?: string;
notes?: string | null;
planningWorkbookPath: string;
referenceWorkbookPath: string;
rosterWorkbookPath?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.validateImportBatch({
chargeabilityWorkbookPath: params.chargeabilityWorkbookPath,
planningWorkbookPath: params.planningWorkbookPath,
referenceWorkbookPath: params.referenceWorkbookPath,
...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}),
...(params.importBatchId !== undefined ? { importBatchId: params.importBatchId } : {}),
...(params.notes !== undefined ? { notes: params.notes } : {}),
...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}),
});
},
async cancel_dispo_import_batch(params: {
id: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.cancelImportBatch({ id: params.id });
},
async list_dispo_staged_resources(params: {
importBatchId: string;
status?: StagedRecordStatus;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedResources({
importBatchId: params.importBatchId,
...(params.status !== undefined ? { status: params.status } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_projects(params: {
importBatchId: string;
status?: StagedRecordStatus;
isTbd?: boolean;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedProjects({
importBatchId: params.importBatchId,
...(params.status !== undefined ? { status: params.status } : {}),
...(params.isTbd !== undefined ? { isTbd: params.isTbd } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_assignments(params: {
importBatchId: string;
status?: StagedRecordStatus;
resourceExternalId?: string;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedAssignments({
importBatchId: params.importBatchId,
...(params.status !== undefined ? { status: params.status } : {}),
...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_vacations(params: {
importBatchId: string;
resourceExternalId?: string;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedVacations({
importBatchId: params.importBatchId,
...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_unresolved_records(params: {
importBatchId: string;
recordType?: DispoStagedRecordType;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedUnresolvedRecords({
importBatchId: params.importBatchId,
...(params.recordType !== undefined ? { recordType: params.recordType } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async resolve_dispo_staged_record(params: {
action: "APPROVE" | "REJECT" | "SKIP";
id: string;
recordType: DispoStagedRecordType;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.resolveStagedRecord({
action: params.action,
id: params.id,
recordType: params.recordType,
});
},
async commit_dispo_import_batch(params: {
allowTbdUnresolved?: boolean;
importBatchId: string;
importTbdProjects?: boolean;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.commitImportBatch({
importBatchId: params.importBatchId,
...(params.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: params.allowTbdUnresolved } : {}),
...(params.importTbdProjects !== undefined ? { importTbdProjects: params.importTbdProjects } : {}),
});
},
...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 };
}
}