fix(agent-teams): align spawn schema and harden inbox rollback behavior

This commit is contained in:
Nguyen Khac Trung Kien
2026-02-08 11:26:20 +07:00
committed by YeonGyu-Kim
parent fe05a1f254
commit 40f844fb85
5 changed files with 102 additions and 20 deletions

View File

@@ -144,24 +144,28 @@ export function readInbox(
return withInboxLock(teamName, () => {
const messages = readInboxMessages(teamName, agentName)
const selected = unreadOnly ? messages.filter((message) => !message.read) : [...messages]
const selectedIndexes = new Set<number>()
const selected = unreadOnly
? messages.filter((message, index) => {
if (!message.read) {
selectedIndexes.add(index)
return true
}
return false
})
: messages.map((message, index) => {
selectedIndexes.add(index)
return message
})
if (!markAsRead || selected.length === 0) {
return selected
}
const selectedSet = unreadOnly ? new Set(selected) : null
let changed = false
const updated = messages.map((message) => {
if (!unreadOnly) {
if (!message.read) {
changed = true
}
return { ...message, read: true }
}
if (selectedSet?.has(message)) {
const updated = messages.map((message, index) => {
if (selectedIndexes.has(index) && !message.read) {
changed = true
return { ...message, read: true }
}
@@ -171,7 +175,7 @@ export function readInbox(
if (changed) {
writeInboxMessages(teamName, agentName, updated)
}
return selected
return updated.filter((_, index) => selectedIndexes.has(index))
})
}

View File

@@ -246,6 +246,8 @@ export async function spawnTeammate(params: SpawnTeammateParams): Promise<TeamTe
updateTeamConfig(params.teamName, (current) => upsertTeammate(current, nextMember))
return nextMember
} catch (error) {
const originalError = error
if (launchedTaskID) {
await params.manager
.cancelTask(launchedTaskID, {
@@ -255,9 +257,20 @@ export async function spawnTeammate(params: SpawnTeammateParams): Promise<TeamTe
})
.catch(() => undefined)
}
updateTeamConfig(params.teamName, (current) => removeTeammate(current, params.name))
clearInbox(params.teamName, params.name)
throw error
try {
updateTeamConfig(params.teamName, (current) => removeTeammate(current, params.name))
} catch (cleanupError) {
void cleanupError
}
try {
clearInbox(params.teamName, params.name)
} catch (cleanupError) {
void cleanupError
}
throw originalError
}
}

View File

@@ -28,7 +28,7 @@ export function createSpawnTeammateTool(manager: BackgroundManager, options?: Ag
team_name: tool.schema.string().describe("Team name"),
name: tool.schema.string().describe("Teammate name"),
prompt: tool.schema.string().describe("Initial teammate prompt"),
category: tool.schema.string().optional().describe("Required category for teammate metadata and routing"),
category: tool.schema.string().describe("Required category for teammate metadata and routing"),
subagent_type: tool.schema.string().optional().describe("Agent name to run (default: sisyphus-junior)"),
model: tool.schema.string().optional().describe("Optional model override in provider/model format"),
plan_mode_required: tool.schema.boolean().optional().describe("Enable plan mode flag in teammate metadata"),
@@ -46,11 +46,11 @@ export function createSpawnTeammateTool(manager: BackgroundManager, options?: Ag
return JSON.stringify({ error: agentError })
}
if (!input.category || !input.category.trim()) {
if (!input.category.trim()) {
return JSON.stringify({ error: "category_required" })
}
if (input.category && input.subagent_type && input.subagent_type !== "sisyphus-junior") {
if (input.subagent_type && input.subagent_type !== "sisyphus-junior") {
return JSON.stringify({ error: "category_conflicts_with_subagent_type" })
}

View File

@@ -363,7 +363,8 @@ describe("agent-teams tools functional", () => {
) as { error?: string }
//#then
expect(result.error).toBe("category_required")
expect(result.error).toBeDefined()
expect(result.error).toContain("category")
})
test("rejects category with incompatible subagent_type", async () => {
@@ -931,6 +932,70 @@ describe("agent-teams tools functional", () => {
expect(Array.isArray(ownInbox)).toBe(true)
})
test("read_inbox returns messages with read=true when mark_as_read is enabled", async () => {
//#given
const { manager } = createMockManager()
const tools = createAgentTeamsTools(manager)
const context = createContext()
await executeJsonTool(tools, "team_create", { team_name: "core" }, context)
await executeJsonTool(
tools,
"spawn_teammate",
{
team_name: "core",
name: "worker_1",
prompt: "Handle release prep",
category: "quick",
},
context,
)
//#when
const unreadBefore = await executeJsonTool(
tools,
"read_inbox",
{
team_name: "core",
agent_name: "worker_1",
unread_only: true,
mark_as_read: false,
},
context,
) as Array<{ read: boolean }>
const markedRead = await executeJsonTool(
tools,
"read_inbox",
{
team_name: "core",
agent_name: "worker_1",
unread_only: true,
mark_as_read: true,
},
context,
) as Array<{ read: boolean }>
const unreadAfter = await executeJsonTool(
tools,
"read_inbox",
{
team_name: "core",
agent_name: "worker_1",
unread_only: true,
mark_as_read: false,
},
context,
) as Array<{ read: boolean }>
//#then
expect(unreadBefore.length).toBeGreaterThan(0)
expect(unreadBefore.every((message) => message.read === false)).toBe(true)
expect(markedRead.length).toBeGreaterThan(0)
expect(markedRead.every((message) => message.read === true)).toBe(true)
expect(unreadAfter).toHaveLength(0)
})
test("rejects unknown session claiming team-lead identity", async () => {
//#given
const { manager } = createMockManager()

View File

@@ -109,7 +109,7 @@ export const TeamSpawnInputSchema = z.object({
team_name: z.string(),
name: z.string(),
prompt: z.string(),
category: z.string().optional(),
category: z.string(),
subagent_type: z.string().optional(),
model: z.string().optional(),
plan_mode_required: z.boolean().optional(),