Skip to content

Consolidate scheduler creation and todo continuation into the engine; remove CLI compensating layers #2657

Description

@acoliver

Problem

The agentic turn loop lives inside the engine (AgenticLoop in packages/agents), and AgentImpl.stream() delegates directly to it. This was the goal of the core API refactoring (#2034, #2372, #1595). However, the CLI still has compensating layers that duplicate engine-owned responsibilities ? and these layers only exist in the CLI's React app, not in ACP/Zed or other consumers.

This divergence was discovered while fixing #2653 (ACP/Zed: task tool crash + todo_pause loop-break). Two specific divergences were found:

1. Scheduler creation: CLI has its own factory, ACP uses the engine fallback

The CLI registers a schedulerFactoryProvider via interactiveToolScheduler.ts that wraps scheduler.schedule in a proper closure. ACP/Zed does not register one, so the subagent falls back to initInteractiveScheduler() in packages/agents/src/core/subagentExecution.ts. That fallback had a receiver-loss bug (schedule: scheduler.schedule without binding this) that the CLI's factory masked. We fixed the symptom in #2655, but the root cause is that scheduler creation is not fully consolidated in the engine ? each consumer shouldn't need to provide its own correctly-binding factory.

Files involved:

  • packages/agents/src/core/subagentExecution.ts ? engine fallback (now fixed)
  • packages/cli/src/runtime/interactiveToolScheduler.ts ? CLI's separate factory
  • packages/core/src/config/toolRegistryFactory.ts ? getInteractiveSubagentSchedulerFactory() indirection

2. Todo continuation: CLI has a React-layer loop on top of the engine loop

The CLI's useTodoContinuation.ts hook fires a brand new agent.stream() call after the engine loop finishes, if there are pending todos. This is a second, outer continuation loop that overlaps with what should be engine-owned behavior. It also has its own todoPaused gate (useTodoPausePreserver.ts) that was masking the fact that AgenticLoop.buildNextMessage() didn't stop on todo_pause.

We fixed AgenticLoop.buildNextMessage() to check for successful todo_pause (#2655), but the CLI still has the redundant React continuation layer. ACP/Zed has no such layer and relies entirely on the engine.

Files involved:

  • packages/cli/src/ui/hooks/useTodoContinuation.ts ? React continuation loop (fires new agent.stream())
  • packages/cli/src/ui/hooks/useTodoPausePreserver.ts ? React pause gate
  • packages/cli/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts ? orchestration
  • packages/agents/src/core/TodoContinuationService.ts ? engine-side service (exists but partially bypassed)
  • packages/agents/src/core/MessageStreamOrchestrator.ts ? checks todoPauseSeen but doesn't see real tool responses from AgenticLoop

3. MessageStreamOrchestrator pause detection is wired to the wrong layer

MessageStreamOrchestrator checks for todo_pause via ToolCallResponse events from Turn.run(), but the real Turn only emits ToolCallRequest events. The actual tool responses come back later from AgenticLoop's scheduler ? which MessageStreamOrchestrator never sees. This means the orchestrator's todoPauseSeen detection only works in tests that mock Turn.run() to emit synthetic ToolCallResponse events. The existing MessageStreamOrchestrator.todoPause.test.ts tests mock this path and don't reflect real execution.

Context: Prior refactoring work

This is a continuation of the multi-issue effort to move the agentic loop and all engine behavior into the packages/agents package:

The engine loop is in the right place. What remains is removing the CLI's duplicated/overlapping layers so there is one continuation and scheduling path for all consumers.

Proposed direction

  1. Consolidate scheduler creation into the engine. The initInteractiveScheduler() fallback in subagentExecution.ts should be the canonical path. The CLI's interactiveToolScheduler.ts factory should be removed or reduced to configuration only (not method-binding). No consumer should need to know that scheduler.schedule requires a closure to preserve this.

  2. Move todo continuation into the engine. The React useTodoContinuation hook's logic (checking for pending todos, generating continuation prompts, deciding whether to continue) should move into the engine ? either into AgenticLoop itself or into a dedicated continuation controller that AgentImpl.stream() uses. The CLI should become a pure event consumer.

  3. Fix the MessageStreamOrchestrator pause detection. Either route real tool responses through the orchestrator, or move the pause check entirely into AgenticLoop.buildNextMessage() (where our Fix ACP/Zed: task tool crash + todo_pause loop-break (Closes #2653) #2655 fix already partially lives). The orchestrator's existing tests need to reflect real execution paths, not mocked ToolCallResponse events.

  4. Remove or deprecate the React-layer pause gate. useTodoPausePreserver.ts and the todoPaused state in useTodoContinuation.ts should become unnecessary once the engine authoritatively handles pause.

  5. Add integration tests that exercise the full agent.stream() ? AgenticLoop path for both CLI and ACP-style consumers, so behavioral parity is enforced.

Why 0.11.0

This is an architecture refactor that touches multiple packages. It should not block 0.10.0 feature work. The immediate bugs from the divergence were fixed in #2655 (merged to 0.10.0). This issue tracks the full consolidation.

@coderabbitai please create a plan

Metadata

Metadata

Assignees

No one assigned

    Labels

    IDE IntegrationVSCode/Zed integration issues etc.architectureArchitectural refactor / clean designsubagentsasync / synch / other types of subagents.

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions