From 0b89017add884f9d9a27efd4dd9bb6423e94804f Mon Sep 17 00:00:00 2001 From: ismeth Date: Thu, 12 Feb 2026 12:38:54 +0100 Subject: [PATCH] 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 --- src/agents/athena/council-orchestrator.ts | 114 ++++++++++++++++++ src/agents/athena/council-result-collector.ts | 36 ++++++ src/agents/athena/index.ts | 2 + src/shared/agent-tool-restrictions.ts | 16 +++ 4 files changed, 168 insertions(+) create mode 100644 src/agents/athena/council-orchestrator.ts create mode 100644 src/agents/athena/council-result-collector.ts diff --git a/src/agents/athena/council-orchestrator.ts b/src/agents/athena/council-orchestrator.ts new file mode 100644 index 000000000..f5ccb10fd --- /dev/null +++ b/src/agents/athena/council-orchestrator.ts @@ -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 +} + +export interface CouncilLauncher { + launch(input: CouncilLaunchInput): Promise +} + +export interface CouncilExecutionInput { + question: string + council: CouncilConfig + launcher: CouncilLauncher + parentSessionID: string + parentMessageID: string + parentAgent?: string +} + +export async function executeCouncil(input: CouncilExecutionInput): Promise { + const { question, council, launcher, parentSessionID, parentMessageID, parentAgent } = input + const prompt = buildCouncilPrompt(question) + const startTimes = new Map() + + 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 +): Promise { + 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 +} diff --git a/src/agents/athena/council-result-collector.ts b/src/agents/athena/council-result-collector.ts new file mode 100644 index 000000000..31faac9cd --- /dev/null +++ b/src/agents/athena/council-result-collector.ts @@ -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 +): 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" +} diff --git a/src/agents/athena/index.ts b/src/agents/athena/index.ts index 6e9204985..3788e9e40 100644 --- a/src/agents/athena/index.ts +++ b/src/agents/athena/index.ts @@ -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" diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index 865251d6f..018ebc5f9 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -4,6 +4,8 @@ * true = tool allowed, false = tool denied. */ +import { createAgentToolRestrictions } from "./permission-compat" + const EXPLORATION_AGENT_DENYLIST: Record = { write: false, edit: false, @@ -11,6 +13,10 @@ const EXPLORATION_AGENT_DENYLIST: Record = { call_omo_agent: false, } +const ATHENA_RESTRICTIONS = permissionToToolBooleans( + createAgentToolRestrictions(["write", "edit", "task"]).permission +) + const AGENT_RESTRICTIONS: Record> = { explore: EXPLORATION_AGENT_DENYLIST, @@ -42,6 +48,16 @@ const AGENT_RESTRICTIONS: Record> = { "sisyphus-junior": { task: false, }, + + athena: ATHENA_RESTRICTIONS, +} + +function permissionToToolBooleans( + permission: Record +): Record { + return Object.fromEntries( + Object.entries(permission).map(([tool, value]) => [tool, value === "allow"]) + ) } export function getAgentToolRestrictions(agentName: string): Record {