Skip to content

refactor: Unify Tool Execution Logic Across Interactive, Non-Interactive, and ACP Modes #3247

@LaZzyMan

Description

@LaZzyMan

name: 'Feature Request'
about: 'Suggest an idea for this project'
title: 'refactor: Unify Tool Execution Logic Across Interactive, Non-Interactive, and ACP Modes'
labels: ['type/feature-request', 'status/needs-triage', 'area/architecture']

What would you like to be added?

Refactor the tool execution architecture to unify the duplicated logic across three execution modes:

Current Architecture

The Qwen Code CLI currently has three execution modes with significantly different implementations:

Mode Entry Point Reasoning Loop Tool Scheduler Key Files
Interactive startInteractiveUI() AgentCore.runReasoningLoop() CoreToolScheduler agent-core.ts, coreToolScheduler.ts
Non-Interactive runNonInteractive() Custom while(true) loop CoreToolScheduler nonInteractiveCli.ts, nonInteractiveToolExecutor.ts
ACP Mode runAcpAgent() Session.#executePrompt() Direct tool.execute() acp-integration/session/Session.ts

Key Differences

  1. Tool Execution Path

    • Interactive & Non-Interactive: Both use CoreToolScheduler via executeToolCall()
    • ACP Mode: Directly calls tool.build().execute() in Session.runTool()
  2. Reasoning Loop

    • Interactive: Uses AgentCore.runReasoningLoop() from packages/core
    • Non-Interactive: Custom loop in nonInteractiveCli.ts
    • ACP Mode: Custom #executePrompt() in Session.ts
  3. Permission Handling (Duplicated in both CoreToolScheduler and Session.runTool()):

    // CoreToolScheduler.ts
    const finalPermission = await evaluatePermissionRules(pm, defaultPermission, ctx);
    
    // Session.ts (runTool)
    const { finalPermission, pmForcedAsk } = await evaluatePermissionRules(
      pm, defaultPermission, pmCtx
    );

Why is this needed?

Problems with Current Design

  1. Code Duplication

    • Permission handling logic is implemented separately in CoreToolScheduler and Session.runTool()
    • Tool execution flow (build → permission check → execute → record result) is duplicated
    • Event emission logic differs between modes
  2. Maintenance Burden

    • Bug fixes must be applied to multiple locations
    • New features (e.g., tool retry logic, timeout handling) need to be implemented 2-3 times
    • Tests must be written for each mode separately, tripling test maintenance
  3. Inconsistent Behavior

    • Risk of modes diverging in behavior over time
    • Some modes may receive bug fixes while others are forgotten
    • Documentation becomes harder to maintain
  4. Onboarding Difficulty

    • New contributors must understand three different code paths
    • Unclear which mode to modify when adding new features
    • Code reviews require checking multiple implementations

Real-World Impact

From git history analysis:

  • Commit 32e7b632b: "refactor: centralize IDE diff interaction in CoreToolScheduler" - This refactoring only affected Interactive/Non-Interactive modes, ACP mode still has separate logic
  • Permission handling comments in both files explicitly reference each other: "same as coreToolScheduler" - indicating awareness of duplication but no unified solution

Expected Goals After Refactoring

Primary Objectives

  1. Single Source of Truth for Tool Execution

    • Extract a unified ToolExecutor interface
    • All three modes call the same core execution logic
    • Mode-specific behavior handled via adapters/plugins
  2. Unified Permission Handling

    • Centralize L3/L4/L5 permission flow in one location
    • Remove duplicated evaluatePermissionRules calls
    • Consistent behavior across all modes
  3. Shared Reasoning Loop

    • Consider making AgentCore.runReasoningLoop() the single reasoning loop
    • ACP mode's #executePrompt() could delegate to AgentCore
    • Reduce custom loop implementations

Proposed Architecture

┌─────────────────────────────────────────────────────────┐
│                    Mode Entry Points                     │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐     │
│  │ Interactive  │ │Non-Interactive│ │     ACP      │     │
│  └──────┬───────┘ └──────┬───────┘ └──────┬───────┘     │
│         │                │                │              │
│         └────────────────┼────────────────┘              │
│                          │                               │
│                          ▼                               │
│         ┌────────────────────────────────┐              │
│         │    Unified Tool Executor       │              │
│         │  (refactored CoreToolScheduler)│              │
│         └────────────────┬───────────────┘              │
│                          │                               │
│                          ▼                               │
│         ┌────────────────────────────────┐              │
│         │      Tool.build().execute()    │              │
│         └────────────────────────────────┘              │
└─────────────────────────────────────────────────────────┘

Implementation Approach

Option A: Adapter Pattern (Recommended)

interface ToolExecutionAdapter {
  schedule(request: ToolCallRequestInfo, signal: AbortSignal): Promise<ToolCallResponseInfo>;
}

class CoreToolSchedulerAdapter implements ToolExecutionAdapter { ... }
class AcpToolExecutionAdapter implements ToolExecutionAdapter { ... }

Option B: Composition Pattern

// ACP Session.ts uses existing executeToolCall
const response = await executeToolCall(config, request, signal, {
  outputUpdateHandler: acpOutputHandler,
  onConfirm: acpPermissionHandler,
});

Success Criteria

  • All three modes use the same core tool execution logic
  • Permission handling is centralized (single evaluatePermissionRules call site)
  • Code duplication reduced by >70% (measured by line count in tool execution paths)
  • All existing tests pass without modification to test logic
  • New mode can be added with <100 lines of mode-specific code

Additional Context

Related Files

  • packages/core/src/core/coreToolScheduler.ts - Core tool scheduler for Interactive/Non-Interactive
  • packages/core/src/core/nonInteractiveToolExecutor.ts - Wrapper for non-interactive mode
  • packages/cli/src/acp-integration/session/Session.ts - ACP mode implementation (lines 700-1100: runTool method)
  • packages/core/src/agents/runtime/agent-core.ts - AgentCore reasoning loop

Historical Context

From git log analysis:

  • ACP integration was added later than the core tool scheduler
  • ACP mode chose independent implementation over refactoring to avoid breaking changes
  • Multiple refactoring commits (e.g., 32e7b632b) centralized logic for Interactive/Non-Interactive but didn't include ACP

Scope Considerations

This refactoring should be backwards compatible:

  • No changes to public APIs or CLI behavior
  • No changes to ACP protocol messages
  • Internal implementation only

Potential Risks

  • ACP mode has tight coupling with protocol-specific event emission
  • Permission flow has subtle differences (e.g., ACP's requestPermission RPC call)
  • Requires careful testing across all three modes

Priority: High - This technical debt accumulates with each new feature
Estimated Effort: 2-3 weeks for full refactoring with proper testing
Breaking Changes: None (internal refactoring only)

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions