Skip to content

Commit f014b57

Browse files
committed
feat(pipeline): add interactive resume/replay with stage reuse and regeneration
- Introduce resume module to replay previous pipeline runs - Support interactive run selection and per-stage output reuse - Enable resuming from a specific stage (e.g. writer) - Load consensus and writer outputs from existing files when reused - Generate new regen run directories without overwriting original data - Integrate resume flow into main entrypoint with --resume and --from-stage - Update graph execution to skip reused stages and continue from checkpoints - Improve debug output handling to support resumed and regenerated runs
1 parent 1edd47a commit f014b57

File tree

8 files changed

+694
-25
lines changed

8 files changed

+694
-25
lines changed

docs/pages/mindmaps/neetcode_ontology_agent_evolved_zh-TW.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
document.addEventListener('DOMContentLoaded', function() {
6060
const { Transformer, Markmap } = window.markmap;
6161
const transformer = new Transformer();
62-
const markdown = `I'm sorry, but it seems that the content to be translated is missing. Could you please provide the specific Markmap content that needs to be translated into Traditional Chinese (Taiwan)?`;
62+
const markdown = `I'm sorry, but it seems like the content to translate was not provided. Could you please provide the content that needs to be translated?`;
6363
const { root } = transformer.transform(markdown);
6464
const svg = d3.select('.markmap').append('svg');
6565
const mm = Markmap.create(svg.node(), { color: (node) => node.payload?.color || '#f59e0b' }, root);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Resume / Replay Mode Usage Guide
2+
3+
## Overview
4+
5+
Resume mode allows you to continue execution from a previous pipeline run, supporting:
6+
- Reusing completed stage outputs (saves tokens and time)
7+
- Re-running from a specific stage (debug-friendly)
8+
- Not overwriting original run data (generates new regen run)
9+
10+
## Usage
11+
12+
### Method 1: Interactive Resume Mode
13+
14+
```bash
15+
python main.py --resume
16+
```
17+
18+
After startup, it will:
19+
1. Scan all previous runs under `outputs/debug/`
20+
2. Display them sorted by time (newest first)
21+
3. Let you select the run to resume
22+
4. Ask whether to reuse each stage's output one by one
23+
24+
### Method 2: Start from a Specific Stage
25+
26+
```bash
27+
python main.py --resume --from-stage writer
28+
```
29+
30+
This will automatically:
31+
- Select the latest run
32+
- Reuse outputs from `expert_review`, `full_discussion`, `consensus`
33+
- Re-run from the `writer` stage
34+
35+
Supported stages:
36+
- `expert_review`
37+
- `full_discussion`
38+
- `consensus`
39+
- `writer`
40+
- `translate`
41+
- `post_process`
42+
43+
## Example Workflows
44+
45+
### Scenario 1: Writer has no output, want to re-run
46+
47+
```bash
48+
# 1. List available runs
49+
python main.py --resume
50+
51+
# 2. Select the latest run (e.g., run_20251215_111303)
52+
53+
# 3. Ask whether to reuse each stage:
54+
# - expert_review: [y] (reuse)
55+
# - full_discussion: [y] (reuse)
56+
# - consensus: [y] (reuse)
57+
# - writer: [n] (regenerate)
58+
59+
# 4. Pipeline will:
60+
# - Skip expert_review, full_discussion, consensus
61+
# - Re-run writer
62+
# - Save output to run_20251215_111303_regen_1/
63+
```
64+
65+
### Scenario 2: Only want to re-run writer
66+
67+
```bash
68+
python main.py --resume --from-stage writer
69+
```
70+
71+
Automatically reuses outputs from all previous stages, only re-runs writer.
72+
73+
## Run Naming Rules
74+
75+
- **Original run**: `run_YYYYMMDD_HHMMSS/`
76+
- **Resume from original run**: `run_YYYYMMDD_HHMMSS_regen_1/`
77+
- **Resume again**: `run_YYYYMMDD_HHMMSS_regen_2/`
78+
79+
**Important**: Original run data is never overwritten, all new outputs are in regen directories.
80+
81+
## State Loading
82+
83+
The system automatically loads:
84+
-**Consensus data**: Loaded from JSON file (if reusing consensus stage)
85+
-**Writer output**: Loaded from writer output file (if reusing writer stage)
86+
- ⚠️ **Expert responses**: Currently only marked as reused, incomplete recovery (needs improvement)
87+
88+
## Debug Output
89+
90+
All intermediate outputs (including reused and regenerated ones) are saved in the new regen run directory for easy comparison and debugging.
91+
92+
## Notes
93+
94+
1. **Ensure debug_output.enabled = true**: Resume mode depends on debug output
95+
2. **API Keys**: Still need to provide API keys (even when reusing stages)
96+
3. **Configuration consistency**: Resume uses current config, which may differ from original run
97+
4. **Partial state recovery**: Currently only partial state recovery is supported, some stages may need to be re-run
98+
99+
## Future Improvements
100+
101+
- [ ] Complete state serialization/deserialization
102+
- [ ] Support resuming from any intermediate state
103+
- [ ] Automatically detect failed stages and suggest resume
104+
- [ ] Support comparing outputs from different runs

tools/ai-markmap-agent/main.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
)
3333
from src.data_sources import DataSourcesLoader
3434
from src.graph import run_pipeline, load_baseline_markmap, handle_versioning_mode
35+
from src.resume import (
36+
scan_previous_runs,
37+
select_run_interactive,
38+
ask_reuse_stage,
39+
RunInfo,
40+
)
3541

3642

3743
def print_banner() -> None:
@@ -151,6 +157,17 @@ def main() -> int:
151157
action="store_true",
152158
help="Enable verbose output"
153159
)
160+
parser.add_argument(
161+
"--resume",
162+
action="store_true",
163+
help="Resume from a previous run (interactive mode)"
164+
)
165+
parser.add_argument(
166+
"--from-stage",
167+
type=str,
168+
choices=["expert_review", "full_discussion", "consensus", "writer", "translate", "post_process"],
169+
help="Start from a specific stage (requires --resume)"
170+
)
154171

155172
args = parser.parse_args()
156173

@@ -163,6 +180,60 @@ def main() -> int:
163180
config = load_config(args.config)
164181
print(" ✓ Configuration loaded\n")
165182

183+
# Step 1.5: Resume mode selection
184+
resume_config = None
185+
if args.resume or args.from_stage:
186+
print("\n" + "=" * 60)
187+
print("Resume Mode")
188+
print("=" * 60)
189+
190+
# Scan for previous runs
191+
debug_config = config.get("debug_output", {})
192+
debug_output_dir = Path(debug_config.get("output_dir", "outputs/debug"))
193+
runs = scan_previous_runs(debug_output_dir)
194+
195+
if not runs:
196+
print("\n ⚠ No previous runs found")
197+
if args.resume:
198+
print(" Starting fresh run instead...\n")
199+
else:
200+
return 1
201+
else:
202+
# Let user select a run
203+
selected_run = select_run_interactive(runs)
204+
if not selected_run:
205+
print("\n ⚠ Cancelled")
206+
return 0
207+
208+
print(f"\n ✓ Selected: {selected_run.run_id}")
209+
210+
# Determine which stages to reuse
211+
reuse_stages = {}
212+
stages = ["expert_review", "full_discussion", "consensus", "writer"]
213+
214+
# If --from-stage is specified, reuse everything before that stage
215+
if args.from_stage:
216+
stage_idx = stages.index(args.from_stage) if args.from_stage in stages else -1
217+
if stage_idx >= 0:
218+
for i in range(stage_idx):
219+
if selected_run.has_stage_output(stages[i]):
220+
reuse_stages[stages[i]] = True
221+
print(f" → Will start from stage: {args.from_stage}")
222+
if reuse_stages:
223+
print(f" → Will reuse stages: {', '.join(reuse_stages.keys())}")
224+
else:
225+
# Interactive: ask for each stage
226+
print("\n Select which stages to reuse:")
227+
for stage in stages:
228+
if selected_run.has_stage_output(stage):
229+
should_reuse = ask_reuse_stage(stage, selected_run)
230+
reuse_stages[stage] = should_reuse
231+
232+
resume_config = {
233+
"run_dir": str(selected_run.run_dir),
234+
"reuse_stages": reuse_stages,
235+
}
236+
166237
# Print workflow summary
167238
print_workflow_summary(config)
168239

@@ -215,6 +286,10 @@ def main() -> int:
215286
# Add baseline to data
216287
data["baseline_markmap"] = baseline_markmap
217288

289+
# Add resume config if available
290+
if resume_config:
291+
data["_resume_config"] = resume_config
292+
218293
# Print summary
219294
print_data_summary(loader.get_summary())
220295

tools/ai-markmap-agent/outputs/versions/v1/neetcode_ontology_agent_evolved_zh-TW.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
document.addEventListener('DOMContentLoaded', function() {
6060
const { Transformer, Markmap } = window.markmap;
6161
const transformer = new Transformer();
62-
const markdown = `I'm sorry, but it seems that the content to be translated is missing. Could you please provide the specific Markmap content that needs to be translated into Traditional Chinese (Taiwan)?`;
62+
const markdown = `I'm sorry, but it seems like the content to translate was not provided. Could you please provide the content that needs to be translated?`;
6363
const { root } = transformer.transform(markdown);
6464
const svg = d3.select('.markmap').append('svg');
6565
const mm = Markmap.create(svg.node(), { color: (node) => node.payload?.color || '#f59e0b' }, root);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
I'm sorry, but it seems that the content to be translated is missing. Could you please provide the specific Markmap content that needs to be translated into Traditional Chinese (Taiwan)?
1+
I'm sorry, but it seems like the content to translate was not provided. Could you please provide the content that needs to be translated?

tools/ai-markmap-agent/src/debug_output.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ class DebugOutputManager:
1919
Saves intermediate outputs to help with debugging and verification.
2020
"""
2121

22-
def __init__(self, config: dict[str, Any] | None = None):
22+
def __init__(self, config: dict[str, Any] | None = None, run_dir: Path | str | None = None):
2323
"""
2424
Initialize the debug output manager.
2525
2626
Args:
2727
config: Configuration dictionary
28+
run_dir: Optional run directory path (for resume mode).
29+
If provided, uses this directory instead of creating new one.
2830
"""
2931
from .config_loader import ConfigLoader
3032

@@ -40,11 +42,18 @@ def __init__(self, config: dict[str, Any] | None = None):
4042
if self.enabled:
4143
self.output_dir.mkdir(parents=True, exist_ok=True)
4244

43-
# Create run-specific directory with timestamp
44-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
45-
self.run_dir = self.output_dir / f"run_{timestamp}"
46-
self.run_dir.mkdir(parents=True, exist_ok=True)
47-
print(f" 📁 Debug outputs: {self.run_dir}")
45+
if run_dir:
46+
# Resume mode: use existing run directory
47+
self.run_dir = Path(run_dir)
48+
if not self.run_dir.exists():
49+
self.run_dir.mkdir(parents=True, exist_ok=True)
50+
print(f" 📁 Debug outputs (resume): {self.run_dir}")
51+
else:
52+
# New run: create run-specific directory with timestamp
53+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
54+
self.run_dir = self.output_dir / f"run_{timestamp}"
55+
self.run_dir.mkdir(parents=True, exist_ok=True)
56+
print(f" 📁 Debug outputs: {self.run_dir}")
4857

4958
def _get_filename(
5059
self,
@@ -314,11 +323,11 @@ def save_post_processing(
314323
_debug_manager: DebugOutputManager | None = None
315324

316325

317-
def get_debug_manager(config: dict[str, Any] | None = None) -> DebugOutputManager:
326+
def get_debug_manager(config: dict[str, Any] | None = None, run_dir: Path | str | None = None) -> DebugOutputManager:
318327
"""Get or create the global debug output manager."""
319328
global _debug_manager
320329
if _debug_manager is None:
321-
_debug_manager = DebugOutputManager(config)
330+
_debug_manager = DebugOutputManager(config, run_dir=run_dir)
322331
return _debug_manager
323332

324333

0 commit comments

Comments
 (0)