feat(athena): add session handoff with Question tool for Atlas/Prometheus routing

After Athena synthesizes council findings, presents user with Question tool
TUI to choose: Atlas (fix now), Prometheus (create plan), or no action.
On selection, session_handoff tool stores intent + calls updateSessionAgent(),
then agent-handoff hook fires on session.idle to switch the main session's
active agent via promptAsync with synthesis context.
This commit is contained in:
ismeth
2026-02-13 12:57:16 +01:00
committed by YeonGyu-Kim
parent 84c91cb8d7
commit 7db0f110ee
16 changed files with 281 additions and 12 deletions
+1
View File
@@ -38,6 +38,7 @@ export { createCallOmoAgent } from "./call-omo-agent"
export { createAthenaCouncilTool } from "./athena-council"
export { createLookAt } from "./look-at"
export { createDelegateTask } from "./delegate-task"
export { createSessionHandoffTool } from "./session-handoff"
export {
createTaskCreateTool,
createTaskGetTool,
+1
View File
@@ -0,0 +1 @@
export { createSessionHandoffTool } from "./tools"
+91
View File
@@ -0,0 +1,91 @@
import { describe, test, expect, beforeEach } from "bun:test"
import { createSessionHandoffTool } from "./tools"
import { consumePendingHandoff, _resetForTesting as resetHandoff } from "../../features/agent-handoff"
import { getSessionAgent, _resetForTesting as resetSession } from "../../features/claude-code-session-state"
describe("session_handoff tool", () => {
const sessionID = "test-session-123"
const messageID = "msg-456"
const agent = "athena"
const toolContext = {
sessionID,
messageID,
agent,
abort: new AbortController().signal,
}
beforeEach(() => {
resetHandoff()
resetSession()
})
//#given valid atlas handoff args
//#when execute is called
//#then it stores pending handoff and updates session agent
test("should queue handoff to atlas", async () => {
const tool = createSessionHandoffTool()
const result = await tool.execute(
{ agent: "atlas", context: "Fix the auth bug based on council findings" },
toolContext
)
expect(result).toContain("atlas")
expect(result).toContain("Handoff queued")
const handoff = consumePendingHandoff(sessionID)
expect(handoff).toEqual({
agent: "atlas",
context: "Fix the auth bug based on council findings",
})
expect(getSessionAgent(sessionID)).toBe("atlas")
})
//#given valid prometheus handoff args
//#when execute is called
//#then it stores pending handoff for prometheus
test("should queue handoff to prometheus", async () => {
const tool = createSessionHandoffTool()
const result = await tool.execute(
{ agent: "Prometheus", context: "Create a plan for the refactoring" },
toolContext
)
expect(result).toContain("prometheus")
expect(result).toContain("Handoff queued")
const handoff = consumePendingHandoff(sessionID)
expect(handoff?.agent).toBe("prometheus")
})
//#given an invalid agent name
//#when execute is called
//#then it returns an error
test("should reject invalid agent names", async () => {
const tool = createSessionHandoffTool()
const result = await tool.execute(
{ agent: "librarian", context: "Some context" },
toolContext
)
expect(result).toContain("Invalid handoff target")
expect(result).toContain("librarian")
expect(consumePendingHandoff(sessionID)).toBeUndefined()
})
//#given agent name with different casing
//#when execute is called
//#then it normalizes to lowercase
test("should handle case-insensitive agent names", async () => {
const tool = createSessionHandoffTool()
await tool.execute(
{ agent: "ATLAS", context: "Fix things" },
toolContext
)
const handoff = consumePendingHandoff(sessionID)
expect(handoff?.agent).toBe("atlas")
expect(getSessionAgent(sessionID)).toBe("atlas")
})
})
+37
View File
@@ -0,0 +1,37 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import { setPendingHandoff } from "../../features/agent-handoff"
import { updateSessionAgent } from "../../features/claude-code-session-state"
import type { SessionHandoffArgs } from "./types"
const DESCRIPTION =
"Switch the active session agent. After calling this tool, the session will transition to the specified agent " +
"with the provided context as its starting prompt. Use this to hand off work to another agent " +
"(e.g., Atlas for fixes, Prometheus for planning). The handoff executes when the current agent's turn completes."
const ALLOWED_AGENTS = new Set(["atlas", "prometheus", "sisyphus", "hephaestus"])
export function createSessionHandoffTool(): ToolDefinition {
return tool({
description: DESCRIPTION,
args: {
agent: tool.schema
.string()
.describe("Target agent name to hand off to (e.g., 'atlas', 'prometheus')"),
context: tool.schema
.string()
.describe("Context message for the target agent — include confirmed findings, the original question, and what action to take"),
},
async execute(args: SessionHandoffArgs, toolContext) {
const agentName = args.agent.toLowerCase()
if (!ALLOWED_AGENTS.has(agentName)) {
return `Invalid handoff target: "${args.agent}". Allowed agents: ${[...ALLOWED_AGENTS].join(", ")}`
}
updateSessionAgent(toolContext.sessionID, agentName)
setPendingHandoff(toolContext.sessionID, agentName, args.context)
return `Handoff queued. Session will switch to ${agentName} when your turn completes.`
},
})
}
+4
View File
@@ -0,0 +1,4 @@
export interface SessionHandoffArgs {
agent: string
context: string
}