fdcee08460
Extract 30+ single-responsibility modules from manager.ts (1556 LOC): - task lifecycle: task-starter, task-completer, task-canceller, task-resumer - task queries: task-queries, task-poller, task-queue-processor - notifications: notification-builder, notification-tracker, parent-session-notifier - session handling: session-validator, session-output-validator, session-todo-checker - spawner: spawner/ directory with focused spawn modules - utilities: duration-formatter, error-classifier, message-storage-locator - result handling: result-handler-context, background-task-completer - shutdown: background-manager-shutdown, process-signal
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
import type { OpencodeClient } from "./constants"
|
|
import { log } from "../../shared"
|
|
|
|
type SessionMessagePart = {
|
|
type?: string
|
|
text?: string
|
|
content?: unknown
|
|
}
|
|
|
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null
|
|
}
|
|
|
|
function getMessageRole(message: unknown): string | undefined {
|
|
if (!isObject(message)) return undefined
|
|
const info = message["info"]
|
|
if (!isObject(info)) return undefined
|
|
const role = info["role"]
|
|
return typeof role === "string" ? role : undefined
|
|
}
|
|
|
|
function getMessageParts(message: unknown): SessionMessagePart[] {
|
|
if (!isObject(message)) return []
|
|
const parts = message["parts"]
|
|
if (!Array.isArray(parts)) return []
|
|
|
|
return parts
|
|
.filter((part): part is SessionMessagePart => isObject(part))
|
|
.map((part) => ({
|
|
type: typeof part["type"] === "string" ? part["type"] : undefined,
|
|
text: typeof part["text"] === "string" ? part["text"] : undefined,
|
|
content: part["content"],
|
|
}))
|
|
}
|
|
|
|
function partHasContent(part: SessionMessagePart): boolean {
|
|
if (part.type === "text" || part.type === "reasoning") {
|
|
return Boolean(part.text && part.text.trim().length > 0)
|
|
}
|
|
if (part.type === "tool") return true
|
|
if (part.type === "tool_result") {
|
|
if (typeof part.content === "string") return part.content.trim().length > 0
|
|
if (Array.isArray(part.content)) return part.content.length > 0
|
|
return Boolean(part.content)
|
|
}
|
|
return false
|
|
}
|
|
|
|
export async function validateSessionHasOutput(
|
|
client: OpencodeClient,
|
|
sessionID: string
|
|
): Promise<boolean> {
|
|
try {
|
|
const response = await client.session.messages({
|
|
path: { id: sessionID },
|
|
})
|
|
|
|
const messagesRaw = "data" in response ? response.data : []
|
|
const messages = Array.isArray(messagesRaw) ? messagesRaw : []
|
|
|
|
const hasAssistantOrToolMessage = messages.some((message) => {
|
|
const role = getMessageRole(message)
|
|
return role === "assistant" || role === "tool"
|
|
})
|
|
|
|
if (!hasAssistantOrToolMessage) {
|
|
log("[background-agent] No assistant/tool messages found in session:", sessionID)
|
|
return false
|
|
}
|
|
|
|
const hasContent = messages.some((message) => {
|
|
const role = getMessageRole(message)
|
|
if (role !== "assistant" && role !== "tool") return false
|
|
const parts = getMessageParts(message)
|
|
return parts.some(partHasContent)
|
|
})
|
|
|
|
if (!hasContent) {
|
|
log("[background-agent] Messages exist but no content found in session:", sessionID)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
} catch (error) {
|
|
log("[background-agent] Error validating session output:", error)
|
|
return true
|
|
}
|
|
}
|