feat(02-02): add council orchestrator and result collector

- Implement executeCouncil with parallel member launch and partial-failure tolerance

- Add result collection mapping and wire Athena exports with read-only athena tool restrictions
This commit is contained in:
ismeth
2026-02-12 12:38:54 +01:00
committed by YeonGyu-Kim
parent 4f9858e7b3
commit 0b89017add
4 changed files with 168 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
import type { LaunchInput, BackgroundTask } from "../../features/background-agent/types"
import { createAgentToolRestrictions } from "../../shared/permission-compat"
import { buildCouncilPrompt } from "./council-prompt"
import { collectCouncilResults } from "./council-result-collector"
import { parseModelString } from "./model-parser"
import type { CouncilConfig, CouncilExecutionResult, CouncilMemberConfig, CouncilMemberResponse } from "./types"
export interface CouncilLaunchInput extends LaunchInput {
temperature?: number
permission?: Record<string, "ask" | "allow" | "deny">
}
export interface CouncilLauncher {
launch(input: CouncilLaunchInput): Promise<BackgroundTask>
}
export interface CouncilExecutionInput {
question: string
council: CouncilConfig
launcher: CouncilLauncher
parentSessionID: string
parentMessageID: string
parentAgent?: string
}
export async function executeCouncil(input: CouncilExecutionInput): Promise<CouncilExecutionResult> {
const { question, council, launcher, parentSessionID, parentMessageID, parentAgent } = input
const prompt = buildCouncilPrompt(question)
const startTimes = new Map<string, number>()
const launchResults = await Promise.allSettled(
council.members.map((member) =>
launchMember(
member,
prompt,
launcher,
parentSessionID,
parentMessageID,
parentAgent,
startTimes
)
)
)
const launchedTasks: BackgroundTask[] = []
const launchedMembers: CouncilMemberConfig[] = []
const launchFailures: CouncilMemberResponse[] = []
launchResults.forEach((result, index) => {
const member = council.members[index]
if (result.status === "fulfilled") {
launchedTasks.push(result.value)
launchedMembers.push(member)
return
}
launchFailures.push({
member,
status: "error",
error: `Launch failed: ${String(result.reason)}`,
taskId: "",
durationMs: 0,
})
})
const collected = collectCouncilResults(launchedTasks, launchedMembers, startTimes)
const responses = [...collected, ...launchFailures]
const completedCount = responses.filter((response) => response.status === "completed").length
return {
question,
responses,
totalMembers: council.members.length,
completedCount,
failedCount: council.members.length - completedCount,
}
}
async function launchMember(
member: CouncilMemberConfig,
prompt: string,
launcher: CouncilLauncher,
parentSessionID: string,
parentMessageID: string,
parentAgent: string | undefined,
startTimes: Map<string, number>
): Promise<BackgroundTask> {
const parsedModel = parseModelString(member.model)
if (!parsedModel) {
throw new Error(`Invalid model string: "${member.model}"`)
}
const restrictions = createAgentToolRestrictions(["write", "edit", "task"])
const memberName = member.name ?? member.model
const task = await launcher.launch({
description: `Council member: ${memberName}`,
prompt,
agent: "athena",
parentSessionID,
parentMessageID,
parentAgent,
model: {
providerID: parsedModel.providerID,
modelID: parsedModel.modelID,
...(member.variant ? { variant: member.variant } : {}),
},
...(member.temperature !== undefined ? { temperature: member.temperature } : {}),
permission: restrictions.permission,
})
startTimes.set(task.id, Date.now())
return task
}

View File

@@ -0,0 +1,36 @@
import type { BackgroundTask, BackgroundTaskStatus } from "../../features/background-agent/types"
import type { CouncilMemberConfig, CouncilMemberResponse, CouncilMemberStatus } from "./types"
export function collectCouncilResults(
tasks: BackgroundTask[],
members: CouncilMemberConfig[],
startTimes: Map<string, number>
): CouncilMemberResponse[] {
return tasks.map((task, index) => {
const member = members[index]
const status = mapTaskStatus(task.status)
const startTime = startTimes.get(task.id) ?? Date.now()
const finishedAt = task.completedAt?.getTime() ?? Date.now()
return {
member,
status,
response: status === "completed" ? task.result : undefined,
error: status === "completed" ? undefined : (task.error ?? `Task status: ${task.status}`),
taskId: task.id,
durationMs: Math.max(0, finishedAt - startTime),
}
})
}
function mapTaskStatus(taskStatus: BackgroundTaskStatus): CouncilMemberStatus {
if (taskStatus === "completed") {
return "completed"
}
if (taskStatus === "cancelled" || taskStatus === "interrupt") {
return "timeout"
}
return "error"
}

View File

@@ -1,4 +1,6 @@
export * from "./types"
export * from "./model-parser"
export * from "./council-prompt"
export * from "./council-orchestrator"
export * from "./council-result-collector"
export * from "../../config/schema/athena"

View File

@@ -4,6 +4,8 @@
* true = tool allowed, false = tool denied.
*/
import { createAgentToolRestrictions } from "./permission-compat"
const EXPLORATION_AGENT_DENYLIST: Record<string, boolean> = {
write: false,
edit: false,
@@ -11,6 +13,10 @@ const EXPLORATION_AGENT_DENYLIST: Record<string, boolean> = {
call_omo_agent: false,
}
const ATHENA_RESTRICTIONS = permissionToToolBooleans(
createAgentToolRestrictions(["write", "edit", "task"]).permission
)
const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
explore: EXPLORATION_AGENT_DENYLIST,
@@ -42,6 +48,16 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
"sisyphus-junior": {
task: false,
},
athena: ATHENA_RESTRICTIONS,
}
function permissionToToolBooleans(
permission: Record<string, "ask" | "allow" | "deny">
): Record<string, boolean> {
return Object.fromEntries(
Object.entries(permission).map(([tool, value]) => [tool, value === "allow"])
)
}
export function getAgentToolRestrictions(agentName: string): Record<string, boolean> {