Skip to content

Commit f2b1c41

Browse files
committed
code walkthroughs: Improve prompting and heartbeating
1 parent 8b6ca9f commit f2b1c41

File tree

1 file changed

+106
-10
lines changed

1 file changed

+106
-10
lines changed

build_tools/generate_code_walkthroughs.py

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@
7878
from shared.model_selector import TaskType, TaskComplexity
7979

8080

81+
# Default heartbeat interval for long-running CLI operations
82+
DEFAULT_HEARTBEAT_INTERVAL = 60.0 # Log heartbeat every 60 seconds
83+
84+
8185
class RateLimitExceeded(Exception):
8286
"""Exception raised when all backends are rate limited."""
8387

@@ -98,6 +102,55 @@ def __init__(self, message: str, backend: str, wait_time: Optional[float] = None
98102
self.wait_time = wait_time
99103

100104

105+
async def _communicate_with_heartbeat(
106+
process: asyncio.subprocess.Process,
107+
input_data: Optional[bytes],
108+
start_time: float,
109+
heartbeat_interval: float = DEFAULT_HEARTBEAT_INTERVAL,
110+
label: str = "CLI",
111+
) -> tuple[bytes, bytes]:
112+
"""
113+
Communicate with a subprocess while logging periodic heartbeats.
114+
115+
This helps track long-running operations and provides visibility
116+
into whether the process is still active.
117+
118+
Args:
119+
process: The subprocess to communicate with
120+
input_data: Data to send to stdin (or None)
121+
start_time: When the operation started (for elapsed time calculation)
122+
heartbeat_interval: Seconds between heartbeat messages
123+
label: Label to use in heartbeat messages (e.g., "Claude", "Copilot")
124+
125+
Returns:
126+
tuple[bytes, bytes]: (stdout, stderr) from the process
127+
"""
128+
129+
async def heartbeat_logger():
130+
"""Log periodic heartbeats while waiting for process."""
131+
while True:
132+
await asyncio.sleep(heartbeat_interval)
133+
elapsed = time.time() - start_time
134+
print(
135+
f"[heartbeat] {label} still running... ({elapsed:.0f}s elapsed)",
136+
file=sys.stderr,
137+
)
138+
139+
heartbeat_task = asyncio.create_task(heartbeat_logger())
140+
try:
141+
if input_data is not None:
142+
stdout, stderr = await process.communicate(input=input_data)
143+
else:
144+
stdout, stderr = await process.communicate()
145+
return stdout, stderr
146+
finally:
147+
heartbeat_task.cancel()
148+
try:
149+
await heartbeat_task
150+
except asyncio.CancelledError:
151+
pass
152+
153+
101154
# =============================================================================
102155
# Backend Management with Failover
103156
# =============================================================================
@@ -762,6 +815,12 @@ async def analyze_file_with_cli(
762815
## Architecture Overview
763816
The Sharpy compiler pipeline flows: Source (.spy) → Lexer → Parser (AST) → Semantic Analysis → RoslynEmitter → C#
764817
818+
## Important: Partial Classes
819+
Note that large modules in the Sharpy codebase are often split across multiple files using C# partial classes.
820+
For example, the standard library in `Sharpy.Core` uses `partial class Exports` spread across directories like
821+
`Partial.List/`, `Partial.Str/`, etc. If you see a partial class, you may need to reference related files
822+
to provide complete documentation. Include cross-referencing links to related partial class files when relevant.
823+
765824
## Task
766825
Create a walkthrough document covering:
767826
@@ -784,11 +843,16 @@ async def analyze_file_with_cli(
784843
- Bullet points for lists
785844
- Emphasis on readability for someone new to the codebase
786845
787-
Focus on providing intuition and understanding, not just restating what the code does line-by-line."""
846+
Focus on providing intuition and understanding, not just restating what the code does line-by-line.
847+
848+
8. **Cross-References**: If this file is a partial class or heavily depends on other files, include links to related documentation files."""
788849

789850
# Build the command for the specified CLI provider
790851
cmd, stdin_input = _build_cli_command(cli_provider, prompt)
791852

853+
# Determine label for heartbeat logging
854+
heartbeat_label = "Claude" if cli_provider == "claude" else "Copilot"
855+
792856
# Call the AI CLI in programmatic mode with write permissions
793857
# Change to the base directory so relative paths work
794858
# Using create_subprocess_exec (not shell) so the prompt argument is safely passed
@@ -804,8 +868,16 @@ async def analyze_file_with_cli(
804868
try:
805869
# Pass prompt via stdin if required (e.g., for Claude CLI)
806870
input_bytes = stdin_input.encode("utf-8") if stdin_input else None
871+
# Use heartbeat-enabled communication for visibility during long operations
807872
stdout, stderr = await asyncio.wait_for(
808-
process.communicate(input=input_bytes), timeout=timeout
873+
_communicate_with_heartbeat(
874+
process,
875+
input_data=input_bytes,
876+
start_time=start_time,
877+
heartbeat_interval=DEFAULT_HEARTBEAT_INTERVAL,
878+
label=heartbeat_label,
879+
),
880+
timeout=timeout,
809881
)
810882
duration = time.time() - start_time
811883

@@ -886,7 +958,9 @@ async def analyze_file_with_cli(
886958
is_stale=is_stale,
887959
extra={"backend": cli_provider},
888960
)
889-
print(f"✓ Generated: {output_path} (via {cli_provider})")
961+
print(
962+
f"✓ Generated: {output_path} (via {cli_provider}, {duration:.1f}s)"
963+
)
890964
return True, cli_provider
891965
else:
892966
log_execution(
@@ -901,13 +975,14 @@ async def analyze_file_with_cli(
901975
extra={"backend": cli_provider},
902976
)
903977
print(
904-
f"Warning: CLI completed but output file not found: {output_path}",
978+
f"Warning: CLI completed but output file not found: {output_path}",
905979
file=sys.stderr,
906980
)
907981
# Print CLI's response for debugging
908982
if stdout:
983+
cli_output = stdout.decode("utf-8")[:500]
909984
print(
910-
f"CLI output: {stdout.decode('utf-8')[:500]}",
985+
f" CLI output preview: {cli_output}",
911986
file=sys.stderr,
912987
)
913988
return False, cli_provider
@@ -925,9 +1000,16 @@ async def analyze_file_with_cli(
9251000
is_stale=is_stale,
9261001
extra={"backend": cli_provider},
9271002
)
928-
print(f"Timeout analyzing {cs_file}", file=sys.stderr)
929-
process.kill()
930-
await process.wait()
1003+
print(
1004+
f"⏱ Timeout analyzing {cs_file} after {timeout}s (backend: {cli_provider})",
1005+
file=sys.stderr,
1006+
)
1007+
# Kill the process on timeout
1008+
try:
1009+
process.kill()
1010+
await process.wait()
1011+
except Exception:
1012+
pass # Process may have already exited
9311013
return False, cli_provider
9321014

9331015
except BackendUnavailable:
@@ -1216,10 +1298,24 @@ async def main_async(config: Config):
12161298

12171299
# Wait between batches to avoid rate limiting (except for the last batch)
12181300
if batch_num + config.parallel_instances < len(files_to_process):
1301+
remaining_batches = total_batches - current_batch_num
12191302
print(
1220-
f"Waiting {config.timeout_between_batches}s to avoid rate limiting..."
1303+
f"\n⏳ Waiting {config.timeout_between_batches}s before next batch "
1304+
f"({remaining_batches} batches remaining)..."
12211305
)
1222-
await asyncio.sleep(config.timeout_between_batches)
1306+
# Use a heartbeat during the wait period for long waits
1307+
wait_start = time.time()
1308+
remaining_wait = config.timeout_between_batches
1309+
while remaining_wait > 0:
1310+
sleep_chunk = min(remaining_wait, 30.0) # Check every 30s
1311+
await asyncio.sleep(sleep_chunk)
1312+
remaining_wait -= sleep_chunk
1313+
if remaining_wait > 0 and config.timeout_between_batches >= 60:
1314+
elapsed = time.time() - wait_start
1315+
print(
1316+
f"[heartbeat] Batch wait: {elapsed:.0f}s / {config.timeout_between_batches}s",
1317+
file=sys.stderr,
1318+
)
12231319
except RateLimitExceeded as e:
12241320
# All backends exhausted, stop processing
12251321
print(

0 commit comments

Comments
 (0)