fix(team-mode): keep member communication tools visible

This commit is contained in:
YeonGyu-Kim
2026-05-10 12:54:16 +09:00
parent 1e4a51e21b
commit b36389ef2c
5 changed files with 82 additions and 37 deletions
+14
View File
@@ -54,6 +54,20 @@ describe("read-only agent tool restrictions", () => {
} }
}) })
test("allows team tools for team member prompt restrictions", () => {
// given
const teamMemberAgentName = "sisyphus-junior"
// when
const restrictions = getAgentToolRestrictions(teamMemberAgentName, { includeTeamToolDenylist: false })
// then
for (const toolName of TEAM_TOOL_NAMES) {
expect(restrictions[toolName]).toBeUndefined()
}
expect(restrictions.task).toBe(false)
})
describe("Oracle", () => { describe("Oracle", () => {
test("denies all file-writing tools", () => { test("denies all file-writing tools", () => {
// given // given
+17 -12
View File
@@ -722,7 +722,9 @@ The fallback retry session is now created and can be inspected directly.
task: false, task: false,
call_omo_agent: true, call_omo_agent: true,
question: false, question: false,
...getAgentToolRestrictions(input.agent), ...getAgentToolRestrictions(input.agent, {
includeTeamToolDenylist: input.teamRunId === undefined,
}),
} }
setSessionTools(sessionID, tools) setSessionTools(sessionID, tools)
return tools return tools
@@ -742,7 +744,9 @@ The fallback retry session is now created and can be inspected directly.
taskId: task.id, taskId: task.id,
}) })
try { try {
const fallbackBody = buildFallbackBody(promptBody, FALLBACK_AGENT) const fallbackBody = buildFallbackBody(promptBody, FALLBACK_AGENT, {
includeTeamToolDenylist: input.teamRunId === undefined,
})
setSessionTools(sessionID, fallbackBody.tools as Record<string, boolean>) setSessionTools(sessionID, fallbackBody.tools as Record<string, boolean>)
await promptWithModelSuggestionRetry(this.client, { await promptWithModelSuggestionRetry(this.client, {
path: { id: sessionID }, path: { id: sessionID },
@@ -1103,7 +1107,9 @@ The fallback retry session is now created and can be inspected directly.
task: false, task: false,
call_omo_agent: true, call_omo_agent: true,
question: false, question: false,
...getAgentToolRestrictions(existingTask.agent), ...getAgentToolRestrictions(existingTask.agent, {
includeTeamToolDenylist: existingTask.teamRunId === undefined,
}),
} }
setSessionTools(existingTask.sessionId!, tools) setSessionTools(existingTask.sessionId!, tools)
return tools return tools
@@ -1584,7 +1590,7 @@ The fallback retry session is now created and can be inspected directly.
}) })
} }
private tryFallbackRetry( private async tryFallbackRetry(
task: BackgroundTask, task: BackgroundTask,
errorInfo: { name?: string; message?: string }, errorInfo: { name?: string; message?: string },
source: string, source: string,
@@ -1620,14 +1626,13 @@ The task was re-queued on a fallback model after a retryable failure.
) )
}, },
}) })
return result.then((retried) => { const retried = await result
if (retried && previousSessionID) { if (retried && previousSessionID) {
this.clearSessionOutputObserved(previousSessionID) this.clearSessionOutputObserved(previousSessionID)
this.clearSessionTodoObservation(previousSessionID) this.clearSessionTodoObservation(previousSessionID)
subagentSessions.delete(previousSessionID) subagentSessions.delete(previousSessionID)
} }
return retried return retried
})
} }
markForNotification(task: BackgroundTask): void { markForNotification(task: BackgroundTask): void {
+30 -18
View File
@@ -29,7 +29,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
return { data: {} } return { data: {} }
}, },
}, },
} as any } as never
const onTaskError = mock(() => {}) const onTaskError = mock(() => {})
@@ -64,7 +64,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
// Wait for the fire-and-forget prompt chain to settle // Wait for the fire-and-forget prompt chain to settle
await new Promise(resolve => setTimeout(resolve, 50)) await new Promise(resolve => setTimeout(resolve, 50))
@@ -76,11 +76,23 @@ describe("background-agent spawner agent-not-found fallback", () => {
expect(promptCalls[1].body.agent).toBe("general") expect(promptCalls[1].body.agent).toBe("general")
// Original prompt content preserved in fallback // Original prompt content preserved in fallback
expect(promptCalls[1].body.parts).toEqual(promptCalls[0].body.parts) expect(promptCalls[1].body.parts).toEqual(promptCalls[0].body.parts)
// Tool restrictions recomputed for fallback agent (general has no restrictions) // Tool restrictions recomputed for fallback agent while preserving delegated-subagent team tool denial
expect(promptCalls[1].body.tools).toEqual({ expect(promptCalls[1].body.tools).toEqual({
task: false, task: false,
call_omo_agent: true, call_omo_agent: true,
question: false, question: false,
team_create: false,
team_delete: false,
team_shutdown_request: false,
team_approve_shutdown: false,
team_reject_shutdown: false,
team_send_message: false,
team_task_create: false,
team_task_list: false,
team_task_update: false,
team_task_get: false,
team_status: false,
team_list: false,
}) })
// Task agent identity updated to reflect fallback // Task agent identity updated to reflect fallback
expect(task.agent).toBe("general") expect(task.agent).toBe("general")
@@ -101,7 +113,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
throw new Error("Connection timeout") throw new Error("Connection timeout")
}, },
}, },
} as any } as never
const onTaskError = mock(() => {}) const onTaskError = mock(() => {})
@@ -133,7 +145,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
await new Promise(resolve => setTimeout(resolve, 50)) await new Promise(resolve => setTimeout(resolve, 50))
//#then //#then
@@ -154,7 +166,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
throw new Error('Agent not found: "Sisyphus-Junior". Available agents: build, explore, general, plan') throw new Error('Agent not found: "Sisyphus-Junior". Available agents: build, explore, general, plan')
}, },
}, },
} as any } as never
const onTaskError = mock(() => {}) const onTaskError = mock(() => {})
@@ -186,7 +198,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
await new Promise(resolve => setTimeout(resolve, 50)) await new Promise(resolve => setTimeout(resolve, 50))
//#then //#then
@@ -213,7 +225,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
return { data: {} } return { data: {} }
}, },
}, },
} as any } as never
const onTaskError = mock(() => {}) const onTaskError = mock(() => {})
@@ -248,7 +260,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
await new Promise(resolve => setTimeout(resolve, 50)) await new Promise(resolve => setTimeout(resolve, 50))
//#then //#then
@@ -276,7 +288,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
return { data: {} } return { data: {} }
}, },
}, },
} as any } as never
const onTaskError = mock(() => {}) const onTaskError = mock(() => {})
@@ -311,7 +323,7 @@ describe("background-agent spawner agent-not-found fallback", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
await new Promise(resolve => setTimeout(resolve, 50)) await new Promise(resolve => setTimeout(resolve, 50))
//#then //#then
@@ -338,11 +350,11 @@ describe("background-agent spawner fallback model promotion", () => {
return { data: {} } return { data: {} }
}), }),
}, },
} as any } as never
const concurrencyManager = { const concurrencyManager = {
release: mock(() => {}), release: mock(() => {}),
} as any } as never
const onTaskError = mock(() => {}) const onTaskError = mock(() => {})
@@ -455,7 +467,7 @@ describe("background-agent spawner fallback model promotion", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
//#then //#then
expect(promptCalls).toHaveLength(1) expect(promptCalls).toHaveLength(1)
@@ -569,7 +581,7 @@ describe("background-agent spawner fallback model promotion", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
await new Promise((resolve) => setTimeout(resolve, 0)) await new Promise((resolve) => setTimeout(resolve, 0))
//#then //#then
@@ -623,7 +635,7 @@ describe("background-agent spawner fallback model promotion", () => {
} }
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
await new Promise((resolve) => setTimeout(resolve, 0)) await new Promise((resolve) => setTimeout(resolve, 0))
//#then //#then
@@ -653,7 +665,7 @@ describe("background-agent spawner tmux callback ordering", () => {
return { data: {} } return { data: {} }
}, },
}, },
} as any } as never
const onSubagentSessionCreated = mock(async () => { const onSubagentSessionCreated = mock(async () => {
events.push("tmux.callback.start") events.push("tmux.callback.start")
@@ -694,7 +706,7 @@ describe("background-agent spawner tmux callback ordering", () => {
try { try {
//#when //#when
await startTask(item as any, ctx as any) await startTask(item as never, ctx as never)
await new Promise((resolve) => setTimeout(resolve, 20)) await new Promise((resolve) => setTimeout(resolve, 20))
//#then //#then
+15 -5
View File
@@ -28,6 +28,7 @@ export function isAgentNotFoundError(error: unknown): boolean {
export function buildFallbackBody( export function buildFallbackBody(
originalBody: Record<string, unknown>, originalBody: Record<string, unknown>,
fallbackAgent: string, fallbackAgent: string,
options: { includeTeamToolDenylist?: boolean } = {},
): Record<string, unknown> { ): Record<string, unknown> {
return { return {
...originalBody, ...originalBody,
@@ -36,7 +37,7 @@ export function buildFallbackBody(
task: false, task: false,
call_omo_agent: true, call_omo_agent: true,
question: false, question: false,
...getAgentToolRestrictions(fallbackAgent), ...getAgentToolRestrictions(fallbackAgent, options),
}, },
} }
} }
@@ -60,6 +61,7 @@ export function createTask(input: LaunchInput): BackgroundTask {
agent: input.agent, agent: input.agent,
parentSessionId: input.parentSessionId, parentSessionId: input.parentSessionId,
parentMessageId: input.parentMessageId, parentMessageId: input.parentMessageId,
teamRunId: input.teamRunId,
parentModel: input.parentModel, parentModel: input.parentModel,
parentAgent: input.parentAgent, parentAgent: input.parentAgent,
model: input.model, model: input.model,
@@ -160,7 +162,9 @@ export async function startTask(
task: false, task: false,
call_omo_agent: true, call_omo_agent: true,
question: false, question: false,
...getAgentToolRestrictions(normalizedAgent), ...getAgentToolRestrictions(normalizedAgent, {
includeTeamToolDenylist: input.teamRunId === undefined,
}),
}, },
parts: [createInternalAgentTextPart(input.prompt)], parts: [createInternalAgentTextPart(input.prompt)],
} }
@@ -179,7 +183,9 @@ export async function startTask(
try { try {
await promptWithModelSuggestionRetry(client, { await promptWithModelSuggestionRetry(client, {
path: { id: sessionID }, path: { id: sessionID },
body: buildFallbackBody(promptBody, FALLBACK_AGENT), body: buildFallbackBody(promptBody, FALLBACK_AGENT, {
includeTeamToolDenylist: input.teamRunId === undefined,
}),
}) })
task.agent = FALLBACK_AGENT task.agent = FALLBACK_AGENT
return return
@@ -294,7 +300,9 @@ export async function resumeTask(
task: false, task: false,
call_omo_agent: true, call_omo_agent: true,
question: false, question: false,
...getAgentToolRestrictions(task.agent), ...getAgentToolRestrictions(task.agent, {
includeTeamToolDenylist: task.teamRunId === undefined,
}),
}, },
parts: [createInternalAgentTextPart(input.prompt)], parts: [createInternalAgentTextPart(input.prompt)],
} }
@@ -312,7 +320,9 @@ export async function resumeTask(
try { try {
await promptWithModelSuggestionRetry(client, { await promptWithModelSuggestionRetry(client, {
path: { id: task.sessionId! }, path: { id: task.sessionId! },
body: buildFallbackBody(resumeBody, FALLBACK_AGENT), body: buildFallbackBody(resumeBody, FALLBACK_AGENT, {
includeTeamToolDenylist: task.teamRunId === undefined,
}),
}) })
task.agent = FALLBACK_AGENT task.agent = FALLBACK_AGENT
return return
+6 -2
View File
@@ -59,14 +59,18 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
}, },
} }
export function getAgentToolRestrictions(agentName: string): Record<string, boolean> { type AgentToolRestrictionsOptions = {
includeTeamToolDenylist?: boolean
}
export function getAgentToolRestrictions(agentName: string, options: AgentToolRestrictionsOptions = {}): Record<string, boolean> {
const stripped = stripInvisibleAgentCharacters(agentName) const stripped = stripInvisibleAgentCharacters(agentName)
const agentRestrictions = AGENT_RESTRICTIONS[stripped] const agentRestrictions = AGENT_RESTRICTIONS[stripped]
?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === stripped.toLowerCase())?.[1] ?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === stripped.toLowerCase())?.[1]
?? {} ?? {}
return { return {
...TEAM_TOOL_DENYLIST, ...(options.includeTeamToolDenylist === false ? {} : TEAM_TOOL_DENYLIST),
...agentRestrictions, ...agentRestrictions,
} }
} }