CodeAct is the one agent loop; Agent is retired
packages/agents/src/agent.ts runs a second loop next to CodeAct: TaskPlanner → ParallelTaskExecutor → TaskExecutor → CodeActExecutor → CompilerAgent, wrapped in a plan approval gate, a plan cache, a checkpoint store, per-phase model selection, and skill auto-select by objective word overlap. One command constructs it. Everything Agent alone wires is therefore reachable only from the CLI, while the loop the product ships — a CodeAct session over a gated belt — is what chat, plan mode, and every sub-agent run on. Two loops means every fix lands twice or in the wrong one: budgets, the permission ladder, transcript compaction, and the scheduler each have to be built for both, and the one nobody uses is where they rot. The repo made this call once when CodeAct replaced script mode (codeact-design.md); this applies it to the loop that survived.
D1. nodetool agent run becomes one CodeAct session over the belt chat uses, with create_plan and execute_plan reachable the way they are in chat. What goes with Agent: CompilerAgent, because synthesis is the parent loop’s next turn and it already reads task:<id> from memory; the plan approval gate, because the permission ladder classifies execute_plan as external and that classification is the approval; the plan cache and the checkpoint store, which no caller supplies; planningModel and reasoningModel, which no caller sets; and skill auto-select, whose replacement is the chat’s skill catalog plus load_skill. What stays: TaskPlanner, ParallelTaskExecutor, TaskExecutor, CodeActExecutor, SubAgentTool, and the supervisor. Chat plan mode runs on the planner and the parallel executor, so those are a product path rather than Agent’s scaffolding.
D2. The workflow AgentNode (packages/llm-nodes/src/nodes/agents.ts) stays a provider-native JSON tool loop, and joins the shared permission gate and the shared RunBudget. A node inside a saved graph wants a small loop a graph author can bound and predict, and provider-native tool calls give that with no sandbox in the path. Handing it the sandbox and the nodetool.* object model is a separate product question — what a node that runs arbitrary code means for a graph somebody else opens and runs — and it is not answered here.
Evidence
Every host that runs a model loop, and where each one is constructed:
| Host | Loop | Constructed in | Gate | Budget |
|---|---|---|---|---|
| Chat turn (web, Telegram, MCP) | one CodeAct session over provider.generateLoop |
packages/websocket/src/session/chat-turn.ts (createChatCodeActSession) |
gateTools |
createChatTurnBudget |
| Chat plan mode | create_plan → TaskPlanner.planMultiTask, execute_plan → ParallelTaskExecutor |
packages/agents/src/capabilities/agents.ts |
inherited belt | the run’s budget, via subAgentRuntime |
Workflow AgentNode |
JSON tool calls over provider.generateLoop |
packages/llm-nodes/src/nodes/agents.ts |
none | its own createRunBudget, or the caller’s via budgetFromContext |
CLI nodetool agent run |
Agent: TaskPlanner → ParallelTaskExecutor → TaskExecutor → CodeActExecutor → CompilerAgent |
packages/cli/src/commands/agent.ts |
none | none |
The call graph is one grep:
grep -rn "new Agent(" packages web electron --include=*.ts --include=*.tsx
It answers with packages/cli/src/commands/agent.ts and the packages/agents/tests/ suites that exercise Agent directly. There is no other production caller, so the CLI is the whole audience for what Agent wires.
The options only Agent passes are dead on the same evidence. checkpointStore and planCache are supplied at one site each, both inside agent.ts, and FileCheckpointStore.load resolves without reading anything until a loadFromDisk() that Agent never awaits, so a resumed run resumes from an empty store. planningModel and reasoningModel appear in agent.ts and task-planner.ts and are set by no caller, so the per-phase model selection they exist for has never selected a different model.
The plan approval gate is orphaned from the other side. PLAN_APPROVAL_CONTEXT_KEY has one reader, Agent.resolveApprovalCallback, and the websocket installs a callback under it (attachPlanApproval, packages/websocket/src/session/chat-history.ts) for an Agent that is never constructed in that process. Retiring Agent deletes the reader and the installer together.
Considered options
- Keep
Agentas a second supported host: rejected, this is the status quo and it is what makes each of A1, A2, A4, and A7 land twice. - Delete
TaskPlannerand the DAG executors withAgent: rejected,create_planandexecute_planrun on them from chat. - Keep
CompilerAgentas the synthesis step under CodeAct: rejected, it stores thefinish_steppayload without validating it againstoutputSchema, and the memory read that replaces it is already whatexecute_plandocuments. - Converge
AgentNodeon CodeAct in the same change: rejected, it changes what a node in a saved graph is allowed to do, which is D2’s separate question. - Give
AgentNodeits own gate and its own budget rather than the shared ones: rejected, a second classification table is exactly the duplication D1 removes.
Consequences
nodetool agent rungains the chat belt’s gate and budget, and loses the plan-first-then-execute shape it had by construction. The rewrite has to give that shape back — an objective that asks for a plan, or a permission mode the command exposes.- Plan approval moves from a bespoke callback to the permission ladder. A host with nobody to ask fails closed at
execute_planinstead of insideAgent. - The
planning_updatemessage type stays, becausecreate_planemits it. The compiler phases stop being produced, so the consumers that render them (packages/cli/src/useExecutionState.ts) lose those branches. AgentPolicy’s bounds have one consumer left,execute_plan. They either fold into the run budget or survive as the constants file the two executors read.- The memory contract is unchanged. “Final synthesis by
CompilerAgent” becomes “the calling loop readstask:<id>afterexecute_planreturns” inpackages/agents/AGENTS.mdanddocs/agent-memory.md. AgentNodestill needs the gate. Its budget already comes from the sharedRunBudget, its own or the calling run’s.