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
|
isUnstableAgent?: boolean
|
||||||
/** Category used for this task (e.g., 'quick', 'visual-engineering') */
|
/** Category used for this task (e.g., 'quick', 'visual-engineering') */
|
||||||
category?: string
|
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 */
|
/** Last message count for stability detection */
|
||||||
lastMsgCount?: number
|
lastMsgCount?: number
|
||||||
@@ -89,6 +95,8 @@ export interface LaunchInput {
|
|||||||
skillContent?: string
|
skillContent?: string
|
||||||
category?: string
|
category?: string
|
||||||
sessionPermission?: SessionPermissionRule[]
|
sessionPermission?: SessionPermissionRule[]
|
||||||
|
/** Flag to write raw output to file on completion */
|
||||||
|
writeOutputToFile?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResumeInput {
|
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