diff --git a/docs/architecture-hardening-backlog.md b/docs/architecture-hardening-backlog.md index 9e16cfd..64302f5 100644 --- a/docs/architecture-hardening-backlog.md +++ b/docs/architecture-hardening-backlog.md @@ -33,12 +33,13 @@ - the adjacent management-level, utilization, calculation-rule, effort-rule, and experience-multiplier read helpers now live in their own domain module, further shrinking the monolithic assistant router without changing the assistant contract - the remaining assistant user-admin helper cluster now lives in its own domain module, covering admin listing, user lifecycle mutations, permission overrides, resource linking, and MFA overrides without changing the assistant contract - the authenticated user self-service assistant helpers now live in their own domain module, covering assignable users, dashboard preferences, favorites, column preferences, and MFA self-service without changing the assistant contract +- the embedded notification, task, reminder, and broadcast assistant helpers now live in their own domain module, keeping the collaboration workflow wiring out of the monolithic router without changing the assistant contract ## Next Up Pin the next structural cleanup on the API side: continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract. -The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the embedded notification/task helpers, or the remaining estimate and project admin helper clusters that are still in the monolithic router. +The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the remaining estimate helpers or the project admin/helper clusters that are still in the monolithic router. ## Remaining Major Themes diff --git a/packages/api/src/router/assistant-tools.ts b/packages/api/src/router/assistant-tools.ts index 03fd758..e692391 100644 --- a/packages/api/src/router/assistant-tools.ts +++ b/packages/api/src/router/assistant-tools.ts @@ -103,6 +103,11 @@ import { createUserAdminExecutors, userAdminToolDefinitions, } from "./assistant-tools/user-admin.js"; +import { + createNotificationsTasksExecutors, + notificationInboxToolDefinitions, + notificationTaskToolDefinitions, +} from "./assistant-tools/notifications-tasks.js"; import type { ToolContext, ToolDef, ToolExecutor } from "./assistant-tools/shared.js"; import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../lib/comment-entity-registry.js"; @@ -2791,69 +2796,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [ ...configReadmodelToolDefinitions, ...userAdminToolDefinitions, ...userSelfServiceToolDefinitions, - { - 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"], - }, - }, - }, + ...notificationInboxToolDefinitions, // ── DASHBOARD DETAIL ── { @@ -2924,240 +2867,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [ }, // ── 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"], - }, - }, - }, + ...notificationTaskToolDefinitions, // ── INSIGHTS & ANOMALIES ── { @@ -5188,96 +4898,21 @@ const executors = { createScopedCallerContext, toAssistantTotpEnableError, }), - - 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, 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}".`, - }; - }, + ...createNotificationsTasksExecutors({ + createNotificationCaller, + createScopedCallerContext, + parseDateTime, + parseOptionalDateTime, + toAssistantTaskNotFoundError, + toAssistantTaskActionError, + toAssistantTaskAssignmentError, + toAssistantBroadcastNotFoundError, + toAssistantReminderNotFoundError, + toAssistantNotificationReadError, + toAssistantNotificationDeletionError, + toAssistantReminderCreationError, + toAssistantNotificationCreationError, + }), // ── DASHBOARD DETAIL ── @@ -5346,361 +4981,6 @@ const executors = { 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, 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, ctx: ToolContext) { diff --git a/packages/api/src/router/assistant-tools/notifications-tasks.ts b/packages/api/src/router/assistant-tools/notifications-tasks.ts new file mode 100644 index 0000000..39ff749 --- /dev/null +++ b/packages/api/src/router/assistant-tools/notifications-tasks.ts @@ -0,0 +1,914 @@ +import type { TRPCContext } from "../../trpc.js"; +import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js"; + +type AssistantToolErrorResult = { error: string }; + +type NotificationCategory = "NOTIFICATION" | "REMINDER" | "TASK" | "APPROVAL"; +type NotificationPriority = "LOW" | "NORMAL" | "HIGH" | "URGENT"; +type TaskStatus = "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED"; +type DeliveryChannel = "in_app" | "email" | "both"; +type ReminderRecurrence = "daily" | "weekly" | "monthly"; +type BroadcastTargetType = "user" | "role" | "project" | "orgUnit" | "all"; + +type NotificationRecord = { + id: string; +}; + +type TaskRecord = { + id: string; + taskStatus?: TaskStatus | null; +}; + +type ReminderRecord = { + id: string; +}; + +type BroadcastRecord = { + id: string; + recipientCount?: number | null; +}; + +type NotificationsTasksDeps = { + createNotificationCaller: (ctx: TRPCContext) => { + list: (params: { unreadOnly?: boolean; limit?: number }) => Promise; + markRead: (params: { id?: string }) => Promise; + unreadCount: () => Promise; + create: (params: { + userId: string; + type: string; + title: string; + body?: string; + entityId?: string; + entityType?: string; + category?: NotificationCategory; + priority?: NotificationPriority; + link?: string; + taskStatus?: TaskStatus; + taskAction?: string; + assigneeId?: string; + dueDate?: Date; + channel?: DeliveryChannel; + senderId?: string; + }) => Promise; + listTasks: (params: { + status?: TaskStatus; + includeAssigned?: boolean; + limit?: number; + }) => Promise; + taskCounts: () => Promise; + getTaskDetail: (params: { id: string }) => Promise; + updateTaskStatus: (params: { id: string; status: TaskStatus }) => Promise; + executeTaskAction: (params: { id: string }) => Promise<{ + task: unknown; + actionResult: { message: string }; + }>; + createReminder: (params: { + title: string; + remindAt: Date; + body?: string; + recurrence?: ReminderRecurrence; + entityId?: string; + entityType?: string; + link?: string; + }) => Promise; + listReminders: (params: { limit?: number }) => Promise; + updateReminder: (params: { + id: string; + title?: string; + body?: string; + remindAt?: Date; + recurrence?: ReminderRecurrence | null; + }) => Promise; + deleteReminder: (params: { id: string }) => Promise; + createTask: (params: { + userId: string; + title: string; + body?: string; + priority?: NotificationPriority; + dueDate?: Date; + taskAction?: string; + entityId?: string; + entityType?: string; + link?: string; + channel?: DeliveryChannel; + }) => Promise; + assignTask: (params: { id: string; assigneeId: string }) => Promise; + createBroadcast: (params: { + title: string; + targetType: BroadcastTargetType; + body?: string; + link?: string; + category?: NotificationCategory; + priority?: NotificationPriority; + channel?: DeliveryChannel; + targetValue?: string; + scheduledAt?: Date; + taskAction?: string; + dueDate?: Date; + }) => Promise; + listBroadcasts: (params: { limit?: number }) => Promise; + getBroadcastById: (params: { id: string }) => Promise; + delete: (params: { id: string }) => Promise; + }; + createScopedCallerContext: (ctx: ToolContext) => TRPCContext; + parseDateTime: (value: string, fieldName: string) => Date; + parseOptionalDateTime: (value: string | undefined, fieldName: string) => Date | undefined; + toAssistantTaskNotFoundError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantTaskActionError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantTaskAssignmentError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantBroadcastNotFoundError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantReminderNotFoundError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantNotificationReadError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantNotificationDeletionError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantReminderCreationError: ( + error: unknown, + ) => AssistantToolErrorResult | null; + toAssistantNotificationCreationError: ( + error: unknown, + context: "notification" | "task" | "broadcast", + ) => AssistantToolErrorResult | null; +}; + +export const notificationInboxToolDefinitions: ToolDef[] = [ + { + 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"], + }, + }, + }, +]; + +export const notificationTaskToolDefinitions: ToolDef[] = [ + { + 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 task 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"], + }, + }, + }, +]; + +export function createNotificationsTasksExecutors( + deps: NotificationsTasksDeps, +): Record { + return { + async list_notifications(params: { unreadOnly?: boolean; limit?: number }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.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 = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + try { + await caller.markRead({ + ...(params.notificationId !== undefined ? { id: params.notificationId } : {}), + }); + } catch (error) { + const mapped = deps.toAssistantNotificationReadError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + scope: ["notification"], + success: true, + message: params.notificationId ? "Notification marked as read." : "All unread notifications marked as read.", + }; + }, + + async get_unread_notification_count(_params: Record, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.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?: NotificationCategory; + priority?: NotificationPriority; + link?: string; + taskStatus?: TaskStatus; + taskAction?: string; + assigneeId?: string; + dueDate?: string; + channel?: DeliveryChannel; + senderId?: string; + }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + const dueDate = deps.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 = deps.toAssistantNotificationCreationError(error, "notification"); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + scope: ["notification"], + success: true, + notification, + notificationId: notification?.id ?? null, + message: `Created notification "${params.title}".`, + }; + }, + + async list_tasks(params: { + status?: TaskStatus; + includeAssigned?: boolean; + limit?: number; + }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.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, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + return caller.taskCounts(); + }, + + async get_task_detail(params: { taskId: string }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + try { + return await caller.getTaskDetail({ id: params.taskId }); + } catch (error) { + const mapped = deps.toAssistantTaskNotFoundError(error); + if (mapped) { + return mapped; + } + throw error; + } + }, + + async update_task_status(params: { taskId: string; status: string }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + + let task; + try { + task = await caller.updateTaskStatus({ + id: params.taskId, + status: params.status as TaskStatus, + }); + } catch (error) { + const mapped = deps.toAssistantTaskNotFoundError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + 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 = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + + let result; + try { + result = await caller.executeTaskAction({ id: params.taskId }); + } catch (error) { + const mapped = deps.toAssistantTaskActionError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + 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 = deps.createNotificationCaller(deps.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 = deps.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 ReminderRecurrence } : {}), + ...(params.entityId !== undefined ? { entityId: params.entityId } : {}), + ...(params.entityType !== undefined ? { entityType: params.entityType } : {}), + ...(params.link !== undefined ? { link: params.link } : {}), + }); + } catch (error) { + const mapped = deps.toAssistantReminderCreationError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + scope: ["notification"], + success: true, + reminder, + reminderId: reminder.id, + message: `Reminder "${params.title}" created.`, + }; + }, + + async list_reminders(params: { limit?: number }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.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?: ReminderRecurrence | null; + }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + const remindAt = deps.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 = deps.toAssistantReminderNotFoundError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + scope: ["notification"], + success: true, + reminder, + reminderId: reminder.id, + message: `Updated reminder ${params.id}.`, + }; + }, + + async delete_reminder(params: { id: string }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + try { + await caller.deleteReminder({ id: params.id }); + } catch (error) { + const mapped = deps.toAssistantReminderNotFoundError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + 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?: DeliveryChannel; + }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + const dueDate = deps.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 NotificationPriority } : {}), + ...(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 = deps.toAssistantNotificationCreationError(error, "task"); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + 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 = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + + let task; + try { + task = await caller.assignTask(params); + } catch (error) { + const mapped = deps.toAssistantTaskAssignmentError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + 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?: NotificationCategory; + priority?: string; + channel?: string; + link?: string; + scheduledAt?: string; + taskAction?: string; + dueDate?: string; + }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + const scheduledAt = deps.parseOptionalDateTime(params.scheduledAt, "scheduledAt"); + const dueDate = deps.parseOptionalDateTime(params.dueDate, "dueDate"); + + let broadcast; + try { + broadcast = await caller.createBroadcast({ + title: params.title, + targetType: params.targetType as BroadcastTargetType, + ...(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 NotificationPriority } : {}), + ...(params.channel !== undefined ? { channel: params.channel as DeliveryChannel } : {}), + ...(params.targetValue !== undefined ? { targetValue: params.targetValue } : {}), + ...(scheduledAt ? { scheduledAt } : {}), + ...(params.taskAction !== undefined ? { taskAction: params.taskAction } : {}), + ...(dueDate ? { dueDate } : {}), + }); + } catch (error) { + const mapped = deps.toAssistantNotificationCreationError(error, "broadcast"); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + 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 = deps.createNotificationCaller(deps.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 = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + try { + return await caller.getBroadcastById({ id: params.id }); + } catch (error) { + const mapped = deps.toAssistantBroadcastNotFoundError(error); + if (mapped) { + return mapped; + } + throw error; + } + }, + + async delete_notification(params: { id: string }, ctx: ToolContext) { + const caller = deps.createNotificationCaller(deps.createScopedCallerContext(ctx)); + try { + await caller.delete({ id: params.id }); + } catch (error) { + const mapped = deps.toAssistantNotificationDeletionError(error); + if (mapped) { + return mapped; + } + throw error; + } + + return { + __action: "invalidate" as const, + scope: ["notification"], + success: true, + id: params.id, + message: `Deleted notification ${params.id}.`, + }; + }, + }; +}