Files
twenty/packages/twenty-server/src/engine/metadata-modules/ai/ai.exception.ts
T
nitin e1828b6f41 [AI] Add thread actions, filters, and archive support (#20068)
## PR Description

### Summary
- Add AI chat thread actions: rename, archive (soft-delete via
`deletedAt`), and hard-delete with confirmation.
- Add chat thread filtering by status (active/archived/all), group-by
mode, and last activity.
- Rework drawer/side-panel thread lists to share thread sections, item
menus, archive icons, and empty-state behavior.
- Extend server chat thread model/API with `deletedAt`, mutations,
broadcasts, and archive-aware stream guards.

### Decisions
- Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a
separate action on archived threads that hard-deletes the row. Aligns
with Twenty's soft-delete convention (Felix's suggestion).
- `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read,
not stored. List query does inline aggregation for sort; `@ResolveField`
covers single-thread / mutation paths so the schema contract is honest
everywhere. Matches `timeline-messaging.service.ts` precedent and the
existing `totalInputCredits` / `totalOutputCredits` `@ResolveField`
pattern in the same resolver.
- Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a
custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats
threads as a flat collection and filters/sorts client-side, so cursor
pagination was performative.
- Sending in an archived chat unarchives it optimistically on the client
and authoritatively on the server.
- Grouping and last-activity filtering use `lastMessageAt ?? updatedAt`
so archive/rename don't bump threads in the list.
- Kept metadata-store core API unchanged; AI chat uses the same local
cast pattern already used by other metadata-store partial updates.


https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87
2026-04-30 15:42:10 +00:00

65 lines
2.4 KiB
TypeScript

import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum AiExceptionCode {
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
AGENT_ALREADY_EXISTS = 'AGENT_ALREADY_EXISTS',
AGENT_IS_STANDARD = 'AGENT_IS_STANDARD',
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
INVALID_AGENT_INPUT = 'INVALID_AGENT_INPUT',
THREAD_NOT_FOUND = 'THREAD_NOT_FOUND',
INVALID_CHAT_THREAD_TITLE = 'INVALID_CHAT_THREAD_TITLE',
MESSAGE_NOT_FOUND = 'MESSAGE_NOT_FOUND',
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS = 'ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS',
}
const getAiExceptionUserFriendlyMessage = (code: AiExceptionCode) => {
switch (code) {
case AiExceptionCode.AGENT_NOT_FOUND:
return msg`Agent not found.`;
case AiExceptionCode.AGENT_ALREADY_EXISTS:
return msg`An agent with this name already exists.`;
case AiExceptionCode.AGENT_IS_STANDARD:
return msg`Standard agents cannot be modified.`;
case AiExceptionCode.AGENT_EXECUTION_FAILED:
return msg`Agent execution failed.`;
case AiExceptionCode.INVALID_AGENT_INPUT:
return msg`Invalid agent input.`;
case AiExceptionCode.THREAD_NOT_FOUND:
return msg`Chat thread not found.`;
case AiExceptionCode.INVALID_CHAT_THREAD_TITLE:
return msg`Chat thread title cannot be empty.`;
case AiExceptionCode.MESSAGE_NOT_FOUND:
return msg`Chat message not found.`;
case AiExceptionCode.API_KEY_NOT_CONFIGURED:
return msg`API key is not configured.`;
case AiExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
return msg`User workspace not found.`;
case AiExceptionCode.ROLE_NOT_FOUND:
return msg`Role not found.`;
case AiExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
return msg`This role cannot be assigned to agents.`;
default:
assertUnreachable(code);
}
};
export class AiException extends CustomException<AiExceptionCode> {
constructor(
message: string,
code: AiExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ?? getAiExceptionUserFriendlyMessage(code),
});
}
}