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
541 lines
14 KiB
TypeScript
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;
|
|
}
|