import type { AgentActivityMessage, AgentActivityNeedsAttentionItem } from "@tutti-os/agent-activity-core"; import type { AgentConversationPromptVM } from "../shared/agentConversation/contracts/agentConversationVM"; import { extractAgentMcpToolTarget } from "../shared/agentMcpToolTarget"; import type { WorkspaceAgentActivityStatus } from "../shared/workspaceAgentActivityListViewModel"; export type WorkspaceAgentMessageCenterDigestPrimaryKind = | "input-required" | "error" | "outcome" | "progress" | "summary"; export interface WorkspaceAgentMessageCenterDigestPrimary { kind: WorkspaceAgentMessageCenterDigestPrimaryKind; summary: string; occurredAtUnixMs: number | null; } export interface WorkspaceAgentMessageCenterDigest { primary: WorkspaceAgentMessageCenterDigestPrimary; } export interface WorkspaceAgentMessageCenterDigestAgentSummary { summary: string; occurredAtUnixMs: number; } export interface BuildWorkspaceAgentMessageCenterDigestInput { fallbackTitle: string; latestAgentMessage: WorkspaceAgentMessageCenterDigestAgentSummary | null; needsAttention: AgentActivityNeedsAttentionItem | null; pendingPrompt: AgentConversationPromptVM | null; status: WorkspaceAgentActivityStatus; } interface DigestAgentMessageSummaryCacheEntry { kind: string; payload: AgentActivityMessage["payload"]; role: string; status: string | null | undefined; summary: string; version: number; } const digestAgentMessageSummaryByMessage = new WeakMap< AgentActivityMessage, DigestAgentMessageSummaryCacheEntry >(); export function buildWorkspaceAgentMessageCenterDigest( input: BuildWorkspaceAgentMessageCenterDigestInput ): WorkspaceAgentMessageCenterDigest { const { fallbackTitle, latestAgentMessage, needsAttention, pendingPrompt, status } = input; const fallbackSummary = firstNonEmptyString( latestAgentMessage?.summary ?? null, needsAttention?.summary ?? null, pendingPromptSummary(pendingPrompt), fallbackTitle ); if (pendingPrompt || needsAttention) { return { primary: { kind: "input-required", summary: firstNonEmptyString( pendingPromptSummary(pendingPrompt), needsAttention?.summary ?? null, fallbackSummary ), occurredAtUnixMs: needsAttention?.occurredAtUnixMs ?? promptTimeUnixMs(pendingPrompt) ?? latestAgentMessage?.occurredAtUnixMs ?? null } }; } if (status === "failed") { return { primary: { kind: "error", summary: fallbackSummary, occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null } }; } if (status === "completed" || status === "canceled" || status === "idle") { return { primary: { kind: "outcome", summary: fallbackSummary, occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null } }; } if (status === "working") { return { primary: { kind: "progress", summary: fallbackSummary, occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null } }; } return { primary: { kind: "summary", summary: fallbackSummary, occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null } }; } export function resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary( message: AgentActivityMessage ): string { const cached = digestAgentMessageSummaryByMessage.get(message); if ( cached && cached.role === message.role && cached.kind === message.kind && cached.status === message.status && cached.payload === message.payload && cached.version === message.version ) { return cached.summary; } if ( !isAgentMessageRole(message.role) || isReasoningMessageKind(message.kind) ) { digestAgentMessageSummaryByMessage.set(message, { kind: message.kind, payload: message.payload, role: message.role, status: message.status, summary: "", version: message.version }); return ""; } const summary = meaningfulMessageSummary(message); digestAgentMessageSummaryByMessage.set(message, { kind: message.kind, payload: message.payload, role: message.role, status: message.status, summary, version: message.version }); return summary; } function pendingPromptSummary( prompt: AgentConversationPromptVM | null ): string | null { if (!prompt) { return null; } switch (prompt.kind) { case "ask-user": return firstNonEmptyString( prompt.title, prompt.questions[0]?.question ?? null ); case "approval": case "exit-plan": case "plan-implementation": return prompt.title; } } function promptTimeUnixMs( prompt: AgentConversationPromptVM | null ): number | null { if (!prompt || !("occurredAtUnixMs" in prompt)) { return null; } return positiveNumber(prompt.occurredAtUnixMs); } function isAgentMessageRole(role: string): boolean { const normalized = role.trim().toLowerCase(); return normalized === "assistant" || normalized === "agent"; } /** * Mirrors the same-named guard in workspaceAgentMessageCenterModel.ts: a * reasoning/thinking message carries an assistant-like `role` but * `kind: "reasoning"` and must never be summarized as a normal agent reply. */ function isReasoningMessageKind(kind: string): boolean { return kind.trim().toLowerCase() === "reasoning"; } type MessageSummarySource = | "payload.summary" | "payload.displayPrompt" | "payload.text" | "payload.content" | "payload.message" | "payload.body" | "tool.error" | "tool.output" | "tool.input" | "payload.title"; interface MessageSummaryCandidate { source: MessageSummarySource; summary: string; } function meaningfulMessageSummary(message: AgentActivityMessage): string { const payload = recordValue(message.payload); const isToolMessage = isToolLikeMessage(message, payload); const candidates = messageSummaryCandidates(payload, isToolMessage); const structuralLabelTokens = isToolMessage ? structuralToolLabelTokens(message, payload) : null; const hasToolDigestSignal = isToolMessage && candidates.some(isToolDigestSignalCandidate); for (const candidate of candidates) { if ( !isGenericToolLabelSummary(candidate, { hasToolDigestSignal, isToolMessage, structuralLabelTokens }) ) { return decorateMessageSummary(message, candidate, isToolMessage); } } return ""; } function messageSummaryCandidates( payload: Record, isToolMessage: boolean ): MessageSummaryCandidate[] { const explicitCandidates = [ summaryCandidate("payload.summary", payload.summary), summaryCandidate("payload.displayPrompt", payload.displayPrompt), summaryCandidate("payload.text", payload.text), summaryCandidate("payload.message", payload.message), summaryCandidate("payload.body", payload.body) ]; if (!isToolMessage) { return [ ...explicitCandidates, summaryCandidate( "payload.content", textFromContentValue(payload.content) ), summaryCandidate("payload.title", payload.title) ].filter((candidate): candidate is MessageSummaryCandidate => Boolean(candidate) ); } return [ ...explicitCandidates, toolErrorSummaryCandidate(payload), toolOutputSummaryCandidate(payload), toolInputSummaryCandidate(payload), summaryCandidate("payload.title", payload.title) ].filter((candidate): candidate is MessageSummaryCandidate => Boolean(candidate) ); } function summaryCandidate( source: MessageSummarySource, value: unknown ): MessageSummaryCandidate | null { const summary = normalizeSummaryText(stringValue(value)); return summary ? { source, summary } : null; } function toolErrorSummaryCandidate( payload: Record ): MessageSummaryCandidate | null { const error = recordValue(payload.error); return summaryCandidate( "tool.error", firstNonEmptyString( stringValue(payload.error), stringValue(error.summary), stringValue(error.message), stringValue(error.text), stringValue(error.stdout), stringValue(error.stderr), stringValue(error.status) ) ); } function toolOutputSummaryCandidate( payload: Record ): MessageSummaryCandidate | null { const output = recordValue(payload.output); return summaryCandidate( "tool.output", firstNonEmptyString( stringValue(output.summary), stringValue(output.message), stringValue(output.text), stringValue(output.stdout), stringValue(output.stderr), stringValue(output.status) ) ); } function toolInputSummaryCandidate( payload: Record ): MessageSummaryCandidate | null { const rootInput = recordValue(payload.input); const nestedInput = recordValue(recordValue(rootInput.toolCall).input); const input = Object.keys(nestedInput).length > 0 ? nestedInput : rootInput; const action = recordValue(input.action); return summaryCandidate( "tool.input", firstNonEmptyString( stringValue(input.summary), stringValue(input.command), stringValue(input.cmd), stringValue(input.path), stringValue(input.file_path), stringValue(input.fileName), stringValue(input.filename), stringValue(input.query), stringArrayFirstValue(input.search_query), stringArrayFirstValue(input.searchQuery), stringValue(input.url), stringValue(input.pattern), stringValue(input.glob), stringValue(input.regex), stringValue(input.prompt), stringValue(action.query), stringValue(action.url) ) ); } function isGenericToolLabelSummary( candidate: MessageSummaryCandidate, context: { hasToolDigestSignal: boolean; isToolMessage: boolean; structuralLabelTokens: Set | null; } ): boolean { if (!context.isToolMessage) { return false; } const normalizedSummary = normalizeToken(candidate.summary); if (!normalizedSummary) { return true; } if (context.structuralLabelTokens?.has(normalizedSummary)) { return true; } return ( candidate.source === "payload.title" && !context.hasToolDigestSignal && looksLikeBareLabel(candidate.summary) ); } function decorateMessageSummary( message: AgentActivityMessage, candidate: MessageSummaryCandidate, isToolMessage: boolean ): string { if (!isToolMessage) { return candidate.summary; } const mcpTarget = extractAgentMcpToolTarget({ payload: message.payload }); if (!mcpTarget) { return candidate.summary; } if (candidate.summary.includes(mcpTarget.displayName)) { return candidate.summary; } return `${mcpTarget.displayName}: ${candidate.summary}`; } function isToolLikeMessage( message: AgentActivityMessage, payload = recordValue(message.payload) ): boolean { const normalizedKind = normalizeToken(message.kind); const normalizedType = [ stringValue(payload.type), stringValue(payload.action), stringValue(payload.requestType) ] .map((value) => normalizeToken(value ?? "")) .join(" "); return ( includesAny(normalizedKind, ["call", "tool"]) || includesAny(normalizedType, ["call", "tool"]) || Boolean( stringValue(payload.callType) || stringValue(payload.toolName) || stringValue(payload.tool) || hasRecordValue(payload.input) || hasRecordValue(payload.output) || hasRecordValue(payload.error) ) ); } function structuralToolLabelTokens( message: AgentActivityMessage, payload = recordValue(message.payload) ): Set { const metadata = recordValue(payload.metadata); return new Set( [ message.kind, message.status, stringValue(payload.type), stringValue(payload.action), stringValue(payload.requestType), stringValue(payload.callType), stringValue(payload.toolName), stringValue(payload.name), stringValue(payload.tool), stringValue(metadata.callType), stringValue(metadata.toolName), stringValue(metadata.name), stringValue(metadata.tool) ] .map((value) => normalizeToken(value ?? "")) .filter(Boolean) ); } function includesAny(value: string, needles: readonly string[]): boolean { return needles.some((needle) => value.includes(normalizeToken(needle))); } function isToolDigestSignalCandidate( candidate: MessageSummaryCandidate ): boolean { return ( candidate.source === "tool.error" || candidate.source === "tool.output" || candidate.source === "tool.input" ); } function looksLikeBareLabel(value: string): boolean { const trimmed = value.trim(); return ( trimmed.length > 0 && trimmed.length <= 40 && !/\s/u.test(trimmed) && !/[/:\\.[\]{}()=#?&]/u.test(trimmed) ); } function normalizeToken(value: string): string { return value.replace(/[_\s.-]+/g, "").toLowerCase(); } function firstNonEmptyString(...values: Array): string { return ( values.find((value) => value !== null && value.trim().length > 0) ?? "" ); } function stringValue(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } function stringArrayFirstValue(value: unknown): string | null { if (!Array.isArray(value)) { return null; } return ( value.map(stringValue).find((item): item is string => Boolean(item)) ?? null ); } function textFromContentValue(value: unknown): string | null { if (typeof value === "string") { return stringValue(value); } if (Array.isArray(value)) { return ( value .map(textFromContentValue) .find((item): item is string => Boolean(item)) ?? null ); } if (!value || typeof value !== "object") { return null; } const record = value as Record; return firstNonEmptyString( stringValue(record.text), "content" in record ? textFromContentValue(record.content) : null ); } function normalizeSummaryText(value: string | null): string | null { if (!value) { return null; } return value .replace(/^#{1,6}\s+error\s*\n+/iu, "") .replace(/\s+/gu, " ") .trim(); } function recordValue(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; } function hasRecordValue(value: unknown): boolean { return Object.keys(recordValue(value)).length > 0; } function positiveNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null; }