Files
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

480 lines
14 KiB
TypeScript

import type { AgentActivityTurn } from "@tutti-os/agent-activity-core";
import type { AgentGUIObservationGap } from "../../../types";
import type {
AgentMessageContentVM,
AgentMessageRowVM
} from "../contracts/agentMessageRowVM";
import type { AgentTranscriptTurnGroup } from "./agentTranscriptModel";
export type AgentTurnTiming =
| {
kind: "live";
startedAtUnixMs: number;
frozenAtUnixMs?: number | null;
observationGapPresentationState?: NonNullable<
AgentGUIObservationGap["presentationState"]
>;
}
| { kind: "settled"; elapsedSeconds: number };
export type AgentTurnDuration =
| { kind: "seconds"; seconds: number }
| { kind: "minutes"; minutes: number }
| { kind: "minutes-seconds"; minutes: number; seconds: number };
export type AgentTurnWorkSectionRow =
AgentTranscriptTurnGroup["rows"][number] & {
renderKey?: string;
};
export interface AgentTurnWorkSectionSegment {
kind: "visible" | "work";
rows: AgentTurnWorkSectionRow[];
}
export interface AgentTurnWorkSectionModel {
timing: AgentTurnTiming;
leadingRows: AgentTurnWorkSectionRow[];
sections: AgentTurnWorkSectionSegment[];
collapseEligible: boolean;
}
/**
* Selects at most one participant header per speaker in each presentation
* turn. Completed collapsed turns prefer a visible message so the Agent header
* does not disappear into the hidden work section.
*/
export function findParticipantHeaderRenderKeys(
groups: readonly AgentTranscriptTurnGroup[],
rowKeys: readonly string[],
modelByGroupKey: ReadonlyMap<string, AgentTurnWorkSectionModel | null>,
participantTurnIndexByRowIndex: ReadonlyMap<number, number>
): ReadonlySet<string> {
const headerCandidateByTurn = new Map<
number,
Map<
AgentMessageRowVM["speaker"],
{ renderKey: string; visibilityPriority: number }
>
>();
for (const group of groups) {
const model = modelByGroupKey.get(group.key);
const renderedRows: ReadonlyArray<{
entry: AgentTurnWorkSectionRow;
visibilityPriority: number;
}> = model
? [
...model.leadingRows.map((entry) => ({
entry,
visibilityPriority: 0
})),
...model.sections.flatMap((section) =>
section.rows.map((entry) => ({
entry,
visibilityPriority:
model.collapseEligible && section.kind === "work" ? 1 : 0
}))
)
]
: group.rows.map((entry) => ({ entry, visibilityPriority: 0 }));
for (const { entry, visibilityPriority } of renderedRows) {
const row = entry.row;
if (row.kind !== "message" || row.messages.length === 0) {
continue;
}
const participantTurnIndex =
participantTurnIndexByRowIndex.get(entry.rowIndex) ?? entry.rowIndex;
let candidateBySpeaker = headerCandidateByTurn.get(participantTurnIndex);
if (!candidateBySpeaker) {
candidateBySpeaker = new Map();
headerCandidateByTurn.set(participantTurnIndex, candidateBySpeaker);
}
const currentCandidate = candidateBySpeaker.get(row.speaker);
if (
currentCandidate &&
currentCandidate.visibilityPriority <= visibilityPriority
) {
continue;
}
candidateBySpeaker.set(row.speaker, {
renderKey: entry.renderKey ?? rowKeys[entry.rowIndex] ?? row.id,
visibilityPriority
});
}
}
return new Set(
[...headerCandidateByTurn.values()].flatMap((candidateBySpeaker) =>
[...candidateBySpeaker.values()].map((candidate) => candidate.renderKey)
)
);
}
interface AgentTurnWorkSectionOptions {
collapseIntermediateAssistantReplies?: boolean;
liveFrozenAtUnixMs?: number | null;
liveObservationGapPresentationState?:
| AgentGUIObservationGap["presentationState"]
| null;
}
export function resolveAgentTurnTiming(
turn: AgentActivityTurn | null | undefined,
isActiveTurn: boolean,
submittedAtUnixMs?: number | null
): AgentTurnTiming | null {
if (!turn || !Number.isFinite(turn.startedAtUnixMs)) {
return null;
}
const timingStartedAtUnixMs =
Number.isFinite(submittedAtUnixMs) &&
(submittedAtUnixMs as number) <= turn.startedAtUnixMs
? (submittedAtUnixMs as number)
: turn.startedAtUnixMs;
if (turn.phase !== "settled") {
return isActiveTurn
? { kind: "live", startedAtUnixMs: timingStartedAtUnixMs }
: null;
}
const endUnixMs = turn.settledAtUnixMs;
if (
!Number.isFinite(endUnixMs) ||
(endUnixMs as number) < timingStartedAtUnixMs
) {
return null;
}
return {
kind: "settled",
elapsedSeconds: Math.max(
0,
Math.floor(((endUnixMs as number) - timingStartedAtUnixMs) / 1_000)
)
};
}
export function formatAgentTurnDuration(
elapsedSeconds: number
): AgentTurnDuration {
const safeSeconds = Math.max(0, Math.floor(elapsedSeconds));
if (safeSeconds < 60) {
return { kind: "seconds", seconds: safeSeconds };
}
const minutes = Math.floor(safeSeconds / 60);
const seconds = safeSeconds % 60;
if (seconds === 0) {
return { kind: "minutes", minutes };
}
return { kind: "minutes-seconds", minutes, seconds };
}
export function buildAgentTurnWorkSectionModel(
group: AgentTranscriptTurnGroup,
turn: AgentActivityTurn | null | undefined,
isActiveTurn = false,
options: AgentTurnWorkSectionOptions = {}
): AgentTurnWorkSectionModel | null {
const leadingRowCount = countLeadingUserRows(group.rows);
const leadingRows = group.rows.slice(0, leadingRowCount);
const submittedAtUnixMs = resolveTurnSubmittedAtUnixMs(
leadingRows,
turn?.startedAtUnixMs
);
const timing = resolveAgentTurnTiming(turn, isActiveTurn, submittedAtUnixMs);
if (!timing) {
return null;
}
const finalTarget = findFinalAssistantTextTarget(group.rows);
const sections = buildOrderedSections(
group.rows,
leadingRowCount,
finalTarget,
options.collapseIntermediateAssistantReplies === true
);
const hasHiddenWork = sections.some(
(section) => section.kind === "work" && section.rows.length > 0
);
const collapseEligible =
finalTarget !== null &&
turn?.phase === "settled" &&
turn.outcome === "completed" &&
hasHiddenWork &&
!groupContainsBlockingMessage(group) &&
!group.rows.some(({ row }) => row.kind === "generated-image");
return {
timing:
timing.kind === "live"
? {
...timing,
...(Number.isFinite(options.liveFrozenAtUnixMs) &&
(options.liveFrozenAtUnixMs as number) >= timing.startedAtUnixMs
? { frozenAtUnixMs: options.liveFrozenAtUnixMs as number }
: {}),
...(options.liveObservationGapPresentationState
? {
observationGapPresentationState:
options.liveObservationGapPresentationState
}
: {})
}
: timing,
leadingRows,
sections,
collapseEligible
};
}
function resolveTurnSubmittedAtUnixMs(
leadingRows: readonly AgentTurnWorkSectionRow[],
fallbackUnixMs: number | null | undefined
): number | null {
const exactTimestamps = leadingRows.flatMap(({ row }) => {
if (row.kind !== "message" || row.speaker !== "user") {
return [];
}
return row.messages.flatMap((message) =>
(message.sourceTimelineItems ?? []).flatMap((item) => {
const value = item.payload?.clientSubmittedAtUnixMs;
return validUnixMs(value) ? [value] : [];
})
);
});
const exact = minimumTimestamp(exactTimestamps);
if (exact !== null) {
return exact;
}
const projected = minimumTimestamp(
leadingRows.flatMap(({ row }) =>
row.kind === "message" && row.speaker === "user"
? [
row.occurredAtUnixMs,
...row.messages.map((message) => message.occurredAtUnixMs)
]
: []
)
);
return projected ?? (validUnixMs(fallbackUnixMs) ? fallbackUnixMs : null);
}
function minimumTimestamp(
values: readonly (number | null | undefined)[]
): number | null {
const valid = values.filter(validUnixMs);
return valid.length > 0 ? Math.min(...valid) : null;
}
function validUnixMs(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
function countLeadingUserRows(
rows: readonly AgentTurnWorkSectionRow[]
): number {
let count = 0;
while (isUserMessageRow(rows[count]?.row)) {
count += 1;
}
return count;
}
function buildOrderedSections(
rows: readonly AgentTurnWorkSectionRow[],
startIndex: number,
finalTarget: { rowIndex: number; messageIndex: number } | null,
collapseIntermediateAssistantReplies: boolean
): AgentTurnWorkSectionSegment[] {
const sections: AgentTurnWorkSectionSegment[] = [];
for (let rowIndex = startIndex; rowIndex < rows.length; rowIndex += 1) {
const entry = rows[rowIndex]!;
if (entry.row.kind === "message" && entry.row.speaker === "assistant") {
if (!collapseIntermediateAssistantReplies) {
appendAssistantMessageSections(sections, entry);
} else if (finalTarget?.rowIndex === rowIndex) {
appendFinalAssistantSections(sections, entry, finalTarget.messageIndex);
} else {
appendSectionRow(sections, "work", entry);
}
continue;
}
appendSectionRow(
sections,
isExplicitWorkRow(entry.row) ? "work" : "visible",
entry
);
}
return sections;
}
function appendFinalAssistantSections(
sections: AgentTurnWorkSectionSegment[],
sourceEntry: AgentTurnWorkSectionRow,
finalMessageIndex: number
): void {
const sourceRow = sourceEntry.row as AgentMessageRowVM;
const messagesBeforeFinal = sourceRow.messages.slice(0, finalMessageIndex);
const messagesAfterFinal = sourceRow.messages.slice(finalMessageIndex + 1);
if (sourceRow.thinking.length > 0 || messagesBeforeFinal.length > 0) {
appendSectionRow(sections, "work", {
...sourceEntry,
renderKey: `${sourceRow.id}:turn-work-before`,
row: cloneAssistantRow(sourceRow, messagesBeforeFinal, sourceRow.thinking)
});
}
appendSectionRow(sections, "visible", {
...sourceEntry,
renderKey: `${sourceRow.id}:turn-final`,
row: cloneAssistantRow(
sourceRow,
[sourceRow.messages[finalMessageIndex]!],
[]
)
});
if (messagesAfterFinal.length > 0) {
appendSectionRow(sections, "work", {
...sourceEntry,
renderKey: `${sourceRow.id}:turn-work-after`,
row: cloneAssistantRow(sourceRow, messagesAfterFinal, [])
});
}
}
function appendAssistantMessageSections(
sections: AgentTurnWorkSectionSegment[],
sourceEntry: AgentTurnWorkSectionRow
): void {
const sourceRow = sourceEntry.row as AgentMessageRowVM;
const parts: Array<{
kind: AgentTurnWorkSectionSegment["kind"];
messages: AgentMessageContentVM[];
thinking: AgentMessageRowVM["thinking"];
}> = [];
if (sourceRow.thinking.length > 0) {
parts.push({ kind: "work", messages: [], thinking: sourceRow.thinking });
}
for (const message of sourceRow.messages) {
const kind = isExplicitWorkMessage(message) ? "work" : "visible";
const previous = parts.at(-1);
if (previous?.kind === kind && previous.thinking.length === 0) {
previous.messages.push(message);
continue;
}
parts.push({ kind, messages: [message], thinking: [] });
}
if (parts.length === 0) {
appendSectionRow(sections, "visible", sourceEntry);
return;
}
if (parts.length === 1 && sourceRow.thinking.length === 0) {
appendSectionRow(sections, parts[0]!.kind, sourceEntry);
return;
}
parts.forEach((part, partIndex) => {
appendSectionRow(sections, part.kind, {
...sourceEntry,
renderKey: `${sourceRow.id}:turn-${part.kind}-${partIndex}`,
row: cloneAssistantRow(sourceRow, part.messages, part.thinking)
});
});
}
function appendSectionRow(
sections: AgentTurnWorkSectionSegment[],
kind: AgentTurnWorkSectionSegment["kind"],
row: AgentTurnWorkSectionRow
): void {
const previous = sections.at(-1);
if (previous?.kind === kind) {
previous.rows.push(row);
return;
}
sections.push({ kind, rows: [row] });
}
function findFinalAssistantTextTarget(
rows: readonly AgentTurnWorkSectionRow[]
): { rowIndex: number; messageIndex: number } | null {
for (let rowIndex = rows.length - 1; rowIndex >= 0; rowIndex -= 1) {
const row = rows[rowIndex]?.row;
if (row?.kind !== "message" || row.speaker !== "assistant") {
continue;
}
for (
let messageIndex = row.messages.length - 1;
messageIndex >= 0;
messageIndex -= 1
) {
const message = row.messages[messageIndex];
if (
message?.isTurnFinalText &&
message.body.trim() &&
message.contentKind !== "image-grid" &&
!message.visibleError &&
!message.systemNotice
) {
return { rowIndex, messageIndex };
}
}
}
return null;
}
function groupContainsBlockingMessage(
group: AgentTranscriptTurnGroup
): boolean {
return group.rows.some(
({ row }) =>
row.kind === "message" &&
row.messages.some((message) =>
Boolean(
message.visibleError ||
(message.systemNotice && message.presentationKind === "content")
)
)
);
}
function isExplicitWorkRow(row: AgentTurnWorkSectionRow["row"]): boolean {
return row.kind === "tool-group" || row.kind === "processing";
}
function isExplicitWorkMessage(message: AgentMessageContentVM): boolean {
return (
message.presentationKind === "specific-progress" ||
message.presentationKind === "turn-boundary"
);
}
function isUserMessageRow(
row: AgentTurnWorkSectionRow["row"] | undefined
): row is AgentMessageRowVM {
return row?.kind === "message" && row.speaker === "user";
}
function cloneAssistantRow(
source: AgentMessageRowVM,
messages: AgentMessageContentVM[],
thinking: AgentMessageRowVM["thinking"]
): AgentMessageRowVM {
return {
...source,
messages,
thinking
};
}