Skip to content

refactor(core): decouple scheduler into orchestration, policy, and confirmation#16895

Merged
abhipatel12 merged 2 commits into
mainfrom
abhi/event-driven-tools.5
Jan 19, 2026
Merged

refactor(core): decouple scheduler into orchestration, policy, and confirmation#16895
abhipatel12 merged 2 commits into
mainfrom
abhi/event-driven-tools.5

Conversation

@abhipatel12
Copy link
Copy Markdown
Contributor

Summary

This PR refactors the Scheduler into a "Clean Sweep" architecture, decoupling
orchestration from business logic. It extracts policy enforcement and user
confirmation into dedicated, functional modules (policy.ts,
confirmation.ts), leaving Scheduler.ts as a pure event-driven orchestrator.
This eliminates race conditions, enforces strict type safety, and improves
testability.

This is a no-op PR, just splitting the code into different files. This code is
not being used anywhere currently.

Once this PR is merged, we will start using this code in the main agent's loop.

Details

  • Modularization: Created policy.ts for permission checks (YOLO, Allowed
    Tools, MCP scopes) and confirmation.ts for the interactive TUI/IDE loop.
  • Orchestration: Scheduler now delegates decision-making, focusing solely
    on queue management and execution lifecycle.
  • Memory Safety: Implemented WeakSet for MessageBus listener
    de-duplication.
  • Testing: Added comprehensive integration tests in scheduler.test.ts
    covering batch auto-approval, cancellation flows, and executor status
    handling.

Related Issues

Fixes #14306

How to Validate

  1. Run npm run test to verify the new test suite (scheduler.test.ts,
    policy.test.ts, confirmation.test.ts).
  2. Run npm run preflight to ensure build integrity.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

@abhipatel12 abhipatel12 requested a review from a team as a code owner January 16, 2026 22:37
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @abhipatel12, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant architectural overhaul to the core Scheduler component, separating its responsibilities into distinct modules for policy enforcement, user confirmation, and pure orchestration. This modular approach, dubbed 'Clean Sweep,' is designed to enhance the system's reliability, maintainability, and testability by isolating concerns and improving type safety. While the changes are foundational and not immediately active, they lay crucial groundwork for future integration into the agent's primary operational flow.

Highlights

  • Architectural Refactoring: The core Scheduler component has been refactored into a 'Clean Sweep' architecture, decoupling orchestration from business logic into dedicated modules.
  • New Policy Module: A new policy.ts module has been introduced to centralize permission checks, including YOLO, Allowed Tools, and MCP scopes.
  • Enhanced Confirmation Flow: The confirmation.ts module now handles the interactive TUI/IDE loop, allowing for user confirmation and modification of tool calls.
  • Pure Orchestration: The Scheduler.ts file is now a pure event-driven orchestrator, focusing solely on queue management and execution lifecycle, delegating decision-making to other modules.
  • Improved Robustness and Testability: The refactoring aims to eliminate race conditions, enforce strict type safety, and significantly improve the testability of the scheduler logic.
  • Comprehensive Testing: Extensive integration tests have been added for scheduler.test.ts, policy.test.ts, and confirmation.test.ts, covering various scenarios including batch auto-approval, cancellation flows, and executor status handling.
  • No-Op Initial Deployment: This PR is currently a no-op, meaning the refactored code is not yet actively used, but it sets the stage for its integration into the main agent's loop post-merge.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jan 16, 2026

Size Change: -2 B (0%)

Total Size: 23.2 MB

ℹ️ View Unchanged
Filename Size Change
./bundle/gemini.js 23.2 MB -2 B (0%)
./bundle/sandbox-macos-permissive-closed.sb 1.03 kB 0 B
./bundle/sandbox-macos-permissive-open.sb 890 B 0 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB 0 B
./bundle/sandbox-macos-restrictive-closed.sb 3.29 kB 0 B
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB 0 B
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB 0 B

compressed-size-action

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-executed refactoring of the scheduler, decoupling orchestration, policy, and confirmation logic into distinct modules. The new "Clean Sweep" architecture is a major improvement, enhancing modularity, testability, and type safety. The addition of comprehensive integration tests for the new scheduler, policy, and confirmation modules is excellent and provides strong confidence in the correctness of the changes.

My review includes one critical feedback item regarding error handling in the waitForConfirmation function, where a fire-and-forget promise could lead to silently swallowed errors and a confusing user experience. Addressing this will further improve the robustness of the new confirmation flow.

Comment on lines +241 to +274
async function waitForConfirmation(
messageBus: MessageBus,
correlationId: string,
signal: AbortSignal,
ideConfirmation?: Promise<DiffUpdateResult>,
): Promise<ConfirmationResult> {
if (ideConfirmation) {
// Pipe IDE resolution to MessageBus. This ensures that the coordinator
// correctly cleans up its listener when the IDE responds first.
void (async () => {
try {
const resolution = await ideConfirmation;
if (signal.aborted) return;

await messageBus.publish({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId,
confirmed: resolution.status === 'accepted',
outcome:
resolution.status === 'accepted'
? ToolConfirmationOutcome.ProceedOnce
: ToolConfirmationOutcome.Cancel,
payload: resolution.content
? { newContent: resolution.content }
: undefined,
});
} catch (error) {
debugLogger.warn('Error waiting for confirmation via IDE', error);
}
})();
}

return awaitConfirmation(messageBus, correlationId, signal);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The use of void (async () => {})() creates a fire-and-forget promise to handle the ideConfirmation. If the ideConfirmation promise rejects (e.g., due to an IDE-side error), the exception is caught and logged as a warning, but it is not propagated. This leads to a confusing user experience where an error in the IDE confirmation flow is silently ignored, and the CLI appears to hang while waiting for a TUI confirmation that the user may not be aware of.

To ensure robustness, errors from the IDE confirmation flow should be propagated. This can be achieved by using Promise.race to handle both the message bus listener and the IDE confirmation flow concurrently. This change ensures that if the IDE flow fails, the entire operation fails clearly, providing immediate feedback to the user.

async function waitForConfirmation(
  messageBus: MessageBus,
  correlationId: string,
  signal: AbortSignal,
  ideConfirmation?: Promise<DiffUpdateResult>,
): Promise<ConfirmationResult> {
  const busListener = awaitConfirmation(messageBus, correlationId, signal);

  if (!ideConfirmation) {
    return busListener;
  }

  // Race the message bus listener with the IDE confirmation handler.
  // This ensures that if the IDE confirmation promise rejects, the error
  // is propagated, rather than being silently ignored.
  const ideHandler = async (): Promise<never> => {
    const resolution = await ideConfirmation;
    if (signal.aborted) {
      // If the operation was cancelled (e.g., via TUI), do not proceed.
      // Return a promise that never resolves to let the busListener's
      // rejection win the race.
      return new Promise(() => {});
    }

    await messageBus.publish({
      type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
      correlationId,
      confirmed: resolution.status === 'accepted',
      outcome:
        resolution.status === 'accepted'
          ? ToolConfirmationOutcome.ProceedOnce
          : ToolConfirmationOutcome.Cancel,
      payload: resolution.content
        ? { newContent: resolution.content }
        : undefined,
    });

    // This promise should never resolve. Its purpose is to either reject if
    // `ideConfirmation` fails, or to hang and let `busListener` resolve
    // after we publish the message.
    return new Promise(() => {});
  };

  return Promise.race([busListener, ideHandler()]);
}

Comment thread packages/core/src/scheduler/scheduler.ts Outdated
Comment thread packages/core/src/scheduler/scheduler.ts Outdated
Comment thread packages/core/src/scheduler/scheduler.ts Outdated
tool,
response: createErrorResponse(
request,
e instanceof Error ? e : new Error(String(e)),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have getErrorMessage(), can we use that here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do, but that returns a string. Are you saying we should refactor createErrorResponse to take a string instead?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good point. I have a slight preference for encapsulating logic like this in the helper function but this also seems fine.

Comment thread packages/core/src/scheduler/scheduler.ts
if (ideConfirmation) {
// Pipe IDE resolution to MessageBus. This ensures that the coordinator
// correctly cleans up its listener when the IDE responds first.
void (async () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be fire and forgotten?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched it to use Promise.race to make it a bit clearer. lmk what you think

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that's better, though I'd still wonder if you could drop the .then() and .catch() as well and return a value from Promise.race() to determine what happens next.

Comment thread packages/core/src/scheduler/confirmation.ts Outdated
Copy link
Copy Markdown
Member

@gundermanc gundermanc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with some suggestions. Potential unresolved promise hang?

spanMetadata.input = requests;

if (this.isProcessing || this.state.isActive) {
return new Promise((resolve, reject) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing I'm realizing looks odd here is we're returning a new Promise<> inside of an async lambda.

Does that mean that the return type of the lambda is Promise<Promise<>>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ts should be flattening nested promises. I've extracted things out now. Let me know if this looks better now

@abhipatel12 abhipatel12 requested review from a team as code owners January 17, 2026 02:12
@abhipatel12 abhipatel12 force-pushed the abhi/event-driven-tools.5 branch from 539182b to c987704 Compare January 17, 2026 02:16
@gemini-cli gemini-cli Bot added the area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality label Jan 19, 2026
@abhipatel12 abhipatel12 enabled auto-merge January 19, 2026 23:11
@abhipatel12 abhipatel12 added this pull request to the merge queue Jan 19, 2026
payload: resolution.content
? { newContent: resolution.content }
: undefined,
}) as ConfirmationResult,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this cast necessary?

Merged via the queue into main with commit e2901f3 Jan 19, 2026
25 checks passed
@abhipatel12 abhipatel12 deleted the abhi/event-driven-tools.5 branch January 19, 2026 23:26
Mango-Lee-dev pushed a commit to Mango-Lee-dev/gemini-cli that referenced this pull request Jan 20, 2026
Thomas-Shephard pushed a commit to Thomas-Shephard/gemini-cli that referenced this pull request Jan 21, 2026
thacio added a commit to thacio/auditaria that referenced this pull request Jan 24, 2026
kuishou68 pushed a commit to iOfficeAI/gemini-cli-pro that referenced this pull request Feb 27, 2026
cocosheng-g pushed a commit that referenced this pull request May 6, 2026
@sripasg sripasg added the size/xl An extra large PR label Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality size/xl An extra large PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Subagent Tool Confirmation (HITL) Support

3 participants