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))
|
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 {
|
function buildLaunchPrompt(teamName: string, teammateName: string, userPrompt: string): string {
|
||||||
return [
|
return [
|
||||||
`You are teammate "${teammateName}" in team "${teamName}".`,
|
`You are teammate "${teammateName}" in team "${teamName}".`,
|
||||||
@ -87,17 +99,28 @@ export async function spawnTeammate(params: SpawnTeammateParams): Promise<TeamTe
|
|||||||
|
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
let sessionID = launched.sessionID
|
let sessionID = launched.sessionID
|
||||||
|
let latestStatus: string | undefined
|
||||||
|
let latestError: string | undefined
|
||||||
while (!sessionID && Date.now() - start < 30_000) {
|
while (!sessionID && Date.now() - start < 30_000) {
|
||||||
await delay(50)
|
await delay(50)
|
||||||
const task = params.manager.getTask(launched.id)
|
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
|
sessionID = task?.sessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sessionID) {
|
||||||
|
throw new Error(resolveLaunchFailureMessage(latestStatus, latestError))
|
||||||
|
}
|
||||||
|
|
||||||
const nextMember: TeamTeammateMember = {
|
const nextMember: TeamTeammateMember = {
|
||||||
...teammate,
|
...teammate,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
backgroundTaskID: launched.id,
|
backgroundTaskID: launched.id,
|
||||||
...(sessionID ? { sessionID } : {}),
|
sessionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = readTeamConfigOrThrow(params.teamName)
|
const current = readTeamConfigOrThrow(params.teamName)
|
||||||
|
|||||||
@ -71,6 +71,14 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
|||||||
execute: async (args: Record<string, unknown>): Promise<string> => {
|
execute: async (args: Record<string, unknown>): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const input = TeamForceKillInputSchema.parse(args)
|
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 config = readTeamConfigOrThrow(input.team_name)
|
||||||
const member = getTeamMember(config, input.agent_name)
|
const member = getTeamMember(config, input.agent_name)
|
||||||
if (!member || !isTeammateMember(member)) {
|
if (!member || !isTeammateMember(member)) {
|
||||||
@ -78,7 +86,11 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
|||||||
}
|
}
|
||||||
|
|
||||||
await cancelTeammateRun(manager, member)
|
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)
|
resetOwnerTasks(input.team_name, input.agent_name)
|
||||||
|
|
||||||
return JSON.stringify({ success: true, message: `${input.agent_name} stopped` })
|
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> => {
|
execute: async (args: Record<string, unknown>): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const input = TeamProcessShutdownInputSchema.parse(args)
|
const input = TeamProcessShutdownInputSchema.parse(args)
|
||||||
|
const teamError = validateTeamName(input.team_name)
|
||||||
|
if (teamError) {
|
||||||
|
return JSON.stringify({ error: teamError })
|
||||||
|
}
|
||||||
if (input.agent_name === "team-lead") {
|
if (input.agent_name === "team-lead") {
|
||||||
return JSON.stringify({ error: "cannot_shutdown_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 config = readTeamConfigOrThrow(input.team_name)
|
||||||
const member = getTeamMember(config, input.agent_name)
|
const member = getTeamMember(config, input.agent_name)
|
||||||
|
|||||||
@ -76,6 +76,24 @@ function createMockManager(): MockManagerHandles {
|
|||||||
return { manager, launchCalls, resumeCalls, cancelCalls }
|
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 {
|
function createContext(): TestToolContext {
|
||||||
return {
|
return {
|
||||||
sessionID: "ses-main",
|
sessionID: "ses-main",
|
||||||
@ -225,6 +243,26 @@ describe("agent-teams tools functional", () => {
|
|||||||
expect(payload.taskId).toBe(createdTask.id)
|
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 () => {
|
test("spawn_teammate + send_message + force_kill_teammate execute end-to-end", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { manager, launchCalls, resumeCalls, cancelCalls } = createMockManager()
|
const { manager, launchCalls, resumeCalls, cancelCalls } = createMockManager()
|
||||||
@ -342,4 +380,35 @@ describe("agent-teams tools functional", () => {
|
|||||||
expect(taskAfterKill.owner).toBeUndefined()
|
expect(taskAfterKill.owner).toBeUndefined()
|
||||||
expect(taskAfterKill.status).toBe("pending")
|
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