fix(agent-teams): fail fast on teammate launch errors
This commit is contained in:
parent
dbcad8fd97
commit
1a5030d359
@ -18,6 +18,18 @@ function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function resolveLaunchFailureMessage(status: string | undefined, error: string | undefined): string {
|
||||
if (status === "error") {
|
||||
return error ? `teammate_launch_failed:${error}` : "teammate_launch_failed"
|
||||
}
|
||||
|
||||
if (status === "cancelled") {
|
||||
return "teammate_launch_cancelled"
|
||||
}
|
||||
|
||||
return "teammate_launch_timeout"
|
||||
}
|
||||
|
||||
function buildLaunchPrompt(teamName: string, teammateName: string, userPrompt: string): string {
|
||||
return [
|
||||
`You are teammate "${teammateName}" in team "${teamName}".`,
|
||||
@ -87,17 +99,28 @@ export async function spawnTeammate(params: SpawnTeammateParams): Promise<TeamTe
|
||||
|
||||
const start = Date.now()
|
||||
let sessionID = launched.sessionID
|
||||
let latestStatus: string | undefined
|
||||
let latestError: string | undefined
|
||||
while (!sessionID && Date.now() - start < 30_000) {
|
||||
await delay(50)
|
||||
const task = params.manager.getTask(launched.id)
|
||||
latestStatus = task?.status
|
||||
latestError = task?.error
|
||||
if (task?.status === "error" || task?.status === "cancelled") {
|
||||
throw new Error(resolveLaunchFailureMessage(task.status, task.error))
|
||||
}
|
||||
sessionID = task?.sessionID
|
||||
}
|
||||
|
||||
if (!sessionID) {
|
||||
throw new Error(resolveLaunchFailureMessage(latestStatus, latestError))
|
||||
}
|
||||
|
||||
const nextMember: TeamTeammateMember = {
|
||||
...teammate,
|
||||
isActive: true,
|
||||
backgroundTaskID: launched.id,
|
||||
...(sessionID ? { sessionID } : {}),
|
||||
sessionID,
|
||||
}
|
||||
|
||||
const current = readTeamConfigOrThrow(params.teamName)
|
||||
|
||||
@ -71,6 +71,14 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
||||
execute: async (args: Record<string, unknown>): Promise<string> => {
|
||||
try {
|
||||
const input = TeamForceKillInputSchema.parse(args)
|
||||
const teamError = validateTeamName(input.team_name)
|
||||
if (teamError) {
|
||||
return JSON.stringify({ error: teamError })
|
||||
}
|
||||
const agentError = validateAgentName(input.agent_name)
|
||||
if (agentError) {
|
||||
return JSON.stringify({ error: agentError })
|
||||
}
|
||||
const config = readTeamConfigOrThrow(input.team_name)
|
||||
const member = getTeamMember(config, input.agent_name)
|
||||
if (!member || !isTeammateMember(member)) {
|
||||
@ -78,7 +86,11 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
||||
}
|
||||
|
||||
await cancelTeammateRun(manager, member)
|
||||
writeTeamConfig(input.team_name, removeTeammate(config, input.agent_name))
|
||||
const refreshedConfig = readTeamConfigOrThrow(input.team_name)
|
||||
const refreshedMember = getTeamMember(refreshedConfig, input.agent_name)
|
||||
if (refreshedMember && isTeammateMember(refreshedMember)) {
|
||||
writeTeamConfig(input.team_name, removeTeammate(refreshedConfig, input.agent_name))
|
||||
}
|
||||
resetOwnerTasks(input.team_name, input.agent_name)
|
||||
|
||||
return JSON.stringify({ success: true, message: `${input.agent_name} stopped` })
|
||||
@ -99,9 +111,17 @@ export function createProcessShutdownTool(): ToolDefinition {
|
||||
execute: async (args: Record<string, unknown>): Promise<string> => {
|
||||
try {
|
||||
const input = TeamProcessShutdownInputSchema.parse(args)
|
||||
const teamError = validateTeamName(input.team_name)
|
||||
if (teamError) {
|
||||
return JSON.stringify({ error: teamError })
|
||||
}
|
||||
if (input.agent_name === "team-lead") {
|
||||
return JSON.stringify({ error: "cannot_shutdown_team_lead" })
|
||||
}
|
||||
const agentError = validateAgentName(input.agent_name)
|
||||
if (agentError) {
|
||||
return JSON.stringify({ error: agentError })
|
||||
}
|
||||
|
||||
const config = readTeamConfigOrThrow(input.team_name)
|
||||
const member = getTeamMember(config, input.agent_name)
|
||||
|
||||
@ -76,6 +76,24 @@ function createMockManager(): MockManagerHandles {
|
||||
return { manager, launchCalls, resumeCalls, cancelCalls }
|
||||
}
|
||||
|
||||
function createFailingLaunchManager(): BackgroundManager {
|
||||
return {
|
||||
launch: async () => ({ id: "bg-fail" }),
|
||||
getTask: () => ({
|
||||
id: "bg-fail",
|
||||
parentSessionID: "ses-main",
|
||||
parentMessageID: "msg-main",
|
||||
description: "failed launch",
|
||||
prompt: "prompt",
|
||||
agent: "sisyphus-junior",
|
||||
status: "error",
|
||||
error: "launch failed",
|
||||
}),
|
||||
resume: async () => ({ id: "resume-unused" }),
|
||||
cancelTask: async () => true,
|
||||
} as unknown as BackgroundManager
|
||||
}
|
||||
|
||||
function createContext(): TestToolContext {
|
||||
return {
|
||||
sessionID: "ses-main",
|
||||
@ -225,6 +243,26 @@ describe("agent-teams tools functional", () => {
|
||||
expect(payload.taskId).toBe(createdTask.id)
|
||||
})
|
||||
|
||||
test("rejects invalid task id input for task_get", async () => {
|
||||
//#given
|
||||
const { manager } = createMockManager()
|
||||
const tools = createAgentTeamsTools(manager)
|
||||
const context = createContext()
|
||||
|
||||
await executeJsonTool(tools, "team_create", { team_name: "core" }, context)
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(
|
||||
tools,
|
||||
"team_task_get",
|
||||
{ team_name: "core", task_id: "../../etc/passwd" },
|
||||
context,
|
||||
) as { error?: string }
|
||||
|
||||
//#then
|
||||
expect(result.error).toBe("task_id_invalid")
|
||||
})
|
||||
|
||||
test("spawn_teammate + send_message + force_kill_teammate execute end-to-end", async () => {
|
||||
//#given
|
||||
const { manager, launchCalls, resumeCalls, cancelCalls } = createMockManager()
|
||||
@ -342,4 +380,35 @@ describe("agent-teams tools functional", () => {
|
||||
expect(taskAfterKill.owner).toBeUndefined()
|
||||
expect(taskAfterKill.status).toBe("pending")
|
||||
})
|
||||
|
||||
test("rolls back teammate when launch fails", async () => {
|
||||
//#given
|
||||
const manager = createFailingLaunchManager()
|
||||
const tools = createAgentTeamsTools(manager)
|
||||
const context = createContext()
|
||||
await executeJsonTool(tools, "team_create", { team_name: "core" }, context)
|
||||
|
||||
//#when
|
||||
const spawnResult = await executeJsonTool(
|
||||
tools,
|
||||
"spawn_teammate",
|
||||
{
|
||||
team_name: "core",
|
||||
name: "worker_1",
|
||||
prompt: "Handle release prep",
|
||||
},
|
||||
context,
|
||||
) as { error?: string }
|
||||
|
||||
//#then
|
||||
expect(spawnResult.error).toBe("teammate_launch_failed:launch failed")
|
||||
|
||||
//#when
|
||||
const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, context) as {
|
||||
members: Array<{ name: string }>
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(config.members.map((member) => member.name)).toEqual(["team-lead"])
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user