godcrm/utils/groupChatMessageItems.ts
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

210 lines
6.6 KiB
TypeScript

/**
* groupChatMessageItems.ts
* ADR-092: Unified Turn Grouping for ChatConversationView (Telegram-style chat)
*
* Groups a flat ChatMessageItem[] array into ChatMessageItemTurn[] for rendering:
* - Consecutive user messages from same sender → single human turn
* - Agent thinking + tool_call + tool_result + final text → single agent turn
* - Date separators preserved as separate items
*/
import type { ChatMessageItem } from '../components/ChatConversationView';
// ---------------------------------------------------------------------------
// Turn — the unit rendered by ChatConversationView
// ---------------------------------------------------------------------------
export interface ChatMessageItemTurn {
/** Stable key for React */
id: string;
/** 'human' for user messages, 'agent' for assistant/tool/thinking groups */
turnType: 'human' | 'agent';
/** Whether this is the current user's message */
isOwn: boolean;
/** Sender info (from first message in group) */
sender?: ChatMessageItem['sender'];
/** Grouped messages — 1 for simple, N for agent steps */
messages: ChatMessageItem[];
/** Whether this is the first message in a consecutive group from same sender */
isFirstInGroup: boolean;
/** Whether this is the last message in a consecutive group from same sender */
isLastInGroup: boolean;
/** Whether agent is still processing */
isProcessing: boolean;
}
/**
* Groups flat ChatMessageItem[] into ChatMessageItemTurn[] suitable for
* the Telegram-style ChatConversationView rendering.
*
* Agent messages with contentType (thinking, tool_call, tool_result)
* are grouped together into a single agent turn with the final text.
* Consecutive user messages from the same sender are grouped into a
* single human turn.
*/
export function groupChatMessageItems(
messages: ChatMessageItem[],
opts: {
chatType: 'agent' | 'direct' | 'group' | 'task';
currentUserId?: number;
isAgentProcessing?: boolean;
},
): ChatMessageItemTurn[] {
const { chatType, currentUserId, isAgentProcessing = false } = opts;
const turns: ChatMessageItemTurn[] = [];
let currentAgentMessages: ChatMessageItem[] = [];
let currentHumanMessages: ChatMessageItem[] = [];
let currentHumanSenderId: number | string | undefined;
/** Buffer for consecutive standalone agent text messages (no tool steps) */
let currentAgentTextMessages: ChatMessageItem[] = [];
let turnCounter = 0;
const isOwnMessage = (msg: ChatMessageItem): boolean => {
if (chatType === 'agent') {
return msg.role === 'user';
}
return !!(currentUserId && msg.sender?.id === currentUserId);
};
/** Flush accumulated agent steps into a turn */
const flushAgentSteps = (processing = false) => {
if (currentAgentMessages.length === 0) return;
const firstMsg = currentAgentMessages[0];
turns.push({
id: `aturn_${firstMsg.id || turnCounter}`,
turnType: 'agent',
isOwn: false,
sender: firstMsg.sender,
messages: [...currentAgentMessages],
isFirstInGroup: true,
isLastInGroup: true,
isProcessing: processing,
});
currentAgentMessages = [];
turnCounter++;
};
/** Flush accumulated consecutive standalone agent text messages into a single turn */
const flushAgentTextMessages = () => {
if (currentAgentTextMessages.length === 0) return;
const firstMsg = currentAgentTextMessages[0];
turns.push({
id: `aturn_${firstMsg.id || turnCounter}`,
turnType: 'agent',
isOwn: false,
sender: firstMsg.sender,
messages: [...currentAgentTextMessages],
isFirstInGroup: true,
isLastInGroup: true,
isProcessing: false,
});
currentAgentTextMessages = [];
turnCounter++;
};
/** Flush accumulated consecutive human messages into a single turn */
const flushHumanMessages = () => {
if (currentHumanMessages.length === 0) return;
const firstMsg = currentHumanMessages[0];
turns.push({
id: `hturn_${firstMsg.id || turnCounter}`,
turnType: 'human',
isOwn: isOwnMessage(firstMsg),
sender: firstMsg.sender,
messages: [...currentHumanMessages],
isFirstInGroup: true,
isLastInGroup: true,
isProcessing: false,
});
currentHumanMessages = [];
currentHumanSenderId = undefined;
turnCounter++;
};
for (const msg of messages) {
const ct = msg.contentType;
// ── User messages: group consecutive from same sender ──
if (msg.role === 'user') {
flushAgentSteps();
flushAgentTextMessages();
const senderId = msg.sender?.id ?? '__me__';
const isSameSender =
currentHumanMessages.length > 0 &&
(senderId === currentHumanSenderId ||
senderId === '__me__' ||
currentHumanSenderId === '__me__');
if (isSameSender) {
currentHumanMessages.push(msg);
if (currentHumanSenderId === '__me__' && senderId !== '__me__') {
currentHumanSenderId = senderId;
}
continue;
}
// Different sender or first message — flush previous
flushHumanMessages();
currentHumanSenderId = senderId;
currentHumanMessages.push(msg);
continue;
}
// ── Any non-user message flushes human batch ──
flushHumanMessages();
// Agent step types: thinking, tool_call, tool_result
if (ct === 'thinking' || ct === 'tool_call' || ct === 'tool_result') {
flushAgentTextMessages(); // steps start a new agent turn
currentAgentMessages.push(msg);
continue;
}
// 'text' from assistant after tool steps → attach to agent steps as final response
if (msg.role === 'assistant' && ct === 'text' && currentAgentMessages.length > 0) {
currentAgentMessages.push(msg);
flushAgentSteps();
continue;
}
// Regular message (no contentType, or plain assistant text without prior steps)
flushAgentSteps();
// Check if it's an agent-type message
const isAgent = msg.role === 'assistant' || msg.role === 'tool' || msg.role === 'system';
if (isAgent) {
// Standalone agent text — buffer for merging consecutive agent messages
currentAgentTextMessages.push(msg);
} else {
// Standalone human message
flushAgentTextMessages();
turns.push({
id: `hturn_${msg.id || turnCounter}`,
turnType: 'human',
isOwn: isOwnMessage(msg),
sender: msg.sender,
messages: [msg],
isFirstInGroup: true,
isLastInGroup: true,
isProcessing: false,
});
turnCounter++;
}
}
// Flush remaining
flushHumanMessages();
flushAgentTextMessages();
flushAgentSteps(isAgentProcessing);
return turns;
}