Skip to content

Commit 9d9aa2f

Browse files
committed
Release v0.1.19: complete plan approval resume mechanism
- Add pending plan state tracking to CodeAgent for deferred approvals - Add try_handle_plan_response() and parse_plan_decision() for non-interactive plan resume - Update REPL and TUI to detect pending plan approvals on next turn - Add 15 integration tests for full plan->approval->execution cycle - Update README with plan mode, subagent orchestration, and setup docs - Apply ruff formatting fixes across codebase
1 parent b291961 commit 9d9aa2f

22 files changed

Lines changed: 562 additions & 73 deletions

README.md

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,55 @@ Agentic coding CLI for [NVIDIA NIM](https://build.nvidia.com). Reads your code,
66
77
## Install
88

9-
From source (editable):
9+
From PyPI:
1010
```bash
11-
pip install -e .
11+
pip install nemocode
1212
```
1313

14-
Or from PyPI:
14+
Or from source (editable):
1515
```bash
16-
pip install nemocode
16+
pip install -e .
1717
```
1818

19-
## Setup
19+
## Quick Start
2020

2121
Run the guided setup wizard:
2222

2323
```bash
2424
nemo setup
2525
```
2626

27-
It defaults to hosted NVIDIA NIM, prompts for `NVIDIA_API_KEY`, and can also configure a local `vllm` or `sglang` endpoint and model for you.
27+
The wizard defaults to **hosted NVIDIA NIM**, prompts for `NVIDIA_API_KEY`, and can also configure a local `vllm` or `sglang` backend for you.
28+
29+
### Hosted NVIDIA NIM (default)
2830

29-
If you just want hosted NIM manually, get a free API key from [build.nvidia.com](https://build.nvidia.com):
31+
Get a free API key from [build.nvidia.com](https://build.nvidia.com):
3032

3133
```bash
3234
export NVIDIA_API_KEY="nvapi-..."
3335
nemo code
3436
```
3537

36-
Hosted Nemotron/NIM endpoints in NeMoCode use `NVIDIA_API_KEY` by default.
38+
Hosted Nemotron endpoints use `NVIDIA_API_KEY` by default. The setup wizard can store it in your system keyring.
3739

38-
Or serve a model locally with [vLLM](https://docs.vllm.ai/) or [SGLang](https://sgl-project.github.io/) on any NVIDIA GPU:
40+
### Local vLLM or SGLang
41+
42+
Serve a model locally on any NVIDIA GPU:
3943

4044
```bash
4145
# vLLM
4246
vllm serve nvidia/NVIDIA-Nemotron-Nano-9B-v2 \
43-
--trust-remote-code --mamba_ssm_cache_dtype float32 \
44-
--enable-auto-tool-choice \
45-
--tool-parser-plugin nemotron_toolcall_parser.py \
46-
--tool-call-parser nemotron_json
47+
--host 0.0.0.0 --port 8000
4748
nemo code -e local-vllm-nano9b
4849

4950
# SGLang (best for Nemotron 3 Super long context on DGX Spark)
5051
python -m sglang.launch_server \
5152
--model nvidia/nemotron-3-super-120b-a12b \
52-
--quantization nvfp4 --trust-remote-code
53-
nemo code -e spark-sglang-super
53+
--host 0.0.0.0 --port 8000
54+
nemo code -e local-sglang-super
5455
```
5556

56-
No GPU? Rent one via [Brev](https://console.brev.dev) — L40S from $1.03/hr:
57+
No GPU? Rent one via [Brev](https://console.brev.dev):
5758

5859
```bash
5960
nemo setup brev
@@ -67,8 +68,32 @@ nemo code "fix the bug in auth.py" -y # one-shot, auto-approve tools
6768
nemo chat "explain this error" # chat, no tools
6869
cat log.txt | nemo code "diagnose" # pipe input
6970
nemo code -f super-nano "refactor" # multi-model formation
71+
nemo code --tui # full-screen TUI
72+
```
73+
74+
## Plan Mode
75+
76+
Plan mode is a read-only planning phase with an approval gate before execution.
77+
78+
- **Read-only**: Plan mode only reads files, searches code, and explores — no writes, shell commands, or commits.
79+
- **Approval gate**: The planner proposes a concrete plan. You review and approve, revise with feedback, or cancel.
80+
- **Execution**: Once approved, a build agent executes the plan with full tool access.
81+
82+
Switch modes in the REPL with Tab or `/mode`:
83+
84+
| Mode | Behavior |
85+
|------|----------|
86+
| `code` | Ask before tool calls (default) |
87+
| `plan` | Read-only planning + approval gate |
88+
| `auto` | Auto-approve everything |
89+
90+
Launch directly in plan mode:
91+
```bash
92+
nemo code --agent plan "implement user auth"
7093
```
7194

95+
The plan agent can also spawn read-only research subagents to help with exploration.
96+
7297
## Endpoints
7398

7499
Works with any OpenAI-compatible API. Pre-configured:
@@ -78,7 +103,7 @@ Works with any OpenAI-compatible API. Pre-configured:
78103
| `nim-super` | Nemotron 3 Super (12B/120B MoE, 1M ctx) | NIM API key |
79104
| `nim-nano` | Nemotron 3 Nano (3B/30B MoE, 1M ctx) | NIM API key |
80105
| `nim-nano-9b` | Nemotron Nano 9B v2 | NIM API key |
81-
| `nim-nano-4b` | Nemotron Nano 4B v1.1 (new!) | NIM API key |
106+
| `nim-nano-4b` | Nemotron Nano 4B v1.1 | NIM API key |
82107
| `nim-vlm` | Nemotron Nano 12B VL (vision) | NIM API key |
83108
| `nim-embed` | Nemotron Embed 1B v2 | NIM API key |
84109
| `nim-rerank` | Nemotron Rerank 1B v2 | NIM API key |
@@ -105,18 +130,32 @@ nemo code -f super-nano "implement caching"
105130
| `vision` | VLM reads screenshots, Super writes code |
106131
| `local` | Nano on local GPU, no internet needed |
107132

108-
## Agents
133+
## Agents & Sub-agent Orchestration
109134

110-
NeMoCode now supports named agent profiles for top-level sessions and delegated sub-agents.
135+
NeMoCode supports named agent profiles for top-level sessions and delegated sub-agents.
111136

112-
- Built-in primary agents: `build`, `plan`
113-
- Built-in sub-agents: `general`, `explore`, `review`, `debug`, `test`, `doc`, `code-search`, `fast`
137+
- **Primary agents**: `build` (default full-access), `plan` (read-only planning)
138+
- **Sub-agents**: `general`, `explore`, `review`, `debug`, `test`, `doc`, `code-search`, `fast`
114139
- Inspect them with `nemo agent ls` and `nemo agent show <name>`
115140
- Switch primary agents with `nemo code --agent <name>` or `/agent <name>` in the REPL/TUI
116-
- Sub-agent orchestration tools are now available in coding sessions: `delegate`, `spawn_agent`, `wait_agent`, `close_agent`, and `resume_agent`
117-
- Define custom agents in `.nemocode.yaml` under `agents:` or in markdown files under `.nemocode/agents/*.md`
118141

119-
Example markdown agent:
142+
### Sub-agent tools
143+
144+
In coding sessions, these orchestration tools are available:
145+
146+
| Tool | Purpose |
147+
|------|---------|
148+
| `delegate` | Spawn a sub-agent and wait for the result |
149+
| `spawn_agent` | Spawn a background sub-agent for parallel work |
150+
| `wait_agent` | Wait for a spawned sub-agent to finish |
151+
| `close_agent` | Close or cancel a sub-agent handle |
152+
| `resume_agent` | Reopen a previously closed sub-agent handle |
153+
154+
Sub-agents inherit read-only mode when delegated from plan mode.
155+
156+
### Custom agents
157+
158+
Define custom agents in `.nemocode.yaml` under `agents:` or as markdown files under `.nemocode/agents/*.md`:
120159

121160
```markdown
122161
---
@@ -134,7 +173,7 @@ tools:
134173
Review the requested changes. Focus on correctness, regressions, and missing tests.
135174
```
136175

137-
## Local GPU setup
176+
## Setup Commands
138177

139178
```bash
140179
nemo setup # guided wizard
@@ -146,7 +185,7 @@ nemo setup nim # NIM container guide
146185
nemo setup brev # rent a cloud GPU
147186
```
148187

149-
## More commands
188+
## More Commands
150189

151190
```bash
152191
nemo endpoint ls / test # manage endpoints
@@ -157,7 +196,7 @@ nemo hardware recommend # GPU-based recommendations
157196
nemo doctor # run diagnostics to check setup
158197
nemo session ls # past conversations
159198
nemo obs pricing # token pricing
160-
nemo init # create .nemocode.yaml without overriding your user default endpoint
199+
nemo init # create .nemocode.yaml without overriding user defaults
161200
```
162201

163202
## Contributing

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "nemocode"
3-
version = "0.1.18"
3+
version = "0.1.19"
44
description = "Terminal-first control plane for NVIDIA Nemotron 3 — agentic coding, RAG, doc-ops, and multi-model formations."
55
readme = "README.md"
66
requires-python = ">=3.11"

src/nemocode/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
"""NeMoCode — Terminal-first agentic coding CLI for NVIDIA Nemotron 3."""
55

6-
__version__ = "0.1.18"
6+
__version__ = "0.1.19"

src/nemocode/cli/commands/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from rich.console import Console
1010
from rich.table import Table
1111

12-
from nemocode.config.agents import resolve_agent_reference
1312
from nemocode.config import load_config
13+
from nemocode.config.agents import resolve_agent_reference
1414
from nemocode.config.schema import AgentMode
1515
from nemocode.core.subagents import list_runs
1616
from nemocode.tools.delegate import _pick_endpoint

src/nemocode/cli/commands/init_cmd.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ def init_cmd(
3434
name: str = typer.Option(None, "--name", help="Project name"),
3535
force: bool = typer.Option(False, "--force", help="Overwrite existing config"),
3636
endpoint: str = typer.Option(None, "--endpoint", help="Project-specific default endpoint"),
37-
formation: str = typer.Option(
38-
None, "--formation", help="Project-specific active formation"
39-
),
37+
formation: str = typer.Option(None, "--formation", help="Project-specific active formation"),
4038
) -> None:
4139
"""Create a .nemocode.yaml project config in the current directory."""
4240
config_path = Path.cwd() / ".nemocode.yaml"

src/nemocode/cli/commands/repl.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,9 @@ def _cmd_agent(self, arg: str) -> bool:
717717
agent = primary_agents.get(resolved)
718718
if agent is None:
719719
available = ", ".join(sorted(primary_agents.keys()))
720-
console.print(f"[red]Unknown primary agent: {arg}[/red]\n[dim]Available: {available}[/dim]")
720+
console.print(
721+
f"[red]Unknown primary agent: {arg}[/red]\n[dim]Available: {available}[/dim]"
722+
)
721723
return True
722724

723725
self._state.agent_name = resolved
@@ -1665,6 +1667,14 @@ async def _ask_user_interactive(question: str, options: list[str]) -> str:
16651667
renderer.finalize()
16661668
_render_status_bar(state)
16671669

1670+
# Check if plan approval is pending after the turn
1671+
if state.agent.has_pending_plan:
1672+
console.print(
1673+
"\n[yellow]Plan awaiting your approval."
1674+
" Reply with [bold]approve[/bold], [bold]revise[/bold],"
1675+
" or [bold]cancel[/bold].[/yellow]"
1676+
)
1677+
16681678
# Auto-save session after each turn
16691679
_auto_save_session(state)
16701680
finally:
@@ -1686,7 +1696,31 @@ async def _run_turn(state: _ReplState, user_input: str, renderer: _TurnRenderer)
16861696
16871697
Handles Ctrl+C gracefully: sets the cancelled flag and drains remaining events
16881698
rather than raising, so the session stays in a consistent state.
1699+
Also handles resume of pending plan approvals.
16891700
"""
1701+
# Check if we're resuming a pending plan approval
1702+
if state.agent.has_pending_plan:
1703+
result = await state.agent.try_handle_plan_response(user_input)
1704+
if result is not None:
1705+
# User input was a recognized plan decision — handle it
1706+
renderer.start_thinking("Processing plan decision")
1707+
try:
1708+
async for event in result:
1709+
if state.cancelled:
1710+
continue
1711+
try:
1712+
renderer.render_event(event)
1713+
except KeyboardInterrupt:
1714+
state.cancel()
1715+
console.print("\n[dim]Cancelling...[/dim]")
1716+
continue
1717+
finally:
1718+
pass
1719+
return
1720+
# Not a plan decision — clear pending state and proceed as normal input
1721+
state.agent._pending_plan_text = None
1722+
state.agent._pending_plan_user_input = None
1723+
16901724
# Start a unified thinking spinner via the renderer.
16911725
# It persists through read-only tool execution, updating with progress,
16921726
# and stops automatically when the text response begins.

src/nemocode/cli/render.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -833,9 +833,7 @@ def summarize_delegate_result(parsed: dict) -> tuple[str, str | None] | None:
833833
preview = str(parsed.get("output") or "").strip()
834834
else:
835835
preview = str(
836-
parsed.get("last_output_preview")
837-
or parsed.get("output_preview")
838-
or ""
836+
parsed.get("last_output_preview") or parsed.get("output_preview") or ""
839837
).strip()
840838
if preview:
841839
preview = preview.splitlines()[0].strip()

src/nemocode/cli/tui.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
summarize_delegate_result,
4343
tool_result_has_embedded_error,
4444
)
45-
from nemocode.config.agents import resolve_agent_reference
4645
from nemocode.config import load_config
46+
from nemocode.config.agents import resolve_agent_reference
4747
from nemocode.config.schema import AgentMode, NeMoCodeConfig
4848
from nemocode.core.context import ContextManager
4949
from nemocode.core.metrics import MetricsCollector, RequestMetrics
@@ -1188,6 +1188,28 @@ async def _run_agent_turn(self, user_input: str) -> None:
11881188
self.post_message(TurnComplete(error="No agent configured"))
11891189
return
11901190

1191+
# Check if we're resuming a pending plan approval
1192+
if agent.has_pending_plan:
1193+
result = await agent.try_handle_plan_response(user_input)
1194+
if result is not None:
1195+
try:
1196+
async for event in result:
1197+
if self._state.cancelled:
1198+
continue
1199+
self.post_message(AgentEventMessage(event))
1200+
except asyncio.CancelledError:
1201+
self.post_message(TurnComplete(error=None))
1202+
return
1203+
except Exception as exc:
1204+
logger.exception("Plan approval handling failed")
1205+
self.post_message(TurnComplete(error=str(exc)))
1206+
return
1207+
self.post_message(TurnComplete())
1208+
return
1209+
# Not a plan decision — clear pending and proceed normally
1210+
agent._pending_plan_text = None
1211+
agent._pending_plan_user_input = None
1212+
11911213
try:
11921214
async for event in agent.run(user_input):
11931215
if self._state.cancelled:
@@ -1299,6 +1321,10 @@ def _on_turn_complete(self, msg: TurnComplete) -> None:
12991321
except NoMatches:
13001322
pass
13011323

1324+
# Check if plan approval is pending
1325+
if self._state.agent and self._state.agent.has_pending_plan:
1326+
chat.add_system("Plan awaiting your approval. Reply with approve, revise, or cancel.")
1327+
13021328
def _show_turn_summary(self, chat: ChatScroll) -> None:
13031329
"""Show a compact performance summary after a turn completes."""
13041330
s = self._state

src/nemocode/config/agents.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
from __future__ import annotations
77

8-
from copy import deepcopy
98
import os
9+
from copy import deepcopy
1010
from pathlib import Path
1111
from typing import TYPE_CHECKING, Any
1212

@@ -47,17 +47,26 @@
4747
"mode": "primary",
4848
"role": "planner",
4949
"tools": [
50-
"fs_read", "git_read", "rg", "glob", "clarify",
51-
"delegate", "spawn_agent", "wait_agent", "close_agent", "resume_agent"
50+
"fs_read",
51+
"git_read",
52+
"rg",
53+
"glob",
54+
"clarify",
55+
"delegate",
56+
"spawn_agent",
57+
"wait_agent",
58+
"close_agent",
59+
"resume_agent",
5260
],
5361
"prompt": (
5462
"You are NeMoCode in plan mode. Read the codebase, analyze the task, "
5563
"and propose concrete next steps without modifying files or running "
56-
"destructive commands. Use ask_user / ask_clarify only when you are blocked on missing requirements. "
57-
"You can also spawn read-only research subagents to help with exploration. "
58-
"When you have a plan, present it clearly for approval. The controller will handle approval, revision, "
59-
"or cancellation after you respond. If the plan is revised, incorporate the user's feedback and return "
60-
"only the updated plan."
64+
"destructive commands. Use ask_clarify only when blocked on requirements. "
65+
"You can also spawn read-only research subagents for exploration. "
66+
"When you have a plan, present it clearly for approval. "
67+
"The controller handles approval, revision, or cancellation after you "
68+
"respond. If revised, incorporate the feedback and return only the "
69+
"updated plan."
6170
),
6271
},
6372
"general": {

src/nemocode/core/first_run.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
from __future__ import annotations
1111

1212
import os
13-
from pathlib import Path
1413
import sys
14+
from pathlib import Path
1515

16+
import typer
1617
from rich.console import Console
1718
from rich.panel import Panel
1819
from rich.table import Table
19-
import typer
2020

2121
from nemocode import __version__
2222
from nemocode.config import ensure_config_dir

0 commit comments

Comments
 (0)