import type { TeamTaskState, UsageState } from "../../state/types.ts"; import { emptyCrewAgentProgress } from "../crew-agent-records.ts"; import type { CrewAgentProgress } from "../crew-agent-runtime.ts"; import type { ProgressEventSummary } from "../output/progress-event-coalescer.ts"; function asRecord(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined; } function safeNum(v: number | undefined): number { return Number.isFinite(v) ? v! : 0; } function textFromContent(content: unknown): string[] { if (typeof content === "string") return [content]; if (!Array.isArray(content)) return []; const text: string[] = []; for (const part of content) { const obj = asRecord(part); if (!obj) continue; if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text); else if (typeof obj.content === "string") text.push(obj.content); } return text; } function eventText(event: unknown): string[] { const obj = asRecord(event); if (!obj) return []; const text: string[] = []; if (typeof obj.text === "string") text.push(obj.text); if (typeof obj.output === "string") text.push(obj.output); text.push(...textFromContent(obj.content)); const message = asRecord(obj.message); if (message) text.push(...textFromContent(message.content)); return text.filter((entry) => entry.trim()); } function numberField(obj: Record, keys: string[]): number | undefined { for (const key of keys) { const value = obj[key]; if (typeof value === "number" && Number.isFinite(value)) return value; } return undefined; } function eventUsage(event: unknown): { input?: number; output?: number; turns?: number } | undefined { const obj = asRecord(event); if (!obj) return undefined; const direct = { input: numberField(obj, ["input", "inputTokens", "input_tokens"]), output: numberField(obj, ["output", "outputTokens", "output_tokens"]), turns: numberField(obj, ["turns", "turnCount", "turn_count"]), }; if (Object.values(direct).some((value) => value !== undefined)) return direct; for (const key of ["usage", "tokenUsage", "tokens", "stats"]) { const nested = eventUsage(obj[key]); if (nested) return nested; } const message = asRecord(obj.message); return message ? eventUsage(message.usage) : undefined; } function previewArgs(args: unknown): string | undefined { if (!args) return undefined; try { const text = typeof args === "string" ? args : JSON.stringify(args); return text.length > 240 ? `${text.slice(0, 240)}…` : text; } catch { return undefined; } } export function applyUsageToProgress( progress: CrewAgentProgress | undefined, usage: UsageState | undefined, ): CrewAgentProgress | undefined { if (!usage) return progress; const base = progress ?? emptyCrewAgentProgress(); const tokens = safeNum(usage.input) + safeNum(usage.output) + safeNum(usage.cacheRead) + safeNum(usage.cacheWrite); return { ...base, tokens, turns: usage.turns ?? base.turns }; } export function shouldFlushProgressEvent(event: unknown): boolean { const type = asRecord(event)?.type; return ( type === "tool_execution_start" || type === "tool_execution_end" || type === "message_start" || type === "message_end" || type === "tool_result_end" ); } export function progressEventSummary(task: TeamTaskState, event: unknown): ProgressEventSummary { const type = asRecord(event)?.type; return { eventType: typeof type === "string" ? type : "event", currentTool: task.agentProgress?.currentTool, toolCount: task.agentProgress?.toolCount, tokens: task.agentProgress?.tokens, turns: task.agentProgress?.turns, activityState: task.agentProgress?.activityState, lastActivityAt: task.agentProgress?.lastActivityAt, }; } export function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress { const obj = asRecord(event); const now = new Date().toISOString(); const next: CrewAgentProgress = { ...progress, recentTools: [...progress.recentTools], recentOutput: [...progress.recentOutput], lastActivityAt: now, activityState: "active", }; if (startedAt) { const startMs = new Date(startedAt).getTime(); next.durationMs = Number.isFinite(startMs) ? Date.now() - startMs : undefined; } if (obj?.type === "tool_execution_start") { next.toolCount += 1; next.currentTool = typeof obj.toolName === "string" ? obj.toolName : typeof obj.name === "string" ? obj.name : "tool"; next.currentToolArgs = previewArgs(obj.args); next.currentToolStartedAt = now; } if (obj?.type === "tool_execution_end") { if (next.currentTool) next.recentTools.push({ tool: next.currentTool, args: next.currentToolArgs, endedAt: now, }); next.currentTool = undefined; next.currentToolArgs = undefined; next.currentToolStartedAt = undefined; } if ((obj?.type === "tool_execution_error" || obj?.type === "tool_execution_failed") && next.currentTool) next.failedTool = next.currentTool; const usage = eventUsage(event); if (usage) { next.tokens = safeNum(usage.input) + safeNum(usage.output); next.turns = usage.turns ?? next.turns; } const text = eventText(event); if (text.length > 0) next.recentOutput.push( ...text .flatMap((entry) => entry.split(/\r?\n/)) .filter(Boolean) .slice(-10), ); if (next.recentTools.length > 25) next.recentTools.splice(0, next.recentTools.length - 25); if (next.recentOutput.length > 50) next.recentOutput.splice(0, next.recentOutput.length - 50); return next; }