import type { BackgroundTask } from "../../../features/background-agent" import type { BackgroundOutputClient } from "../types" import { formatDuration, truncateText, formatMessageTime } from "./utils" import { extractMessages, getErrorMessage, type BackgroundOutputMessagesResult, type FullSessionMessage, extractToolResultText } from "./message-processing" import { consumeNewMessages } from "../../../shared/session-cursor" const MAX_MESSAGE_LIMIT = 100 const THINKING_MAX_CHARS = 2000 export function formatTaskStatus(task: BackgroundTask): string { let duration: string if (task.status === "pending" && task.queuedAt) { duration = formatDuration(task.queuedAt, undefined) } else if (task.startedAt) { duration = formatDuration(task.startedAt, task.completedAt) } else { duration = "N/A" } const promptPreview = truncateText(task.prompt, 500) let progressSection = "" if (task.progress?.lastTool) { progressSection = `\n| Last tool | ${task.progress.lastTool} |` } let lastMessageSection = "" if (task.progress?.lastMessage) { const truncated = truncateText(task.progress.lastMessage, 500) const messageTime = task.progress.lastMessageAt ? task.progress.lastMessageAt.toISOString() : "N/A" lastMessageSection = ` ## Last Message (${messageTime}) \`\`\` ${truncated} \`\`\`` } let statusNote = "" if (task.status === "pending") { statusNote = ` > **Queued**: Task is waiting for a concurrency slot to become available.` } else if (task.status === "running") { statusNote = ` > **Note**: No need to wait explicitly - the system will notify you when this task completes.` } else if (task.status === "error") { statusNote = ` > **Failed**: The task encountered an error. Check the last message for details.` } const durationLabel = task.status === "pending" ? "Queued for" : "Duration" return `# Task Status | Field | Value | |-------|-------| | Task ID | \`${task.id}\` | | Description | ${task.description} | | Agent | ${task.agent} | | Status | **${task.status}** | | ${durationLabel} | ${duration} | | Session ID | \`${task.sessionID}\` |${progressSection} ${statusNote} ## Original Prompt \`\`\` ${promptPreview} \`\`\`${lastMessageSection}` } export async function formatTaskResult(task: BackgroundTask, client: BackgroundOutputClient): Promise { if (!task.sessionID) { return `Error: Task has no sessionID` } const messagesResult: BackgroundOutputMessagesResult = await client.session.messages({ path: { id: task.sessionID }, }) const errorMessage = getErrorMessage(messagesResult) if (errorMessage) { return `Error fetching messages: ${errorMessage}` } const messages = extractMessages(messagesResult) if (!Array.isArray(messages) || messages.length === 0) { return `Task Result Task ID: ${task.id} Description: ${task.description} Duration: ${formatDuration(task.startedAt ?? new Date(), task.completedAt)} Session ID: ${task.sessionID} --- (No messages found)` } // Include both assistant messages AND tool messages // Tool results (grep, glob, bash output) come from role "tool" const relevantMessages = messages.filter( (m) => m.info?.role === "assistant" || m.info?.role === "tool" ) if (relevantMessages.length === 0) { return `Task Result Task ID: ${task.id} Description: ${task.description} Duration: ${formatDuration(task.startedAt ?? new Date(), task.completedAt)} Session ID: ${task.sessionID} --- (No assistant or tool response found)` } // Sort by time ascending (oldest first) to process messages in order const sortedMessages = [...relevantMessages].sort((a, b) => { const timeA = String((a as { info?: { time?: string } }).info?.time ?? "") const timeB = String((b as { info?: { time?: string } }).info?.time ?? "") return timeA.localeCompare(timeB) }) const newMessages = consumeNewMessages(task.sessionID, sortedMessages) if (newMessages.length === 0) { const duration = formatDuration(task.startedAt ?? new Date(), task.completedAt) return `Task Result Task ID: ${task.id} Description: ${task.description} Duration: ${duration} Session ID: ${task.sessionID} --- (No new output since last check)` } // Extract content from ALL messages, not just the last one // Tool results may be in earlier messages while the final message is empty const extractedContent: string[] = [] for (const message of newMessages) { for (const part of message.parts ?? []) { // Handle both "text" and "reasoning" parts (thinking models use "reasoning") if ((part.type === "text" || part.type === "reasoning") && part.text) { extractedContent.push(part.text) } else if (part.type === "tool_result") { // Tool results contain the actual output from tool calls const toolResult = part as { content?: string | Array<{ type: string; text?: string }> } if (typeof toolResult.content === "string" && toolResult.content) { extractedContent.push(toolResult.content) } else if (Array.isArray(toolResult.content)) { // Handle array of content blocks for (const block of toolResult.content) { // Handle both "text" and "reasoning" parts (thinking models use "reasoning") if ((block.type === "text" || block.type === "reasoning") && block.text) { extractedContent.push(block.text) } } } } } } const textContent = extractedContent .filter((text) => text.length > 0) .join("\n\n") const duration = formatDuration(task.startedAt ?? new Date(), task.completedAt) return `Task Result Task ID: ${task.id} Description: ${task.description} Duration: ${duration} Session ID: ${task.sessionID} --- ${textContent || "(No text output)"}` } export async function formatFullSession( task: BackgroundTask, client: BackgroundOutputClient, options: { includeThinking: boolean messageLimit?: number sinceMessageId?: string includeToolResults: boolean thinkingMaxChars?: number } ): Promise { if (!task.sessionID) { return formatTaskStatus(task) } const messagesResult: BackgroundOutputMessagesResult = await client.session.messages({ path: { id: task.sessionID }, }) const errorMessage = getErrorMessage(messagesResult) if (errorMessage) { return `Error fetching messages: ${errorMessage}` } const rawMessages = extractMessages(messagesResult) if (!Array.isArray(rawMessages)) { return "Error fetching messages: invalid response" } const sortedMessages = [...(rawMessages as FullSessionMessage[])].sort((a, b) => { const timeA = String(a.info?.time ?? "") const timeB = String(b.info?.time ?? "") return timeA.localeCompare(timeB) }) let filteredMessages = sortedMessages if (options.sinceMessageId) { const index = filteredMessages.findIndex((message) => message.id === options.sinceMessageId) if (index === -1) { return `Error: since_message_id not found: ${options.sinceMessageId}` } filteredMessages = filteredMessages.slice(index + 1) } const includeThinking = options.includeThinking const includeToolResults = options.includeToolResults const thinkingMaxChars = options.thinkingMaxChars ?? THINKING_MAX_CHARS const normalizedMessages: FullSessionMessage[] = [] for (const message of filteredMessages) { const parts = (message.parts ?? []).filter((part) => { if (part.type === "thinking" || part.type === "reasoning") { return includeThinking } if (part.type === "tool_result") { return includeToolResults } return part.type === "text" }) if (parts.length === 0) { continue } normalizedMessages.push({ ...message, parts }) } const limit = typeof options.messageLimit === "number" ? Math.min(options.messageLimit, MAX_MESSAGE_LIMIT) : undefined const hasMore = limit !== undefined && normalizedMessages.length > limit const visibleMessages = limit !== undefined ? normalizedMessages.slice(0, limit) : normalizedMessages const lines: string[] = [] lines.push("# Full Session Output") lines.push("") lines.push(`Task ID: ${task.id}`) lines.push(`Description: ${task.description}`) lines.push(`Status: ${task.status}`) lines.push(`Session ID: ${task.sessionID}`) lines.push(`Total messages: ${normalizedMessages.length}`) lines.push(`Returned: ${visibleMessages.length}`) lines.push(`Has more: ${hasMore ? "true" : "false"}`) lines.push("") lines.push("## Messages") if (visibleMessages.length === 0) { lines.push("") lines.push("(No messages found)") return lines.join("\n") } for (const message of visibleMessages) { const role = message.info?.role ?? "unknown" const agent = message.info?.agent ? ` (${message.info.agent})` : "" const time = formatMessageTime(message.info?.time) const idLabel = message.id ? ` id=${message.id}` : "" lines.push("") lines.push(`[${role}${agent}] ${time}${idLabel}`) for (const part of message.parts ?? []) { if (part.type === "text" && part.text) { lines.push(part.text.trim()) } else if (part.type === "thinking" && part.thinking) { lines.push(`[thinking] ${truncateText(part.thinking, thinkingMaxChars)}`) } else if (part.type === "reasoning" && part.text) { lines.push(`[thinking] ${truncateText(part.text, thinkingMaxChars)}`) } else if (part.type === "tool_result") { const toolTexts = extractToolResultText(part) for (const toolText of toolTexts) { lines.push(`[tool result] ${toolText}`) } } } } return lines.join("\n") }