import { Buffer } from "node:buffer"; import { sanitizeTerminalText } from "./job-status.js"; import type { LaunchThinkingSource, SubagentSession, ThinkingLevel, UsageStats } from "./types.ts"; export const COLLECTED_OUTPUT_MAX_BYTES = 50 * 1024; export const CAPTURED_TEXT_MAX_BYTES = 50 * 1024; export interface TruncatedText { text: string; truncation?: { originalBytes: number; keptBytes: number }; } export const truncateUtf8 = (text: string, maxBytes: number): TruncatedText => { const bytes = Buffer.from(text, "utf8"); if (bytes.length <= maxBytes) return { text }; let end = Math.max(0, Math.min(bytes.length, Math.floor(maxBytes))); while (end > 0) { const truncated = bytes.subarray(0, end).toString("utf8"); if (Buffer.from(truncated, "utf8").equals(bytes.subarray(0, end))) { return { text: truncated, truncation: { originalBytes: bytes.length, keptBytes: end } }; } end -= 1; } return { text: "", truncation: { originalBytes: bytes.length, keptBytes: 0 } }; }; const truncationNotice = (originalBytes: number, keptBytes: number): string => `Output truncated: retained ${keptBytes} of ${originalBytes} bytes.`; const captureNotice = (label: string, truncation?: { originalBytes: number; keptBytes: number }): string | undefined => truncation && `${label} capture truncated: retained ${truncation.keptBytes} of ${truncation.originalBytes} bytes.`; const terminalSequence = /(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c|$)|(?:\x1b\[|\x9b)[\x20-\x3f]*[\x40-\x7e]/gu; const remainingTerminalControls = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu; const stripTerminalControls = (text: string): string => text.replace(terminalSequence, "").replace(remainingTerminalControls, ""); const boundedMetadataLine = (text: string): string => truncateUtf8(stripTerminalControls(text).replace(/\s+/gu, " ").trim(), CAPTURED_TEXT_MAX_BYTES).text; const sanitizeDiagnostics = (text: string): string => stripTerminalControls(text).replace(/\r\n?/gu, "\n"); const usageLine = ({ usage }: { usage: UsageStats }): string => `- Usage: input ${usage.input}, output ${usage.output}, cache read ${usage.cacheRead}, cache write ${usage.cacheWrite}, cost ${usage.cost}, turns ${usage.turns}`; type LaunchSelection = { launchThinkingLevel?: ThinkingLevel; launchThinkingSource?: LaunchThinkingSource; }; const thinkingSourceLabel = (source?: LaunchThinkingSource): string => { if (source === "job") return "job override"; if (source === "profile") return "profile"; if (source === "parent") return "parent session"; return "model or Pi default"; }; const thinkingSelection = (job: LaunchSelection): string | undefined => { if (job.launchThinkingLevel) { const source = thinkingSourceLabel(job.launchThinkingSource); const level = boundedMetadataLine(job.launchThinkingLevel); return level ? `${level} (${source})` : source; } if (job.launchThinkingSource === "model_or_pi_default") return "model or Pi default"; return undefined; }; const capPayload = (content: string, maxBytes: number): string => { const originalBytes = Buffer.byteLength(content, "utf8"); if (originalBytes <= maxBytes) return content; let notice = truncationNotice(originalBytes, 0); while (true) { const availableBytes = maxBytes - Buffer.byteLength(`\n\n${notice}`, "utf8"); const text = truncateUtf8(content, Math.max(0, availableBytes)).text; const nextNotice = truncationNotice(originalBytes, Buffer.byteLength(text, "utf8")); if (nextNotice === notice) return `${text}\n\n${notice}`; notice = nextNotice; } }; export const capCollectedPayload = (content: string): string => capPayload(content, COLLECTED_OUTPUT_MAX_BYTES); export const capCollectedPayloadWithDiagnostics = ( content: string, diagnostics: readonly string[], ): string => { if (diagnostics.length === 0) return capCollectedPayload(content); const diagnosticSection = `## Collection diagnostics\n\n${diagnostics.map((diagnostic) => `- ${diagnostic}`).join("\n")}`; const suffix = `\n\n${diagnosticSection}`; const contentBudget = COLLECTED_OUTPUT_MAX_BYTES - Buffer.byteLength(suffix, "utf8"); return `${capPayload(content, Math.max(0, contentBudget))}${suffix}`; }; type CollectedGenerationSession = SubagentSession & { generation: SubagentSession["generation"] & { result: NonNullable }; }; const formatGenerationResult = (session: CollectedGenerationSession): string => { const { generation } = session; const result = generation.result; const captureNotices = [ captureNotice("Output", result.outputTruncation), captureNotice("Stderr", result.stderrTruncation), captureNotice("Error", result.errorTruncation), ].filter((notice): notice is string => notice !== undefined); const reportedModel = result.model ?? generation.reportedModel; const safeSessionId = boundedMetadataLine(session.id); const safeAgentName = boundedMetadataLine(session.profile.name); const safeTask = boundedMetadataLine(session.request.task); const safeLaunchModel = session.launchModel === undefined ? undefined : boundedMetadataLine(session.launchModel); const safeThinking = thinkingSelection(session); const safeReportedModel = reportedModel === undefined ? undefined : boundedMetadataLine(reportedModel); const output = sanitizeTerminalText(result.output); const stderr = sanitizeDiagnostics(result.stderr); const errorMessage = result.errorMessage === undefined ? undefined : sanitizeDiagnostics(result.errorMessage); const metadata = [ `- Session: ${session.state}`, `- Work: ${generation.state}`, `- Result: ${generation.resultState}`, `- Agent: ${safeAgentName}`, `- Access: ${session.request.writeAccess ? "write" : "read-only"}`, `- Task: ${safeTask}`, ...(safeLaunchModel ? [`- Launch model: ${safeLaunchModel}`] : []), ...(safeThinking ? [`- Launch thinking: ${boundedMetadataLine(safeThinking)}`] : []), ...(safeReportedModel ? [`- Reported model: ${safeReportedModel}`] : []), usageLine(generation), ]; const includesError = generation.state === "failed" || generation.state === "cancelled" || result.errorMessage !== undefined || result.stderr.length > 0 || result.malformedEventCount > 0; const sections = [ `# Subagent result: ${safeSessionId} ยท Generation: ${generation.number}`, ...(captureNotices.length ? [`## Capture limits\n${captureNotices.join("\n")}`] : []), metadata.join("\n"), `## Result\n\n${output}`, ...(includesError ? [[ "## Error", `Error:\n${errorMessage ?? "none"}`, `Stderr:\n${stderr || "none"}`, `Malformed events: ${result.malformedEventCount}`, ].join("\n\n")] : []), ]; return capCollectedPayload(sections.join("\n\n")); }; export function formatCollectedResult(session: SubagentSession): string { if (!session.generation.result) { throw new Error(`Session ${session.id} generation ${session.generation.number} has no collected result to format`); } return formatGenerationResult(session as CollectedGenerationSession); }