feat(council-archive): add shared extraction utility and background task type extensions
- Create council-response-extractor.ts with ported extractCouncilResponse() - Add writeOutputToFile, outputFilePath, _isCompleting to BackgroundTask - Add writeOutputToFile to LaunchInput - 8/8 extraction tests passing
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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("<COUNCIL_MEMBER_RESPONSE>analysis here</COUNCIL_MEMBER_RESPONSE>")
|
||||
|
||||
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("<COUNCIL_MEMBER_RESPONSE>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("<COUNCIL_MEMBER_RESPONSE></COUNCIL_MEMBER_RESPONSE>")
|
||||
|
||||
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 =
|
||||
"<COUNCIL_MEMBER_RESPONSE>first analysis</COUNCIL_MEMBER_RESPONSE>\nSome interim text\n<COUNCIL_MEMBER_RESPONSE>final analysis</COUNCIL_MEMBER_RESPONSE>"
|
||||
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("<COUNCIL_MEMBER_RESPONSE> </COUNCIL_MEMBER_RESPONSE>")
|
||||
|
||||
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\n<COUNCIL_MEMBER_RESPONSE>the actual response</COUNCIL_MEMBER_RESPONSE>"
|
||||
const result = extractCouncilResponse(text)
|
||||
|
||||
expect(result).toEqual({
|
||||
has_response: true,
|
||||
response_complete: true,
|
||||
result: "the actual response",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
export const OPENING_TAG = "<COUNCIL_MEMBER_RESPONSE>"
|
||||
export const CLOSING_TAG = "</COUNCIL_MEMBER_RESPONSE>"
|
||||
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user