feat(athena): inject intent runtime guidance and action flows

This commit is contained in:
ismeth
2026-03-01 00:18:39 +01:00
committed by YeonGyu-Kim
parent ed2bb1ead7
commit c265200f5f
8 changed files with 825 additions and 12 deletions
@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, expect, it, beforeEach } from "bun:test"
import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises"
import { join } from "node:path"
@@ -232,4 +234,165 @@ describe("createCouncilFinalize", () => {
expect(secondContent).toContain("Second response")
})
})
describe("#given intent-based runtime guidance injection", () => {
it("#then registers critical custom context for Athena runtime guidance", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_intent.md"),
mockTaskOutput("Council: GPT-5", "Plan proposal"),
"utf-8",
)
const calls: Array<{ sessionID: string; content: string; priority?: string; source: string; id: string }> = []
const collector = {
register: (sessionID: string, options: { id: string; source: string; content: string; priority?: string }) => {
calls.push({
sessionID,
id: options.id,
source: options.source,
content: options.content,
priority: options.priority,
})
},
}
const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
const result = await toolDef.execute(
{ task_ids: ["bg_intent"], name: "intent", intent: "PLAN" },
mockCtx,
)
expect(() => JSON.parse(result)).not.toThrow()
expect(calls).toHaveLength(1)
expect(calls[0].sessionID).toBe(mockCtx.sessionID)
expect(calls[0].id).toBe("athena-runtime-guidance")
expect(calls[0].source).toBe("custom")
expect(calls[0].priority).toBe("critical")
expect(calls[0].content).toContain("<athena_runtime_guidance>")
expect(calls[0].content).toContain("intent: PLAN")
expect(calls[0].content).toContain("Execute full plan (Prometheus)")
expect(calls[0].content).toContain("Execute selected phase (Prometheus)")
expect(calls[0].content).toContain(".sisyphus/athena/notes/{council-session-name}")
expect(calls[0].content).not.toContain("Hand off to Atlas to save the plan as .md")
})
it("#then emits diagnose action options for hephaestus and sisyphus", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_diagnose.md"),
mockTaskOutput("Council: Claude", "Root cause found"),
"utf-8",
)
const calls: Array<{ content: string }> = []
const collector = {
register: (_sessionID: string, options: { content: string }) => {
calls.push({ content: options.content })
},
}
const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
const result = await toolDef.execute(
{ task_ids: ["bg_diagnose"], name: "diagnose", intent: "DIAGNOSE" },
mockCtx,
)
expect(() => JSON.parse(result)).not.toThrow()
expect(calls).toHaveLength(1)
expect(calls[0].content).toContain("Implement (Hephaestus)")
expect(calls[0].content).toContain("Implement (Sisyphus)")
expect(calls[0].content).toContain("Implement (Sisyphus ultrawork)")
expect(calls[0].content).toContain("switch_agent(agent=\"hephaestus\")")
expect(calls[0].content).toContain("switch_agent(agent=\"sisyphus\")")
expect(calls[0].content).toContain("prefix the handoff context with \"ultrawork \"")
expect(calls[0].content).not.toContain("Fix now (Atlas)")
expect(calls[0].content).not.toContain("Create plan (Prometheus)")
})
it("#then emits audit processing mode and batching guidance", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_audit.md"),
mockTaskOutput("Council: Claude", "Audit findings"),
"utf-8",
)
const calls: Array<{ content: string }> = []
const collector = {
register: (_sessionID: string, options: { content: string }) => {
calls.push({ content: options.content })
},
}
const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
const result = await toolDef.execute(
{ task_ids: ["bg_audit"], name: "audit", intent: "AUDIT" },
mockCtx,
)
expect(() => JSON.parse(result)).not.toThrow()
expect(calls).toHaveLength(1)
expect(calls[0].content).toContain("How would you like to process the findings?")
expect(calls[0].content).toContain("One by one")
expect(calls[0].content).toContain("By severity/urgency")
expect(calls[0].content).toContain("By quorum")
expect(calls[0].content).toContain("Default batch size: 3 findings per batch")
expect(calls[0].content).toContain("Hard cap: 5 findings")
expect(calls[0].content).toContain("Example Question tool call (batch of 3 findings)")
expect(calls[0].content).toContain("Finding #10: choose how to proceed.")
expect(calls[0].content).toContain("#10 Action")
expect(calls[0].content).toContain("Stop review")
expect(calls[0].content).toContain("#10:A, #11:skip")
expect(calls[0].content).toContain("Which findings should we act on by severity?")
expect(calls[0].content).toContain("All Critical (N)")
expect(calls[0].content).toContain("All High (N)")
expect(calls[0].content).toContain("All Medium (N)")
expect(calls[0].content).toContain("All Low (N)")
expect(calls[0].content).toContain("Which findings should we act on? You can also type specific finding numbers")
expect(calls[0].content).toContain("All Unanimous (N)")
expect(calls[0].content).toContain("All Majority (N)")
expect(calls[0].content).toContain("All Minority (N)")
expect(calls[0].content).toContain("All Solo (N)")
expect(calls[0].content).toContain("Fix now (Atlas)")
expect(calls[0].content).toContain("Create plan (Prometheus)")
})
it("#then emits informational write-to-document path without atlas delegation", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_eval.md"),
mockTaskOutput("Council: Claude", "Option comparison"),
"utf-8",
)
const calls: Array<{ content: string }> = []
const collector = {
register: (_sessionID: string, options: { content: string }) => {
calls.push({ content: options.content })
},
}
const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
const result = await toolDef.execute(
{ task_ids: ["bg_eval"], name: "eval", intent: "EVALUATE" },
mockCtx,
)
expect(() => JSON.parse(result)).not.toThrow()
expect(calls).toHaveLength(1)
expect(calls[0].content).toContain("What should we do with this evaluation?")
expect(calls[0].content).toContain("Adopt option -> create plan (Prometheus)")
expect(calls[0].content).toContain("Adopt option -> implement now")
expect(calls[0].content).toContain(".sisyphus/athena/notes/{council-session-name}")
expect(calls[0].content).not.toContain("Write to document (Atlas)")
})
it("#then rejects invalid intent values", async () => {
const toolDef = createCouncilFinalize(tmpDir)
const result = await toolDef.execute(
{ task_ids: ["bg_none"], name: "invalid-intent", intent: "NOT_A_REAL_INTENT" },
mockCtx,
)
expect(result).toContain("Invalid intent")
expect(result).toContain("NOT_A_REAL_INTENT")
})
})
})
@@ -3,7 +3,13 @@ import { readFile, writeFile, mkdir, rename } from "node:fs/promises"
import { join, isAbsolute, resolve } from "node:path"
import { randomBytes } from "node:crypto"
import { extractCouncilResponse } from "./council-response-extractor"
import {
buildAthenaRuntimeGuidance,
getValidCouncilIntents,
resolveCouncilIntent,
} from "../../agents/athena"
import { log } from "../../shared/logger"
import type { ContextCollector } from "../../features/context-injector"
import type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types"
interface MetaMember {
@@ -16,6 +22,12 @@ interface MetaMember {
response_complete: boolean
}
type RegisterContext = Pick<ContextCollector, "register">
type CouncilFinalizeToolContext = {
sessionID?: string
}
function slugify(text: string): string {
return text
.toLowerCase()
@@ -66,19 +78,46 @@ function formatMetaYaml(archiveName: string, createdAt: string, members: MetaMem
return lines.join("\n") + "\n"
}
export function createCouncilFinalize(basePath?: string): ToolDefinition {
export function createCouncilFinalize(
basePath?: string,
options?: { contextCollector?: RegisterContext }
): ToolDefinition {
const collector = options?.contextCollector
return tool({
description:
"Finalize council task outputs: extract COUNCIL_MEMBER_RESPONSE content from raw task output files, write per-member archive files, and create meta.yaml.",
"Finalize council task outputs: extract COUNCIL_MEMBER_RESPONSE content from raw task output files, write per-member archive files, inject intent-specific Athena runtime guidance, and create meta.yaml.",
args: {
task_ids: tool.schema
.array(tool.schema.string())
.describe("Array of background task IDs whose output files should be processed"),
name: tool.schema.string().describe("Council name used in the archive directory name"),
intent: tool.schema
.string()
.optional()
.describe(`Classified question intent used for runtime Athena guidance injection. Valid intents: ${getValidCouncilIntents().join(", ")}`),
question: tool.schema.string().optional().describe("Original user question that triggered the council"),
prompt_file: tool.schema.string().optional().describe("Path to the council prompt temp file (will be moved into the archive)"),
},
async execute(args: CouncilFinalizeArgs) {
async execute(args: CouncilFinalizeArgs, toolContext: CouncilFinalizeToolContext) {
const resolvedIntent = resolveCouncilIntent(args.intent)
if (args.intent && !resolvedIntent) {
return `Invalid intent: "${args.intent}". Valid intents: ${getValidCouncilIntents().join(", ")}.`
}
if (collector && resolvedIntent && toolContext.sessionID) {
collector.register(toolContext.sessionID, {
id: "athena-runtime-guidance",
source: "custom",
priority: "critical",
content: buildAthenaRuntimeGuidance(resolvedIntent),
metadata: {
intent: resolvedIntent,
source: "council_finalize",
},
})
}
const base = basePath ?? process.cwd()
const hexId = randomBytes(2).toString("hex")
const archiveName = `council-${args.name}-${hexId}`
+1
View File
@@ -1,6 +1,7 @@
export interface CouncilFinalizeArgs {
task_ids: string[]
name: string
intent?: string
question?: string
prompt_file?: string
}
+34
View File
@@ -86,6 +86,40 @@ describe("switch_agent tool", () => {
expect(entry?.agent).toBe("prometheus")
})
//#given valid hephaestus switch args
//#when execute is called
//#then it stores pending switch for hephaestus
test("should queue switch to hephaestus", async () => {
const tool = createToolWithMockClient()
const result = await tool.execute(
{ agent: "Hephaestus", context: "Implement the selected diagnosis fix" },
toolContext
)
expect(result).toContain("hephaestus")
expect(result).toContain("switch")
const entry = consumePendingSwitch(sessionID)
expect(entry?.agent).toBe("hephaestus")
})
//#given valid sisyphus switch args
//#when execute is called
//#then it stores pending switch for sisyphus
test("should queue switch to sisyphus", async () => {
const tool = createToolWithMockClient()
const result = await tool.execute(
{ agent: "Sisyphus", context: "Implement the selected diagnosis fix" },
toolContext
)
expect(result).toContain("sisyphus")
expect(result).toContain("switch")
const entry = consumePendingSwitch(sessionID)
expect(entry?.agent).toBe("sisyphus")
})
//#given an invalid agent name
//#when execute is called
//#then it returns an error