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
194 lines
5.1 KiB
TypeScript
194 lines
5.1 KiB
TypeScript
import type { AgentTranscriptRowVM } from "../contracts/agentTranscriptRowVM";
|
|
|
|
export interface AgentTranscriptComplexity {
|
|
totalScore: number;
|
|
maxTurnScore: number;
|
|
turnScores: readonly number[];
|
|
}
|
|
|
|
export interface AgentTranscriptComplexityAssessment extends AgentTranscriptComplexity {
|
|
shouldVirtualize: boolean;
|
|
turnCount: number;
|
|
}
|
|
|
|
const AGENT_TRANSCRIPT_COMPLEXITY_VIRTUALIZATION_SCORE = 40;
|
|
const AGENT_TRANSCRIPT_COMPLEXITY_SINGLE_TURN_SCORE = 24;
|
|
const AGENT_TRANSCRIPT_COMPLEXITY_TURN_COUNT = 30;
|
|
const AGENT_TRANSCRIPT_COMPLEXITY_MINIMUM_TURN_COUNT = 8;
|
|
|
|
export function assessAgentTranscriptComplexity(
|
|
turnGroups: ReadonlyArray<{
|
|
rows: ReadonlyArray<{ row: AgentTranscriptRowVM }>;
|
|
}>
|
|
): AgentTranscriptComplexityAssessment {
|
|
const complexity = calculateAgentTranscriptComplexity(turnGroups);
|
|
return {
|
|
...complexity,
|
|
shouldVirtualize: shouldVirtualizeAgentTranscript({
|
|
turnCount: turnGroups.length,
|
|
complexity
|
|
}),
|
|
turnCount: turnGroups.length
|
|
};
|
|
}
|
|
|
|
export function calculateAgentTranscriptComplexity(
|
|
turnGroups: ReadonlyArray<{
|
|
rows: ReadonlyArray<{ row: AgentTranscriptRowVM }>;
|
|
}>
|
|
): AgentTranscriptComplexity {
|
|
const turnScores = turnGroups.map((group) => calculateTurnScore(group.rows));
|
|
const totalScore = turnScores.reduce((total, score) => total + score, 0);
|
|
return {
|
|
totalScore,
|
|
maxTurnScore: Math.max(0, ...turnScores),
|
|
turnScores
|
|
};
|
|
}
|
|
|
|
export function shouldVirtualizeAgentTranscript(input: {
|
|
turnCount: number;
|
|
complexity: AgentTranscriptComplexity;
|
|
}): boolean {
|
|
if (input.turnCount < AGENT_TRANSCRIPT_COMPLEXITY_MINIMUM_TURN_COUNT) {
|
|
return false;
|
|
}
|
|
return (
|
|
input.turnCount >= AGENT_TRANSCRIPT_COMPLEXITY_TURN_COUNT ||
|
|
input.complexity.totalScore >=
|
|
AGENT_TRANSCRIPT_COMPLEXITY_VIRTUALIZATION_SCORE ||
|
|
input.complexity.maxTurnScore >=
|
|
AGENT_TRANSCRIPT_COMPLEXITY_SINGLE_TURN_SCORE
|
|
);
|
|
}
|
|
|
|
function calculateTurnScore(
|
|
rows: ReadonlyArray<{ row: AgentTranscriptRowVM }>
|
|
): number {
|
|
let rowCount = 0;
|
|
let charCount = 0;
|
|
let codeFenceCount = 0;
|
|
let tableCount = 0;
|
|
let toolCallCount = 0;
|
|
let thinkingBlockCount = 0;
|
|
let imageCount = 0;
|
|
|
|
for (const { row } of rows) {
|
|
rowCount += 1;
|
|
if (row.kind === "generated-image") {
|
|
imageCount += 1;
|
|
continue;
|
|
}
|
|
if (row.kind === "message") {
|
|
for (const message of row.messages) {
|
|
charCount += message.body.length;
|
|
codeFenceCount += countCodeFences(message.body);
|
|
tableCount += countMarkdownTables(message.body);
|
|
imageCount +=
|
|
(message.images?.length ?? 0) + countMarkdownImages(message.body);
|
|
}
|
|
for (const thinking of row.thinking) {
|
|
thinkingBlockCount += 1;
|
|
charCount += thinking.body.length;
|
|
codeFenceCount += countCodeFences(thinking.body);
|
|
}
|
|
continue;
|
|
}
|
|
if (row.kind === "tool-group") {
|
|
toolCallCount += row.calls.length;
|
|
thinkingBlockCount += row.entries.filter(
|
|
(entry) => entry.kind === "thinking"
|
|
).length;
|
|
charCount += (row.summary ?? "").length;
|
|
continue;
|
|
}
|
|
if (row.kind === "turn-summary") {
|
|
charCount += row.files.reduce(
|
|
(total, file) =>
|
|
total +
|
|
file.label.length +
|
|
file.path.length +
|
|
(file.unifiedDiff?.length ?? 0) +
|
|
(file.content?.length ?? 0),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
rowCount +
|
|
charCount / 1200 +
|
|
codeFenceCount * 4 +
|
|
tableCount * 5 +
|
|
toolCallCount * 2 +
|
|
thinkingBlockCount * 2 +
|
|
imageCount * 4
|
|
);
|
|
}
|
|
|
|
function countCodeFences(value: string): number {
|
|
let count = 0;
|
|
for (const line of value.split("\n")) {
|
|
const trimmed = line.trimStart();
|
|
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
count += 1;
|
|
}
|
|
}
|
|
return Math.ceil(count / 2);
|
|
}
|
|
|
|
function countMarkdownTables(value: string): number {
|
|
const lines = value.split("\n");
|
|
let count = 0;
|
|
for (let index = 1; index < lines.length; index += 1) {
|
|
if (
|
|
looksLikeTableSeparator(lines[index] ?? "") &&
|
|
lines[index - 1]?.includes("|")
|
|
) {
|
|
count += 1;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function countMarkdownImages(value: string): number {
|
|
let count = 0;
|
|
let index = 0;
|
|
while (index < value.length) {
|
|
const imageStart = value.indexOf("![", index);
|
|
if (imageStart < 0) {
|
|
break;
|
|
}
|
|
const labelEnd = value.indexOf("]", imageStart + 2);
|
|
const targetStart = labelEnd >= 0 ? value.indexOf("(", labelEnd) : -1;
|
|
const targetEnd = targetStart >= 0 ? value.indexOf(")", targetStart) : -1;
|
|
if (
|
|
labelEnd >= 0 &&
|
|
targetStart === labelEnd + 1 &&
|
|
targetEnd > targetStart
|
|
) {
|
|
count += 1;
|
|
index = targetEnd + 1;
|
|
continue;
|
|
}
|
|
index = imageStart + 2;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function looksLikeTableSeparator(line: string): boolean {
|
|
const cells = line
|
|
.split("|")
|
|
.map((cell) => cell.trim())
|
|
.filter(Boolean);
|
|
return (
|
|
cells.length > 0 &&
|
|
cells.every((cell) => {
|
|
const normalized = cell.replace(/:/gu, "");
|
|
return (
|
|
normalized.length >= 3 && [...normalized].every((char) => char === "-")
|
|
);
|
|
})
|
|
);
|
|
}
|