Files
tutti/packages/agent/gui/agent-message-center/workspaceAgentMessageCenterDigest.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

541 lines
14 KiB
TypeScript

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<string, unknown>,
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<string, unknown>
): 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<string, unknown>
): 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<string, unknown>
): 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<string> | 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<string> {
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 | null>): 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<string, unknown>;
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<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
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;
}