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:
114
src/agents/athena/council-orchestrator.ts
Normal file
114
src/agents/athena/council-orchestrator.ts
Normal 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
|
||||
}
|
||||
36
src/agents/athena/council-result-collector.ts
Normal file
36
src/agents/athena/council-result-collector.ts
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user