mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
fix(core): address background subagent lifecycle gaps
- Inherit bgConfig from agentConfig so the resolved approval mode is preserved for background agents (foreground would run AUTO_EDIT but background fell back to DEFAULT, which combined with shouldAvoid- PermissionPrompts would auto-deny every permission request). - Honor SubagentStop blocking decisions in background runs by looping on hook output up to 5 iterations, matching runSubagentWithHooks. - Check terminate mode before reporting completion; non-GOAL modes (ERROR, MAX_TURNS, TIMEOUT) are now reported as failures instead of emitting a success notification for an incomplete run. - Exclude SendMessageType.Notification from the UserPromptSubmit hook guard so background completion messages are not rewritten or blocked as if they were user input.
This commit is contained in:
parent
7c7d7c734c
commit
37d1924a3b
2 changed files with 75 additions and 16 deletions
|
|
@ -602,6 +602,7 @@ export class GeminiClient {
|
|||
if (
|
||||
messageType !== SendMessageType.Retry &&
|
||||
messageType !== SendMessageType.Cron &&
|
||||
messageType !== SendMessageType.Notification &&
|
||||
hooksEnabled &&
|
||||
messageBus &&
|
||||
this.config.hasHooksForEvent('UserPromptSubmit')
|
||||
|
|
|
|||
|
|
@ -1020,8 +1020,9 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
|
|||
// we set shouldAvoidPermissionPrompts so the tool scheduler
|
||||
// auto-denies 'ask' decisions — matching claw-code's approach.
|
||||
// PermissionRequest hooks still run and can override the denial.
|
||||
// Inherit from agentConfig so the resolved approval mode is preserved.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const bgConfig = Object.create(this.config) as any;
|
||||
const bgConfig = Object.create(agentConfig) as any;
|
||||
bgConfig.getShouldAvoidPermissionPrompts = () => true;
|
||||
|
||||
// Create a dedicated subagent that uses the bg-specific config.
|
||||
|
|
@ -1035,25 +1036,82 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
|
|||
try {
|
||||
await bgSubagent.execute(contextState, bgAbortController.signal);
|
||||
|
||||
// Fire SubagentStop hook in the background
|
||||
// Fire SubagentStop hook with blocking-decision loop (mirrors
|
||||
// foreground runSubagentWithHooks): if the hook blocks, feed the
|
||||
// reason back and re-execute up to maxIterations times.
|
||||
if (hookSystem && !bgAbortController.signal.aborted) {
|
||||
try {
|
||||
await hookSystem.fireSubagentStopEvent(
|
||||
hookOpts.agentId,
|
||||
hookOpts.agentType,
|
||||
this.config.getTranscriptPath(),
|
||||
bgSubagent.getFinalText(),
|
||||
false,
|
||||
resolvedMode,
|
||||
);
|
||||
} catch (hookError) {
|
||||
debugLogger.warn(
|
||||
`[Agent] Background SubagentStop hook failed: ${hookError}`,
|
||||
);
|
||||
const transcriptPath = this.config.getTranscriptPath();
|
||||
let stopHookActive = false;
|
||||
let continueExecution = true;
|
||||
let iterationCount = 0;
|
||||
const maxIterations = 5;
|
||||
|
||||
while (continueExecution) {
|
||||
iterationCount++;
|
||||
if (iterationCount >= maxIterations) {
|
||||
debugLogger.warn(
|
||||
`[Agent] Background SubagentStop hook reached maximum iterations (${maxIterations}), forcing stop`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const stopHookOutput = await hookSystem.fireSubagentStopEvent(
|
||||
hookOpts.agentId,
|
||||
hookOpts.agentType,
|
||||
transcriptPath,
|
||||
bgSubagent.getFinalText(),
|
||||
stopHookActive,
|
||||
resolvedMode,
|
||||
bgAbortController.signal,
|
||||
);
|
||||
|
||||
const typedStopOutput = stopHookOutput as
|
||||
| StopHookOutput
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
typedStopOutput?.isBlockingDecision() ||
|
||||
typedStopOutput?.shouldStopExecution()
|
||||
) {
|
||||
const continueReason = typedStopOutput.getEffectiveReason();
|
||||
stopHookActive = true;
|
||||
|
||||
const continueContext = new ContextState();
|
||||
continueContext.set('task_prompt', continueReason);
|
||||
await bgSubagent.execute(
|
||||
continueContext,
|
||||
bgAbortController.signal,
|
||||
);
|
||||
|
||||
if (bgAbortController.signal.aborted) {
|
||||
continueExecution = false;
|
||||
}
|
||||
} else {
|
||||
continueExecution = false;
|
||||
}
|
||||
} catch (hookError) {
|
||||
debugLogger.warn(
|
||||
`[Agent] Background SubagentStop hook failed, allowing stop: ${hookError}`,
|
||||
);
|
||||
continueExecution = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.complete(hookOpts.agentId, bgSubagent.getFinalText());
|
||||
// Report terminate mode: only GOAL counts as success. ERROR,
|
||||
// MAX_TURNS, and TIMEOUT are surfaced as failures so the parent
|
||||
// model (and the UI) don't treat incomplete runs as completed.
|
||||
const terminateMode = bgSubagent.getTerminateMode();
|
||||
const finalText = bgSubagent.getFinalText();
|
||||
if (terminateMode === AgentTerminateMode.GOAL) {
|
||||
registry.complete(hookOpts.agentId, finalText);
|
||||
} else {
|
||||
registry.fail(
|
||||
hookOpts.agentId,
|
||||
finalText || `Agent terminated with mode: ${terminateMode}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue