diff --git a/src/features/background-agent/types.ts b/src/features/background-agent/types.ts index 3c5928872..7ac4d5d14 100644 --- a/src/features/background-agent/types.ts +++ b/src/features/background-agent/types.ts @@ -63,6 +63,12 @@ export interface BackgroundTask { isUnstableAgent?: boolean /** Category used for this task (e.g., 'quick', 'visual-engineering') */ category?: string + /** Flag to write raw output to file on completion */ + writeOutputToFile?: boolean + /** Path to the output file after successful write */ + outputFilePath?: string + /** Race condition guard: prevents concurrent re-entry during async completion */ + _isCompleting?: boolean /** Last message count for stability detection */ lastMsgCount?: number @@ -89,6 +95,8 @@ export interface LaunchInput { skillContent?: string category?: string sessionPermission?: SessionPermissionRule[] + /** Flag to write raw output to file on completion */ + writeOutputToFile?: boolean } export interface ResumeInput { diff --git a/src/tools/council-archive/council-response-extractor.test.ts b/src/tools/council-archive/council-response-extractor.test.ts new file mode 100644 index 000000000..d86eeb54c --- /dev/null +++ b/src/tools/council-archive/council-response-extractor.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "bun:test" +import { extractCouncilResponse } from "./council-response-extractor" + +describe("extractCouncilResponse", () => { + describe("#given complete COUNCIL_MEMBER_RESPONSE tags", () => { + it("#then returns has_response true, response_complete true, and the content", () => { + const result = extractCouncilResponse("analysis here") + + expect(result).toEqual({ + has_response: true, + response_complete: true, + result: "analysis here", + }) + }) + }) + + describe("#given incomplete tags (opening but no closing)", () => { + it("#then returns has_response true, response_complete false, and partial content", () => { + const result = extractCouncilResponse("partial analysis") + + expect(result).toEqual({ + has_response: true, + response_complete: false, + result: "partial analysis", + }) + }) + }) + + describe("#given missing tags (no opening tag)", () => { + it("#then returns has_response false, response_complete false, and null result", () => { + const result = extractCouncilResponse("Just some plain text without any tags.") + + expect(result).toEqual({ + has_response: false, + response_complete: false, + result: null, + }) + }) + }) + + describe("#given empty content between tags", () => { + it("#then returns has_response true, response_complete true, and empty string result", () => { + const result = extractCouncilResponse("") + + expect(result).toEqual({ + has_response: true, + response_complete: true, + result: "", + }) + }) + }) + + describe("#given multiple tag pairs", () => { + it("#then returns content from the last opening tag", () => { + const text = + "first analysis\nSome interim text\nfinal analysis" + const result = extractCouncilResponse(text) + + expect(result).toEqual({ + has_response: true, + response_complete: true, + result: "final analysis", + }) + }) + }) + + describe("#given an empty string", () => { + it("#then returns has_response false and null result", () => { + const result = extractCouncilResponse("") + + expect(result).toEqual({ + has_response: false, + response_complete: false, + result: null, + }) + }) + }) + + describe("#given whitespace-only content between tags", () => { + it("#then returns has_response true, response_complete true, and empty string result", () => { + const result = extractCouncilResponse(" ") + + expect(result).toEqual({ + has_response: true, + response_complete: true, + result: "", + }) + }) + }) + + describe("#given content with surrounding text before the opening tag", () => { + it("#then returns only the tagged content", () => { + const text = "Some preamble text\nthe actual response" + const result = extractCouncilResponse(text) + + expect(result).toEqual({ + has_response: true, + response_complete: true, + result: "the actual response", + }) + }) + }) +}) diff --git a/src/tools/council-archive/council-response-extractor.ts b/src/tools/council-archive/council-response-extractor.ts new file mode 100644 index 000000000..76c439e70 --- /dev/null +++ b/src/tools/council-archive/council-response-extractor.ts @@ -0,0 +1,26 @@ +export const OPENING_TAG = "" +export const CLOSING_TAG = "" + +export interface CouncilResponseExtraction { + has_response: boolean + response_complete: boolean + result: string | null +} + +export function extractCouncilResponse(fullText: string): CouncilResponseExtraction { + const lastOpenIdx = fullText.lastIndexOf(OPENING_TAG) + if (lastOpenIdx === -1) { + return { has_response: false, response_complete: false, result: null } + } + + const contentStart = lastOpenIdx + OPENING_TAG.length + const closingAfterLastOpen = fullText.indexOf(CLOSING_TAG, contentStart) + + if (closingAfterLastOpen === -1) { + const partial = fullText.slice(contentStart).trim() + return { has_response: true, response_complete: false, result: partial || null } + } + + const content = fullText.slice(contentStart, closingAfterLastOpen).trim() + return { has_response: true, response_complete: true, result: content } +}