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

9692 lines
332 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, Prisma, ImportBatchStatus, StagedRecordStatus, DispoStagedRecordType, VacationType } from "@capakraken/db";
import {
CreateAssignmentSchema,
CreateClientSchema,
CreateCountrySchema,
type CreateEstimateInput,
CreateHolidayCalendarEntrySchema,
CreateHolidayCalendarSchema,
CreateProjectSchema,
CreateResourceSchema,
CreateMetroCitySchema,
CreateOrgUnitSchema,
CreateRoleSchema,
AllocationStatus,
EstimateExportFormat,
EstimateStatus,
PermissionKey,
PreviewResolvedHolidaysSchema,
SystemRole,
UpdateClientSchema,
type UpdateEstimateDraftInput,
UpdateCountrySchema,
UpdateHolidayCalendarEntrySchema,
UpdateHolidayCalendarSchema,
UpdateAssignmentSchema,
UpdateMetroCitySchema,
UpdateOrgUnitSchema,
UpdateProjectSchema,
UpdateRoleSchema,
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 { isAiConfigured } from "../ai-client.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";
// ─── 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",
"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",
]);
// ─── Types ──────────────────────────────────────────────────────────────────
export type ToolContext = {
db: typeof prisma;
userId: string;
userRole: string;
permissions: Set<PermissionKey>;
session?: TRPCContext["session"];
dbUser?: TRPCContext["dbUser"];
roleDefaults?: TRPCContext["roleDefaults"];
};
export interface ToolDef {
type: "function";
function: {
name: string;
description: string;
parameters: Record<string, unknown>;
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ToolExecutor = (params: any, ctx: ToolContext) => Promise<unknown>;
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 };
};
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);
}
}
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 === "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. 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"],
},
},
},
{
type: "function",
function: {
name: "find_best_project_resource",
description: "Advanced assistant tool: find the best already-assigned resource on a project for a given period, ranked by remaining capacity or LCR. Holiday- and vacation-aware. Requires viewCosts and advanced assistant permissions.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
minHoursPerDay: { type: "number", description: "Minimum remaining availability per effective working day. Default: 3." },
rankingMode: { type: "string", description: "Ranking mode: lowest_lcr, highest_remaining_hours_per_day, or highest_remaining_hours. Default: lowest_lcr." },
chapter: { type: "string", description: "Optional chapter filter for candidate resources." },
roleName: { type: "string", description: "Optional role filter for candidate resources." },
},
required: ["projectIdentifier"],
},
},
},
{
type: "function",
function: {
name: "get_timeline_entries_view",
description: "Advanced assistant tool: read-only timeline entries view with the same timeline/disposition readmodel used by the app. Returns allocations, demands, assignments, and matching holiday overlays for a period.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the view." },
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the view." },
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the view." },
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the view." },
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
},
},
},
},
{
type: "function",
function: {
name: "get_timeline_holiday_overlays",
description: "Advanced assistant tool: read-only holiday overlays for the timeline, resolved with the same holiday logic as the app. Useful to explain regional holiday differences for assigned or filtered resources.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the overlays." },
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the overlays via matching assignments." },
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the overlays via matching projects." },
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the overlays." },
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
},
},
},
},
{
type: "function",
function: {
name: "get_project_timeline_context",
description: "Advanced assistant tool: read-only project timeline/disposition context. Reuses the same project context readmodel as the app and adds holiday overlays plus cross-project overlap summaries for assigned resources.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Optional holiday/conflict window start date in YYYY-MM-DD. Defaults to the project start date when available." },
endDate: { type: "string", description: "Optional holiday/conflict window end date in YYYY-MM-DD. Defaults to the project end date when available." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted." },
},
required: ["projectIdentifier"],
},
},
},
{
type: "function",
function: {
name: "preview_project_shift",
description: "Advanced assistant tool: read-only preview of the timeline shift validation for a project. Uses the same preview logic as the timeline router and does not write changes.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
newStartDate: { type: "string", description: "New start date in YYYY-MM-DD." },
newEndDate: { type: "string", description: "New end date in YYYY-MM-DD." },
},
required: ["projectIdentifier", "newStartDate", "newEndDate"],
},
},
},
{
type: "function",
function: {
name: "update_timeline_allocation_inline",
description: "Advanced assistant mutation: update a timeline allocation inline with the same manager/admin + manageAllocations validation as the timeline API. Supports hours/day, dates, includeSaturday, and role changes. Requires useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
allocationId: { type: "string", description: "Allocation, assignment, or demand row ID to update." },
hoursPerDay: { type: "number", description: "Optional new booked hours per day." },
startDate: { type: "string", description: "Optional new start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional new end date in YYYY-MM-DD." },
includeSaturday: { type: "boolean", description: "Optional Saturday-working flag stored in metadata." },
role: { type: "string", description: "Optional new role label." },
},
required: ["allocationId"],
},
},
},
{
type: "function",
function: {
name: "apply_timeline_project_shift",
description: "Advanced assistant mutation: apply the real timeline project shift mutation, including validation, date movement, cost recalculation, audit logging, and SSE. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
newStartDate: { type: "string", description: "New project start date in YYYY-MM-DD." },
newEndDate: { type: "string", description: "New project end date in YYYY-MM-DD." },
},
required: ["projectIdentifier", "newStartDate", "newEndDate"],
},
},
},
{
type: "function",
function: {
name: "quick_assign_timeline_resource",
description: "Advanced assistant mutation: create a timeline quick-assignment with the same manager/admin + manageAllocations rules as the timeline UI. Resolves resource and project identifiers before calling the real mutation. Requires useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
role: { type: "string", description: "Role label. Default: Team Member." },
roleId: { type: "string", description: "Optional concrete role ID." },
status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
},
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "batch_quick_assign_timeline_resources",
description: "Advanced assistant mutation: batch-create timeline quick-assignments using the same timeline router logic, permission checks, and audit/SSE side effects as the app. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
assignments: {
type: "array",
minItems: 1,
maxItems: 50,
items: {
type: "object",
properties: {
resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
role: { type: "string", description: "Role label. Default: Team Member." },
status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
},
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
},
description: "Assignment rows to create in one batch.",
},
},
required: ["assignments"],
},
},
},
{
type: "function",
function: {
name: "batch_shift_timeline_allocations",
description: "Advanced assistant mutation: shift multiple timeline allocations by a shared day delta using the real timeline batch move/resize mutation. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
allocationIds: { type: "array", items: { type: "string" }, description: "Allocation IDs to shift." },
daysDelta: { type: "integer", description: "Signed day delta to apply." },
mode: { type: "string", enum: ["move", "resize-start", "resize-end"], description: "Shift mode. Default: move." },
},
required: ["allocationIds", "daysDelta"],
},
},
},
{
type: "function",
function: {
name: "list_allocations",
description: "List assignments/allocations, optionally filtered by resource or project. Shows who is assigned where, hours/day, dates, and cost.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Filter by resource ID" },
projectId: { type: "string", description: "Filter by project ID" },
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
projectCode: { type: "string", description: "Filter by project short code (partial match)" },
status: { type: "string", description: "Filter by status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
limit: { type: "integer", description: "Max results. Default: 30" },
},
},
},
},
{
type: "function",
function: {
name: "get_budget_status",
description: "Get the budget status of a project: total budget, confirmed/proposed costs, remaining, utilization percentage.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "get_vacation_balance",
description: "Get the holiday-aware vacation balance for a resource via the real entitlement workflow. Authenticated users can read their own balance; manager/admin/controller can read broader balances.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
year: { type: "integer", description: "Year. Default: current year" },
},
required: ["resourceId"],
},
},
},
{
type: "function",
function: {
name: "list_vacations_upcoming",
description: "List upcoming vacations across all resources, or for a specific resource/team. Shows who is off when.",
parameters: {
type: "object",
properties: {
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
chapter: { type: "string", description: "Filter by chapter/team" },
daysAhead: { type: "integer", description: "How many days ahead to look. Default: 30" },
limit: { type: "integer", description: "Max results. Default: 30" },
},
},
},
},
{
type: "function",
function: {
name: "list_holidays_by_region",
description: "List resolved public holidays for a country, federal state, and optionally a city in a given year or date range. Use this to compare regions such as Bayern vs Hamburg.",
parameters: {
type: "object",
properties: {
countryCode: { type: "string", description: "Country code such as DE, ES, US, IN." },
federalState: { type: "string", description: "Federal state / region code, e.g. BY, HH, NRW." },
metroCity: { type: "string", description: "Optional city name for local city-specific holidays, e.g. Augsburg." },
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
},
required: ["countryCode"],
},
},
},
{
type: "function",
function: {
name: "get_resource_holidays",
description: "List resolved public holidays for a specific resource based on that person's country, federal state, and city context.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Resource ID, EID, or display name." },
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "list_holiday_calendars",
description: "List holiday calendars including scope, assignment, active flag, priority, and entry count. Useful to inspect the calendar-editor configuration context.",
parameters: {
type: "object",
properties: {
includeInactive: { type: "boolean", description: "Include inactive calendars. Default: false." },
countryCode: { type: "string", description: "Optional country code filter such as DE or ES." },
scopeType: { type: "string", description: "Optional scope filter: COUNTRY, STATE, CITY." },
stateCode: { type: "string", description: "Optional state/region code filter such as BY or NRW." },
metroCity: { type: "string", description: "Optional city-name filter." },
},
},
},
},
{
type: "function",
function: {
name: "get_holiday_calendar",
description: "Get a single holiday calendar including all entries. Accepts either the calendar ID or its name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Holiday calendar ID or name." },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "preview_resolved_holiday_calendar",
description: "Preview the resolved holiday result for a country/state/city scope and year, including which calendar each holiday comes from.",
parameters: {
type: "object",
properties: {
countryId: { type: "string", description: "Country ID." },
stateCode: { type: "string", description: "Optional state/region code." },
metroCityId: { type: "string", description: "Optional metro city ID for city-specific preview." },
year: { type: "integer", description: "Full year, e.g. 2026." },
},
required: ["countryId", "year"],
},
},
},
{
type: "function",
function: {
name: "create_holiday_calendar",
description: "Create a holiday calendar for a country, state, or city scope. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Calendar name." },
scopeType: { type: "string", description: "COUNTRY, STATE, or CITY." },
countryId: { type: "string", description: "Country ID." },
stateCode: { type: "string", description: "Required for STATE calendars." },
metroCityId: { type: "string", description: "Required for CITY calendars." },
isActive: { type: "boolean", description: "Whether the calendar is active. Default: true." },
priority: { type: "integer", description: "Priority used during calendar resolution. Default: 0." },
},
required: ["name", "scopeType", "countryId"],
},
},
},
{
type: "function",
function: {
name: "update_holiday_calendar",
description: "Update an existing holiday calendar. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar ID." },
data: {
type: "object",
properties: {
name: { type: "string" },
stateCode: { type: "string" },
metroCityId: { type: "string" },
isActive: { type: "boolean" },
priority: { type: "integer" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_holiday_calendar",
description: "Delete a holiday calendar and all of its entries. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_holiday_calendar_entry",
description: "Create a holiday entry in an existing calendar. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
holidayCalendarId: { type: "string", description: "Holiday calendar ID." },
date: { type: "string", description: "Date in YYYY-MM-DD format." },
name: { type: "string", description: "Holiday name." },
isRecurringAnnual: { type: "boolean", description: "Whether the holiday repeats every year." },
source: { type: "string", description: "Optional source or legal basis." },
},
required: ["holidayCalendarId", "date", "name"],
},
},
},
{
type: "function",
function: {
name: "update_holiday_calendar_entry",
description: "Update an existing holiday calendar entry. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar entry ID." },
data: {
type: "object",
properties: {
date: { type: "string", description: "Date in YYYY-MM-DD format." },
name: { type: "string" },
isRecurringAnnual: { type: "boolean" },
source: { type: "string" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_holiday_calendar_entry",
description: "Delete a holiday calendar entry. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar entry ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "list_roles",
description: "List all available roles with their colors.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "search_by_skill",
description: "Find resources that have a specific skill.",
parameters: {
type: "object",
properties: {
skill: { type: "string", description: "Skill name to search for" },
},
required: ["skill"],
},
},
},
{
type: "function",
function: {
name: "get_statistics",
description: "Get overview statistics: total resources, projects, active allocations, budget summary, projects by status, chapter breakdown.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_chargeability",
description: "Get chargeability data for a resource in a given month: hours booked vs available, chargeability %, target comparison.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, eid, or name" },
month: { type: "string", description: "Month in YYYY-MM format, e.g. 2026-03. Default: current month" },
},
required: ["resourceId"],
},
},
},
{
type: "function",
function: {
name: "get_chargeability_report",
description: "Get the detailed chargeability report readmodel for a month range, including group totals and per-resource month series. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
startMonth: { type: "string", description: "Start month in YYYY-MM format." },
endMonth: { type: "string", description: "End month in YYYY-MM format." },
orgUnitId: { type: "string", description: "Optional org unit filter." },
managementLevelGroupId: { type: "string", description: "Optional management level group filter." },
countryId: { type: "string", description: "Optional country filter." },
includeProposed: { type: "boolean", description: "Whether proposed bookings should count towards chargeability. Default: false." },
resourceQuery: { type: "string", description: "Optional resource filter by name or eid after loading the report." },
resourceLimit: { type: "integer", description: "Maximum number of resources returned. Default: 25, max 100." },
},
required: ["startMonth", "endMonth"],
},
},
},
{
type: "function",
function: {
name: "get_resource_computation_graph",
description: "Get the resource computation graph with transparent SAH, holiday, absence, allocation, chargeability, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, eid, or display name." },
month: { type: "string", description: "Month in YYYY-MM format." },
domain: { type: "string", enum: ["INPUT", "SAH", "ALLOCATION", "RULES", "CHARGEABILITY", "BUDGET"], description: "Optional domain filter for graph nodes." },
includeLinks: { type: "boolean", description: "Include graph links for the selected nodes. Default: false." },
},
required: ["resourceId", "month"],
},
},
},
{
type: "function",
function: {
name: "get_project_computation_graph",
description: "Get the project computation graph with estimate, commercial, effort, experience, spread, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID, short code, or project name." },
domain: { type: "string", enum: ["INPUT", "ESTIMATE", "COMMERCIAL", "EXPERIENCE", "EFFORT", "SPREAD", "BUDGET"], description: "Optional domain filter for graph nodes." },
includeLinks: { type: "boolean", description: "Include graph links for the selected nodes. Default: false." },
},
required: ["projectId"],
},
},
},
{
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: "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 ──
{
type: "function",
function: {
name: "create_allocation",
description: "Create a new allocation/booking for a resource on a project. Requires manageAllocations permission. Always confirm with the user before calling this. Created with PROPOSED status.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID" },
projectId: { type: "string", description: "Project 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 (e.g. 8)" },
role: { type: "string", description: "Optional role name" },
},
required: ["resourceId", "projectId", "startDate", "endDate", "hoursPerDay"],
},
},
},
{
type: "function",
function: {
name: "cancel_allocation",
description: "Cancel an existing allocation. Can find by allocation ID, or by resource name + project code + date range. Requires manageAllocations permission. Always confirm with the user before calling this.",
parameters: {
type: "object",
properties: {
allocationId: { type: "string", description: "Allocation ID (if known)" },
resourceName: { type: "string", description: "Resource name (partial match)" },
projectCode: { type: "string", description: "Project short code (partial match)" },
startDate: { type: "string", description: "Filter by start date YYYY-MM-DD" },
endDate: { type: "string", description: "Filter by end date YYYY-MM-DD" },
},
},
},
},
{
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_allocation_status",
description: "Change the status of an existing allocation. Can reactivate cancelled allocations, confirm proposed ones, etc. Requires manageAllocations permission. Always confirm with the user before calling.",
parameters: {
type: "object",
properties: {
allocationId: { type: "string", description: "Allocation ID" },
resourceName: { type: "string", description: "Resource name (partial match, used if no allocationId)" },
projectCode: { type: "string", description: "Project short code (partial match, used if no allocationId)" },
startDate: { type: "string", description: "Start date filter YYYY-MM-DD (used if no allocationId)" },
newStatus: { type: "string", description: "New status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
},
required: ["newStatus"],
},
},
},
{
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 ──
{
type: "function",
function: {
name: "create_role",
description: "Create a new role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Role name" },
description: { type: "string", description: "Optional role description" },
color: { type: "string", description: "Hex color (e.g. #3b82f6). Default: #6b7280" },
},
required: ["name"],
},
},
},
{
type: "function",
function: {
name: "update_role",
description: "Update a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Role ID" },
name: { type: "string", description: "New name" },
description: { type: "string", description: "New description" },
color: { type: "string", description: "New hex color" },
isActive: { type: "boolean", description: "Set active state" },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "delete_role",
description: "Delete a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Role ID" },
},
required: ["id"],
},
},
},
// ── CLIENTS ──
{
type: "function",
function: {
name: "create_client",
description: "Create a new client. Requires manager or admin role. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Client name" },
code: { type: "string", description: "Client code" },
parentId: { type: "string", description: "Optional parent client ID" },
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
tags: { type: "array", items: { type: "string" }, description: "Optional client tags" },
},
required: ["name"],
},
},
},
{
type: "function",
function: {
name: "update_client",
description: "Update a client. Requires manager or admin role. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Client ID" },
name: { type: "string", description: "New name" },
code: { type: "string", description: "New code" },
sortOrder: { type: "integer", description: "New sort order" },
isActive: { type: "boolean", description: "Set active state" },
parentId: { type: "string", description: "Parent client ID; use null to clear" },
tags: { type: "array", items: { type: "string" }, description: "Replacement client tags" },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "delete_client",
description: "Delete a client. Requires admin role. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Client ID" },
},
required: ["id"],
},
},
},
// ── 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"],
},
},
},
{
type: "function",
function: {
name: "create_country",
description: "Create a country with daily working hours and optional schedule rules. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
code: { type: "string", description: "ISO country code such as DE or ES." },
name: { type: "string", description: "Country name." },
dailyWorkingHours: { type: "number", description: "Standard daily working hours." },
scheduleRules: {
type: "object",
description: "Optional schedule rule object such as the Spain reduced-hours configuration.",
},
},
required: ["code", "name"],
},
},
},
{
type: "function",
function: {
name: "update_country",
description: "Update a country including working hours, schedule rules, or active state. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Country ID." },
data: {
type: "object",
properties: {
code: { type: "string" },
name: { type: "string" },
dailyWorkingHours: { type: "number" },
scheduleRules: { type: ["object", "null"] },
isActive: { type: "boolean" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "create_metro_city",
description: "Create a metro city for a country. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
countryId: { type: "string", description: "Country ID." },
name: { type: "string", description: "Metro city name." },
},
required: ["countryId", "name"],
},
},
},
{
type: "function",
function: {
name: "update_metro_city",
description: "Rename a metro city. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Metro city ID." },
data: {
type: "object",
properties: {
name: { type: "string" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_metro_city",
description: "Delete a metro city when no resource is assigned to it. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Metro city ID." },
},
required: ["id"],
},
},
},
{
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 ──
{
type: "function",
function: {
name: "create_org_unit",
description: "Create a new organizational unit. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Org unit name" },
shortName: { type: "string", description: "Short name/code" },
level: { type: "integer", description: "Level (5, 6, or 7)" },
parentId: { type: "string", description: "Parent org unit ID (optional)" },
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
},
required: ["name", "level"],
},
},
},
{
type: "function",
function: {
name: "update_org_unit",
description: "Update an organizational unit. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Org unit ID" },
name: { type: "string", description: "New name" },
shortName: { type: "string", description: "New short name" },
sortOrder: { type: "integer", description: "New sort order" },
isActive: { type: "boolean", description: "Set active state" },
parentId: { type: "string", description: "Parent org unit ID; use null to clear" },
},
required: ["id"],
},
},
},
// ── 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",
},
limit: { type: "integer", description: "Max results. Default: 50" },
},
required: ["entity", "columns"],
},
},
},
{
type: "function",
function: {
name: "list_comments",
description: "List comments (with replies) for a specific entity such as an estimate, scope item, or demand line.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'estimate', 'estimate_version', 'scope_item', 'demand_line')" },
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 an entity (estimate, scope item, demand line, etc.). Supports @mentions. Always confirm with the user first.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'estimate', 'estimate_version', 'scope_item')" },
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). Only the comment author or an admin can do this.",
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"],
},
},
},
{
type: "function",
function: {
name: "get_system_settings",
description: "Get sanitized system settings through the real settings router. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "update_system_settings",
description: "Update system settings through the real settings router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
aiProvider: { type: "string", enum: ["openai", "azure"] },
azureOpenAiEndpoint: { type: "string" },
azureOpenAiDeployment: { type: "string" },
azureOpenAiApiKey: { type: "string" },
azureApiVersion: { type: "string" },
aiMaxCompletionTokens: { type: "integer" },
aiTemperature: { type: "number" },
aiSummaryPrompt: { type: "string" },
scoreWeights: { type: "object" },
scoreVisibleRoles: { type: "array", items: { type: "string" } },
smtpHost: { type: "string" },
smtpPort: { type: "integer" },
smtpUser: { type: "string" },
smtpPassword: { type: "string" },
smtpFrom: { type: "string" },
smtpTls: { type: "boolean" },
anonymizationEnabled: { type: "boolean" },
anonymizationDomain: { type: "string" },
anonymizationSeed: { type: "string" },
anonymizationMode: { type: "string", enum: ["global"] },
azureDalleDeployment: { type: "string" },
azureDalleEndpoint: { type: "string" },
azureDalleApiKey: { type: "string" },
geminiApiKey: { type: "string" },
geminiModel: { type: "string" },
imageProvider: { type: "string", enum: ["dalle", "gemini"] },
vacationDefaultDays: { type: "integer" },
timelineUndoMaxSteps: { type: "integer" },
},
},
},
},
{
type: "function",
function: {
name: "test_ai_connection",
description: "Run the real AI connection test from system settings. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "test_smtp_connection",
description: "Run the real SMTP connection test from system settings. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "test_gemini_connection",
description: "Run the real Gemini connection test from system settings. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "get_ai_configured",
description: "Get whether AI is configured for the current system via the real settings router. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "list_system_role_configs",
description: "List system role configuration defaults via the real system-role-config router. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "update_system_role_config",
description: "Update one system role configuration via the real system-role-config router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
role: { type: "string", description: "System role key." },
label: { type: "string", description: "Optional role label." },
description: { type: "string", description: "Optional role description." },
color: { type: "string", description: "Optional role color." },
defaultPermissions: { type: "array", items: { type: "string" }, description: "Optional default permission set." },
},
required: ["role"],
},
},
},
{
type: "function",
function: {
name: "list_webhooks",
description: "List webhooks via the real webhook router. Secrets are masked in assistant responses. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "get_webhook",
description: "Get one webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_webhook",
description: "Create a webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Webhook name." },
url: { type: "string", description: "Webhook target URL." },
secret: { type: "string", description: "Optional webhook signing secret." },
events: { type: "array", items: { type: "string" }, description: "Subscribed webhook events." },
isActive: { type: "boolean", description: "Whether the webhook is active. Default: true." },
},
required: ["name", "url", "events"],
},
},
},
{
type: "function",
function: {
name: "update_webhook",
description: "Update a webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
data: {
type: "object",
properties: {
name: { type: "string" },
url: { type: "string" },
secret: { type: "string" },
events: { type: "array", items: { type: "string" } },
isActive: { type: "boolean" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_webhook",
description: "Delete a webhook via the real webhook router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "test_webhook",
description: "Send a real test payload to a webhook via the real webhook router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "list_audit_log_entries",
description: "List audit log entries with full audit-router filters and cursor pagination. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Optional entity type filter." },
entityId: { type: "string", description: "Optional entity ID filter." },
userId: { type: "string", description: "Optional user ID filter." },
action: { type: "string", description: "Optional action filter such as CREATE, UPDATE, DELETE, SHIFT, IMPORT." },
source: { type: "string", description: "Optional source filter such as ui or assistant." },
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
search: { type: "string", description: "Optional case-insensitive search across entity name, summary, and entity type." },
limit: { type: "integer", description: "Max results. Default: 50, max: 100." },
cursor: { type: "string", description: "Optional pagination cursor (last seen audit entry ID)." },
},
},
},
},
{
type: "function",
function: {
name: "get_audit_log_entry",
description: "Get one audit log entry including the full changes payload. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Audit log entry ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "get_audit_log_timeline",
description: "Get audit log entries grouped by day for a time window. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
limit: { type: "integer", description: "Max entries. Default: 200, max: 500." },
},
},
},
},
{
type: "function",
function: {
name: "get_audit_activity_summary",
description: "Get audit activity totals by entity type, action, and user for a date range. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
},
},
},
},
{
type: "function",
function: {
name: "get_shoring_ratio",
description: "Get the onshore/offshore staffing ratio for a project. Higher offshore is better (cost-efficient). The threshold is the MINIMUM offshore target. Shows country breakdown and whether the target is met.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
},
required: ["projectId"],
},
},
},
];
// ─── 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;
},
async find_best_project_resource(params: {
projectIdentifier: string;
startDate?: string;
endDate?: string;
durationDays?: number;
minHoursPerDay?: number;
rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
chapter?: string;
roleName?: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const caller = createStaffingCaller(createScopedCallerContext(ctx));
return caller.getBestProjectResourceDetail({
projectId: project.id,
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
...(params.minHoursPerDay !== undefined ? { minHoursPerDay: params.minHoursPerDay } : {}),
...(params.rankingMode ? { rankingMode: params.rankingMode } : {}),
...(params.chapter ? { chapter: params.chapter } : {}),
...(params.roleName ? { roleName: params.roleName } : {}),
});
},
async get_timeline_entries_view(params: {
startDate?: string;
endDate?: string;
durationDays?: number;
resourceIds?: string[];
projectIds?: string[];
clientIds?: string[];
chapters?: string[];
eids?: string[];
countryCodes?: string[];
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getEntriesDetail({ ...params });
},
async get_timeline_holiday_overlays(params: {
startDate?: string;
endDate?: string;
durationDays?: number;
resourceIds?: string[];
projectIds?: string[];
clientIds?: string[];
chapters?: string[];
eids?: string[];
countryCodes?: string[];
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getHolidayOverlayDetail({ ...params });
},
async get_project_timeline_context(params: {
projectIdentifier: string;
startDate?: string;
endDate?: string;
durationDays?: number;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getProjectContextDetail({
projectId: project.id,
...(params.startDate ? { startDate: params.startDate } : {}),
...(params.endDate ? { endDate: params.endDate } : {}),
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
});
},
async preview_project_shift(params: {
projectIdentifier: string;
newStartDate: string;
newEndDate: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getShiftPreviewDetail({
projectId: project.id,
newStartDate: parseIsoDate(params.newStartDate, "newStartDate"),
newEndDate: parseIsoDate(params.newEndDate, "newEndDate"),
});
},
async update_timeline_allocation_inline(params: {
allocationId: string;
hoursPerDay?: number;
startDate?: string;
endDate?: string;
includeSaturday?: boolean;
role?: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
let updated;
try {
updated = await caller.updateAllocationInline({
allocationId: params.allocationId,
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.includeSaturday !== undefined ? { includeSaturday: params.includeSaturday } : {}),
...(params.role !== undefined ? { role: params.role } : {}),
});
} catch (error) {
const mapped = toAssistantTimelineMutationError(error, "updateInline");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Updated timeline allocation ${updated.id}.`,
allocation: {
id: updated.id,
projectId: updated.projectId,
resourceId: updated.resourceId ?? null,
startDate: fmtDate(updated.startDate),
endDate: fmtDate(updated.endDate),
hoursPerDay: updated.hoursPerDay,
role: updated.role ?? null,
status: updated.status,
},
};
},
async apply_timeline_project_shift(params: {
projectIdentifier: string;
newStartDate: string;
newEndDate: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const newStartDate = parseIsoDate(params.newStartDate, "newStartDate");
const newEndDate = parseIsoDate(params.newEndDate, "newEndDate");
const caller = createTimelineCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.applyShift({
projectId: project.id,
newStartDate,
newEndDate,
});
} catch (error) {
const mapped = toAssistantTimelineMutationError(error, "applyShift");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Shifted project ${project.shortCode ?? project.name ?? project.id} to ${fmtDate(newStartDate)} - ${fmtDate(newEndDate)}.`,
project: {
id: result.project.id,
startDate: fmtDate(result.project.startDate),
endDate: fmtDate(result.project.endDate),
},
validation: result.validation,
};
},
async quick_assign_timeline_resource(params: {
resourceIdentifier: string;
projectIdentifier: string;
startDate: string;
endDate: string;
hoursPerDay?: number;
role?: string;
roleId?: string;
status?: AllocationStatus;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceIdentifier),
resolveProjectIdentifier(ctx, params.projectIdentifier),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
let allocation;
try {
allocation = await caller.quickAssign({
resourceId: resource.id,
projectId: project.id,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
...(params.role !== undefined ? { role: params.role } : {}),
...(params.roleId !== undefined ? { roleId: params.roleId } : {}),
...(params.status !== undefined ? { status: params.status } : {}),
});
} catch (error) {
const mapped = toAssistantTimelineMutationError(error, "quickAssign");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Quick-assigned ${resource.displayName} to ${project.name} (${project.shortCode ?? project.id}).`,
allocation: {
id: allocation.id,
projectId: allocation.projectId,
resourceId: allocation.resourceId ?? null,
startDate: fmtDate(toDate(allocation.startDate)),
endDate: fmtDate(toDate(allocation.endDate)),
hoursPerDay: allocation.hoursPerDay,
role: allocation.role ?? null,
status: allocation.status,
},
};
},
async batch_quick_assign_timeline_resources(params: {
assignments: Array<{
resourceIdentifier: string;
projectIdentifier: string;
startDate: string;
endDate: string;
hoursPerDay?: number;
role?: string;
status?: AllocationStatus;
}>;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const resolvedAssignments = await Promise.all(params.assignments.map(async (assignment, index) => {
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, assignment.resourceIdentifier),
resolveProjectIdentifier(ctx, assignment.projectIdentifier),
]);
if ("error" in resource) {
return toAssistantIndexedFieldError(index, "resourceIdentifier", resource.error);
}
if ("error" in project) {
return toAssistantIndexedFieldError(index, "projectIdentifier", project.error);
}
return {
resourceId: resource.id,
projectId: project.id,
startDate: parseIsoDate(assignment.startDate, `assignments[${index}].startDate`),
endDate: parseIsoDate(assignment.endDate, `assignments[${index}].endDate`),
...(assignment.hoursPerDay !== undefined ? { hoursPerDay: assignment.hoursPerDay } : {}),
...(assignment.role !== undefined ? { role: assignment.role } : {}),
...(assignment.status !== undefined ? { status: assignment.status } : {}),
};
}));
const resolutionError = resolvedAssignments.find(
(assignment): assignment is AssistantIndexedFieldErrorResult => isAssistantToolErrorResult(assignment),
);
if (resolutionError) {
return resolutionError;
}
const validAssignments = resolvedAssignments.filter(
(assignment): assignment is BatchQuickAssignmentInput => !isAssistantToolErrorResult(assignment),
);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.batchQuickAssign({
assignments: validAssignments,
});
} catch (error) {
const mapped = toAssistantTimelineMutationError(error, "quickAssign");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Created ${result.count} timeline quick-assignment(s).`,
count: result.count,
};
},
async batch_shift_timeline_allocations(params: {
allocationIds: string[];
daysDelta: number;
mode?: "move" | "resize-start" | "resize-end";
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.batchShiftAllocations({
allocationIds: params.allocationIds,
daysDelta: params.daysDelta,
...(params.mode !== undefined ? { mode: params.mode } : {}),
});
} catch (error) {
const mapped = toAssistantTimelineMutationError(error, "batchShift");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Shifted ${result.count} allocation(s) by ${params.daysDelta} day(s).`,
count: result.count,
};
},
async list_allocations(params: {
resourceId?: string; projectId?: string;
resourceName?: string; projectCode?: string;
status?: string; limit?: number;
}, ctx: ToolContext) {
const caller = createAllocationCaller(createScopedCallerContext(ctx));
const status = params.status && Object.values(AllocationStatus).includes(params.status as AllocationStatus)
? params.status as AllocationStatus
: undefined;
const readModel = await caller.listView({
resourceId: params.resourceId,
projectId: params.projectId,
status,
});
const resourceNameQuery = params.resourceName?.trim().toLowerCase();
const projectCodeQuery = params.projectCode?.trim().toLowerCase();
const limit = Math.min(params.limit ?? 30, 50);
return readModel.assignments
.filter((assignment) => {
if (
resourceNameQuery
&& !assignment.resource?.displayName?.toLowerCase().includes(resourceNameQuery)
) {
return false;
}
if (
projectCodeQuery
&& !assignment.project?.shortCode?.toLowerCase().includes(projectCodeQuery)
) {
return false;
}
return true;
})
.slice(0, limit)
.map((assignment) => ({
id: assignment.id,
resource: assignment.resource?.displayName ?? "Unknown",
resourceEid: assignment.resource?.eid ?? null,
project: assignment.project?.name ?? "Unknown",
projectCode: assignment.project?.shortCode ?? null,
role: assignment.role ?? assignment.roleEntity?.name ?? null,
status: assignment.status,
hoursPerDay: assignment.hoursPerDay,
dailyCost: fmtEur(assignment.dailyCostCents),
start: fmtDate(new Date(assignment.startDate)),
end: fmtDate(new Date(assignment.endDate)),
}));
},
async get_budget_status(params: { projectId: string }, ctx: ToolContext) {
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
const budgetStatus = await caller.getBudgetStatus({ projectId: project.id });
if (budgetStatus.budgetCents <= 0) {
return {
project: budgetStatus.projectName,
code: budgetStatus.projectCode,
budget: "Not set",
note: "No budget defined for this project",
totalAllocations: budgetStatus.totalAllocations,
};
}
return {
project: budgetStatus.projectName,
code: budgetStatus.projectCode,
budget: fmtEur(budgetStatus.budgetCents),
confirmed: fmtEur(budgetStatus.confirmedCents),
proposed: fmtEur(budgetStatus.proposedCents),
allocated: fmtEur(budgetStatus.allocatedCents),
remaining: fmtEur(budgetStatus.remainingCents),
utilization: `${budgetStatus.utilizationPercent.toFixed(1)}%`,
winWeighted: fmtEur(budgetStatus.winProbabilityWeightedCents),
};
},
async get_vacation_balance(params: { resourceId: string; year?: number }, ctx: ToolContext) {
const year = params.year ?? new Date().getFullYear();
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
return caller.getBalanceDetail({ resourceId: resource.id, year });
},
async list_vacations_upcoming(params: {
resourceName?: string; chapter?: string; daysAhead?: number; limit?: number;
}, ctx: ToolContext) {
const daysAhead = params.daysAhead ?? 30;
const limit = Math.min(params.limit ?? 30, 50);
const caller = createVacationCaller(createScopedCallerContext(ctx));
const now = new Date();
const until = new Date(now.getTime() + daysAhead * 24 * 60 * 60 * 1000);
const vacations = await caller.list({
status: "APPROVED",
startDate: now,
endDate: until,
limit,
});
const filtered = vacations
.filter((vacation) => {
if (params.resourceName) {
const resourceName = vacation.resource?.displayName?.toLowerCase() ?? "";
if (!resourceName.includes(params.resourceName.toLowerCase())) {
return false;
}
}
if (params.chapter) {
const chapter = vacation.resource?.chapter?.toLowerCase() ?? "";
if (!chapter.includes(params.chapter.toLowerCase())) {
return false;
}
}
return true;
})
.slice(0, limit);
return filtered.map((v) => ({
resource: v.resource.displayName,
eid: v.resource.eid,
chapter: v.resource.chapter ?? null,
type: v.type,
start: fmtDate(v.startDate),
end: fmtDate(v.endDate),
isHalfDay: v.isHalfDay,
halfDayPart: v.halfDayPart,
}));
},
async list_holidays_by_region(params: {
countryCode: string;
federalState?: string;
metroCity?: string;
year?: number;
periodStart?: string;
periodEnd?: string;
}, ctx: ToolContext) {
const { year, periodStart, periodEnd } = resolveHolidayPeriod(params);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const resolved = await caller.resolveHolidaysDetail({
periodStart,
periodEnd,
countryCode: params.countryCode.trim().toUpperCase(),
...(params.federalState ? { stateCode: params.federalState } : {}),
...(params.metroCity ? { metroCityName: params.metroCity } : {}),
});
return {
locationContext: resolved.locationContext,
year,
periodStart: resolved.periodStart,
periodEnd: resolved.periodEnd,
count: resolved.count,
summary: resolved.summary,
holidays: resolved.holidays,
};
},
async get_resource_holidays(params: {
identifier: string;
year?: number;
periodStart?: string;
periodEnd?: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.identifier);
if ("error" in resource) return resource;
const { year, periodStart, periodEnd } = resolveHolidayPeriod(params);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const resolved = await caller.resolveResourceHolidaysDetail({
resourceId: resource.id,
periodStart,
periodEnd,
});
return {
resource: resolved.resource,
year,
periodStart: resolved.periodStart,
periodEnd: resolved.periodEnd,
count: resolved.count,
summary: resolved.summary,
holidays: resolved.holidays,
};
},
async list_holiday_calendars(params: {
includeInactive?: boolean;
countryCode?: string;
scopeType?: "COUNTRY" | "STATE" | "CITY";
stateCode?: string;
metroCity?: string;
}, ctx: ToolContext) {
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
return caller.listCalendarsDetail(params);
},
async get_holiday_calendar(params: {
identifier: string;
}, ctx: ToolContext) {
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const identifier = params.identifier.trim();
return resolveEntityOrAssistantError(
() => caller.getCalendarByIdentifierDetail({ identifier }),
`Holiday calendar not found: ${identifier}`,
);
},
async preview_resolved_holiday_calendar(params: {
countryId: string;
stateCode?: string;
metroCityId?: string;
year: number;
}, ctx: ToolContext) {
const input = PreviewResolvedHolidaysSchema.parse(params);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
return caller.previewResolvedHolidaysDetail(input);
},
async create_holiday_calendar(params: {
name: string;
scopeType: "COUNTRY" | "STATE" | "CITY";
countryId: string;
stateCode?: string;
metroCityId?: string;
isActive?: boolean;
priority?: number;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
let created;
try {
created = await caller.createCalendar(CreateHolidayCalendarSchema.parse(params));
} catch (error) {
const mapped = toAssistantHolidayCalendarMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
calendar: formatHolidayCalendar(created),
message: `Created holiday calendar: ${created.name}`,
};
},
async update_holiday_calendar(params: {
id: string;
data: {
name?: string;
stateCode?: string | null;
metroCityId?: string | null;
isActive?: boolean;
priority?: number;
};
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateHolidayCalendarSchema.parse(params.data),
};
let updated;
try {
updated = await caller.updateCalendar(input);
} catch (error) {
const mapped = toAssistantHolidayCalendarMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
calendar: formatHolidayCalendar(updated),
message: `Updated holiday calendar: ${updated.name}`,
};
},
async delete_holiday_calendar(params: {
id: string;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
let deleted;
try {
deleted = await caller.deleteCalendar({ id: params.id });
} catch (error) {
const mapped = toAssistantHolidayCalendarNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
message: `Deleted holiday calendar: ${deleted.name}`,
};
},
async create_holiday_calendar_entry(params: {
holidayCalendarId: string;
date: string;
name: string;
isRecurringAnnual?: boolean;
source?: string;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
let created;
try {
created = await caller.createEntry(CreateHolidayCalendarEntrySchema.parse(params));
} catch (error) {
const mapped = toAssistantHolidayEntryMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
entry: formatHolidayCalendarEntry(created),
message: `Created holiday entry: ${created.name}`,
};
},
async update_holiday_calendar_entry(params: {
id: string;
data: {
date?: string;
name?: string;
isRecurringAnnual?: boolean;
source?: string | null;
};
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateHolidayCalendarEntrySchema.parse(params.data),
};
let updated;
try {
updated = await caller.updateEntry(input);
} catch (error) {
const mapped = toAssistantHolidayEntryMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
entry: formatHolidayCalendarEntry(updated),
message: `Updated holiday entry: ${updated.name}`,
};
},
async delete_holiday_calendar_entry(params: {
id: string;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
let deleted;
try {
deleted = await caller.deleteEntry({ id: params.id });
} catch (error) {
const mapped = toAssistantHolidayEntryNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
message: `Deleted holiday entry: ${deleted.name}`,
};
},
async list_roles(_params: Record<string, never>, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
const roles = await caller.list({});
return roles.map((role) => ({
id: role.id,
name: role.name,
color: role.color,
}));
},
async search_by_skill(params: { skill: string }, ctx: ToolContext) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
const matched = await caller.searchBySkills({
rules: [{ skill: params.skill, minProficiency: 1 }],
operator: "OR",
});
return matched.slice(0, 20).map((resource) => ({
id: resource.id,
eid: resource.eid,
name: resource.displayName,
matchedSkill: resource.matchedSkills[0]?.skill ?? null,
level: resource.matchedSkills[0]?.proficiency ?? null,
chapter: resource.chapter ?? null,
}));
},
async get_statistics(_params: Record<string, never>, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getStatisticsDetail();
},
async get_chargeability(params: { resourceId: string; month?: string }, ctx: ToolContext) {
const now = new Date();
const month = params.month ?? `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createResourceCaller(createScopedCallerContext(ctx));
return caller.getChargeabilitySummary({
resourceId: resource.id,
month,
});
},
async get_chargeability_report(params: {
startMonth: string;
endMonth: string;
orgUnitId?: string;
managementLevelGroupId?: string;
countryId?: string;
includeProposed?: boolean;
resourceQuery?: string;
resourceLimit?: number;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createChargeabilityReportCaller(createScopedCallerContext(ctx));
return caller.getDetail({
startMonth: params.startMonth,
endMonth: params.endMonth,
...(params.orgUnitId ? { orgUnitId: params.orgUnitId } : {}),
...(params.managementLevelGroupId ? { managementLevelGroupId: params.managementLevelGroupId } : {}),
...(params.countryId ? { countryId: params.countryId } : {}),
includeProposed: params.includeProposed ?? false,
...(params.resourceQuery ? { resourceQuery: params.resourceQuery } : {}),
...(params.resourceLimit !== undefined ? { resourceLimit: params.resourceLimit } : {}),
});
},
async get_resource_computation_graph(params: {
resourceId: string;
month: string;
domain?: string;
includeLinks?: boolean;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createComputationGraphCaller(createScopedCallerContext(ctx));
return caller.getResourceDataDetail({
resourceId: resource.id,
month: params.month,
...(params.domain ? { domain: params.domain } : {}),
...(params.includeLinks !== undefined ? { includeLinks: params.includeLinks } : {}),
});
},
async get_project_computation_graph(params: {
projectId: string;
domain?: string;
includeLinks?: boolean;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const caller = createComputationGraphCaller(createScopedCallerContext(ctx));
return caller.getProjectDataDetail({
projectId: project.id,
...(params.domain ? { domain: params.domain } : {}),
...(params.includeLinks !== undefined ? { includeLinks: params.includeLinks } : {}),
});
},
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 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 create_allocation(params: {
resourceId: string; projectId: string;
startDate: string; endDate: string;
hoursPerDay: number; role?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceId),
resolveProjectIdentifier(ctx, params.projectId),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
try {
const result = await caller.ensureAssignment({
resourceId: resource.id,
projectId: project.id,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
hoursPerDay: params.hoursPerDay,
...(params.role ? { role: params.role } : {}),
});
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `${result.action === "reactivated" ? "Reactivated" : "Created"} allocation: ${resource.displayName}${project.name} (${project.shortCode}), ${params.hoursPerDay}h/day, ${params.startDate} to ${params.endDate}`,
allocationId: result.assignment.id,
status: result.assignment.status,
};
} catch (error) {
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "Allocation already exists for this resource/project/dates. No new allocation created." };
}
throw error;
}
},
async cancel_allocation(params: {
allocationId?: string;
resourceName?: string; projectCode?: string;
startDate?: string; endDate?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const caller = createAllocationCaller(createScopedCallerContext(ctx));
let resourceId: string | undefined;
let projectId: string | undefined;
if (!params.allocationId && params.resourceName && params.projectCode) {
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceName),
resolveProjectIdentifier(ctx, params.projectCode),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
resourceId = resource.id;
projectId = project.id;
}
const startDate = parseOptionalIsoDate(params.startDate, "startDate");
const endDate = parseOptionalIsoDate(params.endDate, "endDate");
let assignment;
try {
assignment = await caller.resolveAssignment({
...(params.allocationId ? { assignmentId: params.allocationId } : {}),
...(resourceId ? { resourceId } : {}),
...(projectId ? { projectId } : {}),
...(startDate ? { startDate } : {}),
...(endDate ? { endDate } : {}),
selectionMode: "WINDOW",
excludeCancelled: true,
});
} catch (error) {
const mapped = toAssistantAllocationNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
try {
await caller.updateAssignment({
id: assignment.id,
data: UpdateAssignmentSchema.parse({ status: AllocationStatus.CANCELLED }),
});
} catch (error) {
const mapped = toAssistantAllocationNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `Cancelled allocation: ${assignment.resource.displayName}${assignment.project.name} (${assignment.project.shortCode}), ${fmtDate(assignment.startDate)} to ${fmtDate(assignment.endDate)}`,
};
},
async update_allocation_status(params: {
allocationId?: string;
resourceName?: string; projectCode?: string;
startDate?: string;
newStatus: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const validStatuses = ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"];
if (!validStatuses.includes(params.newStatus)) {
return { error: `Invalid status: ${params.newStatus}. Valid: ${validStatuses.join(", ")}` };
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
let resourceId: string | undefined;
let projectId: string | undefined;
if (!params.allocationId && params.resourceName && params.projectCode) {
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceName),
resolveProjectIdentifier(ctx, params.projectCode),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
resourceId = resource.id;
projectId = project.id;
}
const startDate = parseOptionalIsoDate(params.startDate, "startDate");
let assignment;
try {
assignment = await caller.resolveAssignment({
...(params.allocationId ? { assignmentId: params.allocationId } : {}),
...(resourceId ? { resourceId } : {}),
...(projectId ? { projectId } : {}),
...(startDate ? { startDate } : {}),
selectionMode: "EXACT_START",
});
} catch (error) {
const mapped = toAssistantAllocationNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
const oldStatus = assignment.status;
try {
await caller.updateAssignment({
id: assignment.id,
data: UpdateAssignmentSchema.parse({
status: params.newStatus as AllocationStatus,
}),
});
} catch (error) {
const mapped = toAssistantAllocationNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `Updated allocation status: ${assignment.resource.displayName}${assignment.project.name} (${assignment.project.shortCode}), ${fmtDate(assignment.startDate)} to ${fmtDate(assignment.endDate)}: ${oldStatus}${params.newStatus}`,
};
},
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 ──
async create_role(params: {
name: string;
description?: string;
color?: string;
}, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
let role;
try {
role = await caller.create(CreateRoleSchema.parse(params));
} catch (error) {
const mapped = toAssistantRoleMutationError(error, "create");
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["role"], success: true, message: `Created role: ${role.name}`, roleId: role.id, role };
},
async update_role(params: {
id: string;
name?: string;
description?: string;
color?: string;
isActive?: boolean;
}, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
const data = UpdateRoleSchema.parse({
...(params.name !== undefined ? { name: params.name } : {}),
...(params.description !== undefined ? { description: params.description } : {}),
...(params.color !== undefined ? { color: params.color } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
});
if (Object.keys(data).length === 0) return { error: "No fields to update" };
let role;
try {
role = await caller.update({ id: params.id, data });
} catch (error) {
const mapped = toAssistantRoleMutationError(error, "update");
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["role"], success: true, message: `Updated role: ${role.name}`, roleId: role.id, role };
},
async delete_role(params: { id: string }, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
let role;
try {
role = await caller.getById({ id: params.id });
await caller.delete({ id: params.id });
} catch (error) {
const mapped = toAssistantRoleMutationError(error, "delete");
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["role"], success: true, message: `Deleted role: ${role.name}` };
},
// ── CLIENTS ──
async create_client(params: {
name: string;
code?: string;
parentId?: string;
sortOrder?: number;
tags?: string[];
}, ctx: ToolContext) {
const caller = createClientCaller(createScopedCallerContext(ctx));
let client;
try {
client = await caller.create(CreateClientSchema.parse(params));
} catch (error) {
const mapped = toAssistantClientMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["client"], success: true, message: `Created client: ${client.name}`, clientId: client.id, client };
},
async update_client(params: {
id: string;
name?: string;
code?: string | null;
sortOrder?: number;
isActive?: boolean;
parentId?: string | null;
tags?: string[];
}, ctx: ToolContext) {
const caller = createClientCaller(createScopedCallerContext(ctx));
const data = UpdateClientSchema.parse({
...(params.name !== undefined ? { name: params.name } : {}),
...(params.code !== undefined ? { code: params.code } : {}),
...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
...(params.parentId !== undefined ? { parentId: params.parentId } : {}),
...(params.tags !== undefined ? { tags: params.tags } : {}),
});
if (Object.keys(data).length === 0) return { error: "No fields to update" };
let client;
try {
client = await caller.update({ id: params.id, data });
} catch (error) {
const mapped = toAssistantClientMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["client"], success: true, message: `Updated client: ${client.name}`, clientId: client.id, client };
},
async delete_client(params: { id: string }, ctx: ToolContext) {
const caller = createClientCaller(createScopedCallerContext(ctx));
let client;
try {
client = await caller.getById({ id: params.id });
await caller.delete({ id: params.id });
} catch (error) {
const mapped = toAssistantClientMutationError(error, "delete");
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["client"], success: true, message: `Deleted client: ${client.name}` };
},
// ── 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);
},
async create_country(params: {
code: string;
name: string;
dailyWorkingHours?: number;
scheduleRules?: Prisma.JsonValue | null;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
let created;
try {
created = await caller.create(CreateCountrySchema.parse(params));
} catch (error) {
const mapped = toAssistantCountryMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
country: formatCountry(created),
message: `Created country: ${created.name}`,
};
},
async update_country(params: {
id: string;
data: {
code?: string;
name?: string;
dailyWorkingHours?: number;
scheduleRules?: Prisma.JsonValue | null;
isActive?: boolean;
};
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateCountrySchema.parse(params.data),
};
let updated;
try {
updated = await caller.update(input);
} catch (error) {
const mapped = toAssistantCountryMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
country: formatCountry(updated),
message: `Updated country: ${updated.name}`,
};
},
async create_metro_city(params: { countryId: string; name: string }, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
let created;
try {
created = await caller.createCity(CreateMetroCitySchema.parse(params));
} catch (error) {
const mapped = toAssistantMetroCityMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
metroCity: created,
message: `Created metro city: ${created.name}`,
};
},
async update_metro_city(params: { id: string; data: { name?: string } }, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateMetroCitySchema.parse(params.data),
};
let updated;
try {
updated = await caller.updateCity(input);
} catch (error) {
const mapped = toAssistantMetroCityMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
metroCity: updated,
message: `Updated metro city: ${updated.name}`,
};
},
async delete_metro_city(params: { id: string }, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
let deleted;
try {
deleted = await caller.deleteCity({ id: params.id });
} catch (error) {
const mapped = toAssistantMetroCityMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
message: `Deleted metro city: ${deleted.name ?? params.id}`,
};
},
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 ──
async create_org_unit(params: {
name: string;
shortName?: string;
level: number;
parentId?: string;
sortOrder?: number;
}, ctx: ToolContext) {
const caller = createOrgUnitCaller(createScopedCallerContext(ctx));
let ou;
try {
ou = await caller.create(CreateOrgUnitSchema.parse(params));
} catch (error) {
const mapped = toAssistantOrgUnitMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["orgUnit"], success: true, message: `Created org unit: ${ou.name}`, orgUnitId: ou.id, orgUnit: ou };
},
async update_org_unit(params: {
id: string;
name?: string;
shortName?: string | null;
sortOrder?: number;
isActive?: boolean;
parentId?: string | null;
}, ctx: ToolContext) {
const caller = createOrgUnitCaller(createScopedCallerContext(ctx));
const data = UpdateOrgUnitSchema.parse({
...(params.name !== undefined ? { name: params.name } : {}),
...(params.shortName !== undefined ? { shortName: params.shortName } : {}),
...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
...(params.parentId !== undefined ? { parentId: params.parentId } : {}),
});
if (Object.keys(data).length === 0) return { error: "No fields to update" };
let ou;
try {
ou = await caller.update({ id: params.id, data });
} catch (error) {
const mapped = toAssistantOrgUnitMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return { __action: "invalidate", scope: ["orgUnit"], success: true, message: `Updated org unit: ${ou.name}`, orgUnitId: ou.id, orgUnit: ou };
},
// ─── 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;
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,
sortDir: "asc",
limit: Math.min(params.limit ?? 50, 200),
offset: 0,
});
return {
rows: result.rows,
rowCount: result.rows.length,
columns: result.columns,
};
},
async list_comments(params: { entityType: string; 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: string;
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 } : {}),
});
},
async get_system_settings(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.getSystemSettings();
},
async update_system_settings(params: {
aiProvider?: "openai" | "azure";
azureOpenAiEndpoint?: string;
azureOpenAiDeployment?: string;
azureOpenAiApiKey?: string;
azureApiVersion?: string;
aiMaxCompletionTokens?: number;
aiTemperature?: number;
aiSummaryPrompt?: string;
scoreWeights?: {
skillDepth: number;
skillBreadth: number;
costEfficiency: number;
chargeability: number;
experience: number;
};
scoreVisibleRoles?: SystemRole[];
smtpHost?: string;
smtpPort?: number;
smtpUser?: string;
smtpPassword?: string;
smtpFrom?: string;
smtpTls?: boolean;
anonymizationEnabled?: boolean;
anonymizationDomain?: string;
anonymizationSeed?: string;
anonymizationMode?: "global";
azureDalleDeployment?: string;
azureDalleEndpoint?: string;
azureDalleApiKey?: string;
geminiApiKey?: string;
geminiModel?: string;
imageProvider?: "dalle" | "gemini";
vacationDefaultDays?: number;
timelineUndoMaxSteps?: number;
}, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.updateSystemSettings(params);
},
async test_ai_connection(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.testAiConnection();
},
async test_smtp_connection(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.testSmtpConnection();
},
async test_gemini_connection(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.testGeminiConnection();
},
async get_ai_configured(_params: Record<string, never>, ctx: ToolContext) {
const settings = await ctx.db.systemSettings.findUnique({
where: { id: "singleton" },
select: {
aiProvider: true,
azureOpenAiEndpoint: true,
azureOpenAiDeployment: true,
azureOpenAiApiKey: true,
},
});
return { configured: isAiConfigured(settings) };
},
async list_system_role_configs(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSystemRoleConfigCaller(createScopedCallerContext(ctx));
return caller.list();
},
async update_system_role_config(params: {
role: string;
label?: string;
description?: string | null;
color?: string | null;
defaultPermissions?: string[];
}, ctx: ToolContext) {
const caller = createSystemRoleConfigCaller(createScopedCallerContext(ctx));
return caller.update(params);
},
async list_webhooks(_params: Record<string, never>, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
const webhooks = await caller.list();
return sanitizeWebhookList(webhooks);
},
async get_webhook(params: {
id: string;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
let webhook;
try {
webhook = await caller.getById({ id: params.id });
} catch (error) {
const mapped = toAssistantWebhookNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return sanitizeWebhook(webhook);
},
async create_webhook(params: {
name: string;
url: string;
secret?: string;
events: string[];
isActive?: boolean;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
let webhook;
try {
webhook = await caller.create({
name: params.name,
url: params.url,
events: params.events as [string, ...string[]],
...(params.secret !== undefined ? { secret: params.secret } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
});
} catch (error) {
const mapped = toAssistantWebhookMutationError(error, "create");
if (mapped) {
return mapped;
}
throw error;
}
return sanitizeWebhook(webhook);
},
async update_webhook(params: {
id: string;
data: {
name?: string;
url?: string;
secret?: string | null;
events?: string[];
isActive?: boolean;
};
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
let webhook;
try {
webhook = await caller.update({
id: params.id,
data: {
...(params.data.name !== undefined ? { name: params.data.name } : {}),
...(params.data.url !== undefined ? { url: params.data.url } : {}),
...(params.data.secret !== undefined ? { secret: params.data.secret } : {}),
...(params.data.events !== undefined ? { events: params.data.events as [string, ...string[]] } : {}),
...(params.data.isActive !== undefined ? { isActive: params.data.isActive } : {}),
},
});
} catch (error) {
const mapped = toAssistantWebhookMutationError(error, "update");
if (mapped) {
return mapped;
}
throw error;
}
return sanitizeWebhook(webhook);
},
async delete_webhook(params: {
id: string;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
try {
await caller.delete({ id: params.id });
} catch (error) {
const mapped = toAssistantWebhookNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return { ok: true, id: params.id };
},
async test_webhook(params: {
id: string;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
try {
return await caller.test({ id: params.id });
} catch (error) {
const mapped = toAssistantWebhookNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async list_audit_log_entries(params: {
entityType?: string;
entityId?: string;
userId?: string;
action?: string;
source?: string;
startDate?: string;
endDate?: string;
search?: string;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
const result = await caller.listDetail({
...(params.entityType ? { entityType: params.entityType } : {}),
...(params.entityId ? { entityId: params.entityId } : {}),
...(params.userId ? { userId: params.userId } : {}),
...(params.action ? { action: params.action } : {}),
...(params.source ? { source: params.source } : {}),
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.search ? { search: params.search } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 100) } : {}),
});
return {
filters: {
entityType: params.entityType ?? null,
entityId: params.entityId ?? null,
userId: params.userId ?? null,
action: params.action ?? null,
source: params.source ?? null,
startDate: params.startDate ?? null,
endDate: params.endDate ?? null,
search: params.search ?? null,
},
itemCount: result.items.length,
nextCursor: result.nextCursor ?? null,
items: result.items,
};
},
async get_audit_log_entry(params: {
id: string;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
try {
return await caller.getByIdDetail({ id: params.id });
} catch (error) {
const mapped = toAssistantAuditLogEntryNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async get_audit_log_timeline(params: {
startDate?: string;
endDate?: string;
limit?: number;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getTimelineDetail({
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 500) } : {}),
});
},
async get_audit_activity_summary(params: {
startDate?: string;
endDate?: string;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getActivitySummary({
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
});
},
async get_shoring_ratio(params: { projectId: string }, ctx: ToolContext) {
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) return project;
const caller = createProjectCaller(createScopedCallerContext(ctx));
const result = await caller.getShoringRatio({ projectId: project.id });
if (result.totalHours <= 0) {
return `Project "${project.name}" (${project.shortCode}): No active assignments — shoring ratio not available.`;
}
const countryParts = Object.entries(result.byCountry)
.sort((a, b) => b[1].pct - a[1].pct)
.map(([code, info]) => `${code} ${info.pct}% (${info.resourceCount} people)`)
.join(", ");
const status = result.offshoreRatio >= result.threshold
? `Target met (>=${result.threshold}% offshore)`
: result.offshoreRatio >= result.threshold - 10
? `Close to target (${result.threshold}% offshore needed)`
: `Below target — only ${result.offshoreRatio}% offshore, need ${result.threshold}%`;
return `Project "${project.name}" (${project.shortCode}): ${result.onshoreRatio}% onshore (${result.onshoreCountryCode}), ${result.offshoreRatio}% offshore. ${status}. Breakdown: ${countryParts}.${result.unknownCount > 0 ? ` (${result.unknownCount} resource(s) without country)` : ""}`;
},
};
// ─── 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 };
}
}