Merge pull request #2575 from apple-ouyang/fix/issue-2571-subagent-safeguards

fix(delegate-task): add subagent turn limit and model routing transparency
This commit is contained in:
YeonGyu-Kim
2026-03-25 21:46:01 +09:00
committed by GitHub
2 changed files with 38 additions and 2 deletions

View File

@@ -39,6 +39,8 @@ export function isSessionComplete(messages: SessionMessage[]): boolean {
return lastUser.info.id < lastAssistant.info.id
}
const DEFAULT_MAX_ASSISTANT_TURNS = 300
export async function pollSyncSession(
ctx: ToolContextWithMetadata,
client: OpencodeClient,
@@ -48,16 +50,20 @@ export async function pollSyncSession(
toastManager: { removeTask: (id: string) => void } | null | undefined
taskId: string | undefined
anchorMessageCount?: number
maxAssistantTurns?: number
},
timeoutMs?: number
): Promise<string | null> {
const syncTiming = getTimingConfig()
const maxPollTimeMs = Math.max(timeoutMs ?? getDefaultSyncPollTimeoutMs(), 50)
const maxTurns = input.maxAssistantTurns ?? DEFAULT_MAX_ASSISTANT_TURNS
const pollStart = Date.now()
let pollCount = 0
let timedOut = false
let assistantTurnCount = 0
let lastSeenAssistantId: string | undefined
log("[task] Starting poll loop", { sessionID: input.sessionID, agentToUse: input.agentToUse })
log("[task] Starting poll loop", { sessionID: input.sessionID, agentToUse: input.agentToUse, maxTurns })
while (Date.now() - pollStart < maxPollTimeMs) {
if (ctx.abort?.aborted) {
@@ -112,7 +118,23 @@ export async function pollSyncSession(
break
}
// 计数新出现的 assistant 轮次,用于熔断无限循环
const lastAssistant = [...msgs].reverse().find((m) => m.info?.role === "assistant")
if (lastAssistant?.info?.id && lastAssistant.info.id !== lastSeenAssistantId) {
lastSeenAssistantId = lastAssistant.info.id
assistantTurnCount++
if (assistantTurnCount >= maxTurns) {
log("[task] Max assistant turns reached, aborting to prevent infinite loop", {
sessionID: input.sessionID,
assistantTurnCount,
maxTurns,
})
abortSyncSession(client, input.sessionID, "max_turns_exceeded")
if (input.toastManager && input.taskId) input.toastManager.removeTask(input.taskId)
return `Task aborted: subagent exceeded ${maxTurns} assistant turns without completing. This usually indicates an infinite tool-call loop. Session ID: ${input.sessionID}`
}
}
const hasAssistantText = msgs.some((m) => {
if (m.info?.role !== "assistant") return false
const parts = m.parts ?? []

View File

@@ -149,9 +149,23 @@ export async function executeSyncTask(
const duration = formatDuration(startTime)
// 检测模型路由是否与父 session 不同,给用户可见的提示
const actualModelStr = categoryModel
? `${categoryModel.providerID}/${categoryModel.modelID}`
: undefined
const parentModelStr = parentContext.model
? `${parentContext.model.providerID}/${parentContext.model.modelID}`
: undefined
const modelRoutingNote =
actualModelStr && parentModelStr && actualModelStr !== parentModelStr
? `\n⚠ Model routing: parent used ${parentModelStr}, this subagent used ${actualModelStr} (via category: ${args.category ?? "unknown"})`
: actualModelStr
? `\nModel: ${actualModelStr}${args.category ? ` (category: ${args.category})` : ""}`
: ""
return `Task completed in ${duration}.
Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""}
Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""}${modelRoutingNote}
---