2026-02-13 12:57:16 +01:00
|
|
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
2026-03-01 14:42:27 +01:00
|
|
|
import { normalizeAgentForPrompt } from "../../shared/agent-display-names"
|
|
|
|
|
import { log } from "../../shared/logger"
|
2026-02-13 13:18:07 +01:00
|
|
|
import type { SwitchAgentArgs } from "./types"
|
2026-02-13 12:57:16 +01:00
|
|
|
|
|
|
|
|
const DESCRIPTION =
|
|
|
|
|
"Switch the active session agent. After calling this tool, the session will transition to the specified agent " +
|
2026-02-13 13:18:07 +01:00
|
|
|
"with the provided context as its starting prompt. Use this to route work to another agent " +
|
|
|
|
|
"(e.g., Atlas for fixes, Prometheus for planning). The switch executes when the current agent's turn completes."
|
2026-02-13 12:57:16 +01:00
|
|
|
|
|
|
|
|
const ALLOWED_AGENTS = new Set(["atlas", "prometheus", "sisyphus", "hephaestus"])
|
|
|
|
|
|
2026-02-18 19:26:33 +01:00
|
|
|
type SessionClient = {
|
|
|
|
|
session: {
|
2026-03-01 14:42:27 +01:00
|
|
|
create: (input?: { body?: { parentID?: string; title?: string } }) => Promise<unknown>
|
2026-02-18 19:26:33 +01:00
|
|
|
promptAsync: (input: {
|
|
|
|
|
path: { id: string }
|
2026-03-01 14:42:27 +01:00
|
|
|
body: { agent?: string; parts: Array<{ type: "text"; text: string }> }
|
2026-02-18 19:26:33 +01:00
|
|
|
}) => Promise<unknown>
|
2026-03-01 14:42:27 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractSessionId(response: unknown): string | undefined {
|
|
|
|
|
if (typeof response !== "object" || response === null) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const root = response as Record<string, unknown>
|
|
|
|
|
|
|
|
|
|
if (typeof root.id === "string" && root.id.length > 0) {
|
|
|
|
|
return root.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = root.data
|
|
|
|
|
if (typeof data === "object" && data !== null) {
|
|
|
|
|
const dataRecord = data as Record<string, unknown>
|
|
|
|
|
if (typeof dataRecord.id === "string" && dataRecord.id.length > 0) {
|
|
|
|
|
return dataRecord.id
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function navigateTuiToSession(client: SessionClient, sessionID: string): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
await (client as any)._client.post({
|
|
|
|
|
url: "/tui/select-session",
|
|
|
|
|
body: { sessionID },
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
})
|
|
|
|
|
return true
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
2026-02-18 19:26:33 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createSwitchAgentTool(args: {
|
|
|
|
|
client: SessionClient
|
|
|
|
|
}): ToolDefinition {
|
|
|
|
|
const { client } = args
|
|
|
|
|
|
2026-02-13 12:57:16 +01:00
|
|
|
return tool({
|
|
|
|
|
description: DESCRIPTION,
|
|
|
|
|
args: {
|
|
|
|
|
agent: tool.schema
|
|
|
|
|
.string()
|
2026-02-13 13:18:07 +01:00
|
|
|
.describe("Target agent name to switch to (e.g., 'atlas', 'prometheus')"),
|
2026-02-13 12:57:16 +01:00
|
|
|
context: tool.schema
|
|
|
|
|
.string()
|
|
|
|
|
.describe("Context message for the target agent — include confirmed findings, the original question, and what action to take"),
|
|
|
|
|
},
|
2026-02-13 13:18:07 +01:00
|
|
|
async execute(args: SwitchAgentArgs, toolContext) {
|
2026-02-13 12:57:16 +01:00
|
|
|
const agentName = args.agent.toLowerCase()
|
|
|
|
|
|
|
|
|
|
if (!ALLOWED_AGENTS.has(agentName)) {
|
2026-02-13 13:18:07 +01:00
|
|
|
return `Invalid switch target: "${args.agent}". Allowed agents: ${[...ALLOWED_AGENTS].join(", ")}`
|
2026-02-13 12:57:16 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-01 14:42:27 +01:00
|
|
|
const targetAgent = normalizeAgentForPrompt(agentName)
|
|
|
|
|
if (!targetAgent) {
|
|
|
|
|
return `Invalid switch target: "${args.agent}". Could not resolve agent name.`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const errors: string[] = []
|
|
|
|
|
|
|
|
|
|
const response = await client.session.create().catch((error: unknown) => {
|
|
|
|
|
errors.push(`session.create failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!response) {
|
|
|
|
|
return `Failed to create handoff session. ${errors.join("; ")}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newSessionID = extractSessionId(response)
|
|
|
|
|
if (!newSessionID) {
|
|
|
|
|
return `Failed to extract session ID from create response: ${JSON.stringify(response)}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const promptResult = await client.session.promptAsync({
|
|
|
|
|
path: { id: newSessionID },
|
|
|
|
|
body: {
|
|
|
|
|
agent: targetAgent,
|
|
|
|
|
parts: [{ type: "text", text: args.context }],
|
|
|
|
|
},
|
|
|
|
|
}).catch((error: unknown) => {
|
|
|
|
|
errors.push(`promptAsync failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const tuiNavigated = await navigateTuiToSession(client, newSessionID)
|
|
|
|
|
|
|
|
|
|
log("[switch-agent] Agent switch applied via fresh session", {
|
|
|
|
|
sourceSessionID: toolContext.sessionID,
|
|
|
|
|
newSessionID,
|
|
|
|
|
agent: targetAgent,
|
|
|
|
|
tuiNavigated,
|
|
|
|
|
promptDelivered: promptResult !== null,
|
2026-02-18 19:26:33 +01:00
|
|
|
})
|
2026-02-13 12:57:16 +01:00
|
|
|
|
2026-03-01 14:42:27 +01:00
|
|
|
const parts = [`Agent switch to ${agentName} initiated. New session: ${newSessionID}`]
|
|
|
|
|
if (!promptResult) parts.push("(warning: prompt delivery failed)")
|
|
|
|
|
if (tuiNavigated) parts.push("Navigated TUI to new session.")
|
|
|
|
|
if (errors.length > 0) parts.push(`Errors: ${errors.join("; ")}`)
|
|
|
|
|
return parts.join(" ")
|
2026-02-13 12:57:16 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|