code-puppy 0.0.94__tar.gz → 0.0.95__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {code_puppy-0.0.94 → code_puppy-0.0.95}/PKG-INFO +1 -1
  2. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/agent.py +4 -1
  3. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/command_line/prompt_toolkit_completion.py +2 -6
  4. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/main.py +109 -58
  5. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/message_history_processor.py +10 -7
  6. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/status_display.py +55 -41
  7. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/tools/__init__.py +0 -1
  8. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/tools/command_runner.py +5 -2
  9. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/tools/file_operations.py +20 -7
  10. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/tools/token_check.py +7 -2
  11. {code_puppy-0.0.94 → code_puppy-0.0.95}/pyproject.toml +1 -1
  12. {code_puppy-0.0.94 → code_puppy-0.0.95}/.gitignore +0 -0
  13. {code_puppy-0.0.94 → code_puppy-0.0.95}/LICENSE +0 -0
  14. {code_puppy-0.0.94 → code_puppy-0.0.95}/README.md +0 -0
  15. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/__init__.py +0 -0
  16. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/agent_prompts.py +0 -0
  17. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/command_line/__init__.py +0 -0
  18. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/command_line/file_path_completion.py +0 -0
  19. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/command_line/meta_command_handler.py +0 -0
  20. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/command_line/model_picker_completion.py +0 -0
  21. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/command_line/motd.py +0 -0
  22. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/command_line/utils.py +0 -0
  23. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/config.py +0 -0
  24. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/model_factory.py +0 -0
  25. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/models.json +0 -0
  26. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/state_management.py +0 -0
  27. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/summarization_agent.py +0 -0
  28. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/token_utils.py +0 -0
  29. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/tools/common.py +0 -0
  30. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/tools/file_modifications.py +0 -0
  31. {code_puppy-0.0.94 → code_puppy-0.0.95}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.94
3
+ Version: 0.0.95
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -19,6 +19,7 @@ from code_puppy.tools.common import console
19
19
 
20
20
  MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH", None)
21
21
 
22
+
22
23
  def load_puppy_rules():
23
24
  global PUPPY_RULES
24
25
  puppy_rules_path = Path("AGENT.md")
@@ -27,9 +28,11 @@ def load_puppy_rules():
27
28
  puppy_rules = f.read()
28
29
  return puppy_rules
29
30
 
31
+
30
32
  # Load at import
31
33
  PUPPY_RULES = load_puppy_rules()
32
34
 
35
+
33
36
  class AgentResponse(pydantic.BaseModel):
34
37
  """Represents a response from the agent."""
35
38
 
@@ -80,7 +83,7 @@ def reload_code_generation_agent():
80
83
  output_type=str,
81
84
  retries=3,
82
85
  history_processors=[message_history_accumulator],
83
- toolsets=_load_mcp_servers()
86
+ toolsets=_load_mcp_servers(),
84
87
  )
85
88
  register_all_tools(agent)
86
89
  _code_generation_agent = agent
@@ -1,7 +1,3 @@
1
-
2
-
3
-
4
-
5
1
  # ANSI color codes are no longer necessary because prompt_toolkit handles
6
2
  # styling via the `Style` class. We keep them here commented-out in case
7
3
  # someone needs raw ANSI later, but they are unused in the current code.
@@ -175,7 +171,7 @@ async def get_input_with_combined_completion(
175
171
  def _(event):
176
172
  event.app.current_buffer.insert_text("\n")
177
173
 
178
- @bindings.add('c-c')
174
+ @bindings.add("c-c")
179
175
  def _(event):
180
176
  """Cancel the current prompt when the user presses the ESC key alone."""
181
177
  event.app.exit(exception=KeyboardInterrupt)
@@ -226,4 +222,4 @@ if __name__ == "__main__":
226
222
  break
227
223
  print("\nGoodbye!")
228
224
 
229
- asyncio.run(main())
225
+ asyncio.run(main())
@@ -28,6 +28,7 @@ from code_puppy.message_history_processor import message_history_processor
28
28
  # from code_puppy.tools import * # noqa: F403
29
29
  import logfire
30
30
 
31
+
31
32
  # Define a function to get the secret file path
32
33
  def get_secret_file_path():
33
34
  hidden_directory = os.path.join(os.path.expanduser("~"), ".agent_secret")
@@ -39,8 +40,7 @@ def get_secret_file_path():
39
40
  async def main():
40
41
  # Ensure the config directory and puppy.cfg with name info exist (prompt user if needed)
41
42
  logfire.configure(
42
- token="pylf_v1_us_8G5nLznQtHMRsL4hsNG5v3fPWKjyXbysrMgrQ1bV1wRP",
43
- console=False
43
+ token="pylf_v1_us_8G5nLznQtHMRsL4hsNG5v3fPWKjyXbysrMgrQ1bV1wRP", console=False
44
44
  )
45
45
  logfire.instrument_pydantic_ai()
46
46
  ensure_config_exists()
@@ -201,150 +201,196 @@ async def interactive_mode(history_file_path: str) -> None:
201
201
  try:
202
202
  prettier_code_blocks()
203
203
  local_cancelled = False
204
-
204
+
205
205
  # Initialize status display for tokens per second and loading messages
206
206
  status_display = StatusDisplay(console)
207
-
207
+
208
208
  # Print a message indicating we're about to start processing
209
209
  console.print("\nStarting task processing...")
210
-
210
+
211
211
  async def track_tokens_from_messages():
212
212
  """
213
213
  Track real token counts from message history.
214
-
214
+
215
215
  This async function runs in the background and periodically checks
216
216
  the message history for new tokens. When new tokens are detected,
217
217
  it updates the StatusDisplay with the incremental count to calculate
218
218
  an accurate tokens-per-second rate.
219
-
219
+
220
220
  It also looks for SSE stream time_info data to get precise token rate
221
221
  calculations using the formula: completion_tokens * 1 / completion_time
222
-
222
+
223
223
  The function continues running until status_display.is_active becomes False.
224
224
  """
225
- from code_puppy.message_history_processor import estimate_tokens_for_message
225
+ from code_puppy.message_history_processor import (
226
+ estimate_tokens_for_message,
227
+ )
226
228
  import json
227
229
  import re
228
-
230
+
229
231
  last_token_total = 0
230
232
  last_sse_data = None
231
-
233
+
232
234
  while status_display.is_active:
233
235
  # Get real token count from message history
234
236
  messages = get_message_history()
235
237
  if messages:
236
238
  # Calculate total tokens across all messages
237
- current_token_total = sum(estimate_tokens_for_message(msg) for msg in messages)
238
-
239
+ current_token_total = sum(
240
+ estimate_tokens_for_message(msg) for msg in messages
241
+ )
242
+
239
243
  # If tokens increased, update the display with the incremental count
240
244
  if current_token_total > last_token_total:
241
- status_display.update_token_count(current_token_total - last_token_total)
245
+ status_display.update_token_count(
246
+ current_token_total - last_token_total
247
+ )
242
248
  last_token_total = current_token_total
243
-
249
+
244
250
  # Try to find SSE stream data in assistant messages
245
251
  for msg in messages:
246
252
  # Handle different message types (dict or ModelMessage objects)
247
- if hasattr(msg, 'role') and msg.role == 'assistant':
253
+ if hasattr(msg, "role") and msg.role == "assistant":
248
254
  # ModelMessage object with role attribute
249
- content = msg.content if hasattr(msg, 'content') else ''
250
- elif isinstance(msg, dict) and msg.get('role') == 'assistant':
255
+ content = (
256
+ msg.content if hasattr(msg, "content") else ""
257
+ )
258
+ elif (
259
+ isinstance(msg, dict)
260
+ and msg.get("role") == "assistant"
261
+ ):
251
262
  # Dictionary with 'role' key
252
- content = msg.get('content', '')
263
+ content = msg.get("content", "")
253
264
  # Support for ModelRequest/ModelResponse objects
254
- elif hasattr(msg, 'message') and hasattr(msg.message, 'role') and msg.message.role == 'assistant':
265
+ elif (
266
+ hasattr(msg, "message")
267
+ and hasattr(msg.message, "role")
268
+ and msg.message.role == "assistant"
269
+ ):
255
270
  # Access content through the message attribute
256
- content = msg.message.content if hasattr(msg.message, 'content') else ''
271
+ content = (
272
+ msg.message.content
273
+ if hasattr(msg.message, "content")
274
+ else ""
275
+ )
257
276
  else:
258
277
  # Skip if not an assistant message or unrecognized format
259
278
  continue
260
-
279
+
261
280
  # Convert content to string if it's not already
262
281
  if not isinstance(content, str):
263
282
  try:
264
283
  content = str(content)
265
- except:
284
+ except Exception:
266
285
  continue
267
-
286
+
268
287
  # Look for SSE usage data pattern in the message content
269
- sse_matches = re.findall(r'\{\s*"usage".*?"time_info".*?\}', content, re.DOTALL)
288
+ sse_matches = re.findall(
289
+ r'\{\s*"usage".*?"time_info".*?\}',
290
+ content,
291
+ re.DOTALL,
292
+ )
270
293
  for match in sse_matches:
271
294
  try:
272
295
  # Parse the JSON data
273
296
  sse_data = json.loads(match)
274
- if sse_data != last_sse_data: # Only process new data
297
+ if (
298
+ sse_data != last_sse_data
299
+ ): # Only process new data
275
300
  # Check if we have time_info and completion_tokens
276
- if 'time_info' in sse_data and 'completion_time' in sse_data['time_info'] and \
277
- 'usage' in sse_data and 'completion_tokens' in sse_data['usage']:
278
- completion_time = float(sse_data['time_info']['completion_time'])
279
- completion_tokens = int(sse_data['usage']['completion_tokens'])
280
-
301
+ if (
302
+ "time_info" in sse_data
303
+ and "completion_time"
304
+ in sse_data["time_info"]
305
+ and "usage" in sse_data
306
+ and "completion_tokens"
307
+ in sse_data["usage"]
308
+ ):
309
+ completion_time = float(
310
+ sse_data["time_info"][
311
+ "completion_time"
312
+ ]
313
+ )
314
+ completion_tokens = int(
315
+ sse_data["usage"][
316
+ "completion_tokens"
317
+ ]
318
+ )
319
+
281
320
  # Update rate using the accurate SSE data
282
- if completion_time > 0 and completion_tokens > 0:
283
- status_display.update_rate_from_sse(completion_tokens, completion_time)
321
+ if (
322
+ completion_time > 0
323
+ and completion_tokens > 0
324
+ ):
325
+ status_display.update_rate_from_sse(
326
+ completion_tokens,
327
+ completion_time,
328
+ )
284
329
  last_sse_data = sse_data
285
330
  except (json.JSONDecodeError, KeyError, ValueError):
286
331
  # Ignore parsing errors and continue
287
332
  pass
288
-
333
+
289
334
  # Small sleep interval for responsive updates without excessive CPU usage
290
335
  await asyncio.sleep(0.1)
291
-
336
+
292
337
  async def wrap_agent_run(original_run, *args, **kwargs):
293
338
  """
294
339
  Wraps the agent's run method to enable token tracking.
295
-
340
+
296
341
  This wrapper preserves the original functionality while allowing
297
342
  us to track tokens as they are generated by the model. No additional
298
343
  logic is needed here since the token tracking happens in a separate task.
299
-
344
+
300
345
  Args:
301
346
  original_run: The original agent.run method
302
347
  *args, **kwargs: Arguments to pass to the original run method
303
-
348
+
304
349
  Returns:
305
350
  The result from the original run method
306
351
  """
307
352
  result = await original_run(*args, **kwargs)
308
353
  return result
309
-
354
+
310
355
  async def run_agent_task():
311
356
  """
312
357
  Main task runner for the agent with token tracking.
313
-
314
- This function:
358
+
359
+ This function:
315
360
  1. Sets up the agent with token tracking
316
361
  2. Starts the status display showing token rate
317
362
  3. Runs the agent with the user's task
318
363
  4. Ensures proper cleanup of all resources
319
-
364
+
320
365
  Returns the agent's result or raises any exceptions that occurred.
321
366
  """
322
367
  # Token tracking task reference for cleanup
323
368
  token_tracking_task = None
324
-
369
+
325
370
  try:
326
371
  # Initialize the agent
327
372
  agent = get_code_generation_agent()
328
-
373
+
329
374
  # Start status display
330
375
  status_display.start()
331
-
376
+
332
377
  # Start token tracking
333
- token_tracking_task = asyncio.create_task(track_tokens_from_messages())
334
-
378
+ token_tracking_task = asyncio.create_task(
379
+ track_tokens_from_messages()
380
+ )
381
+
335
382
  # Create a wrapper for the agent's run method
336
383
  original_run = agent.run
337
-
384
+
338
385
  async def wrapped_run(*args, **kwargs):
339
386
  return await wrap_agent_run(original_run, *args, **kwargs)
340
-
387
+
341
388
  agent.run = wrapped_run
342
-
389
+
343
390
  # Run the agent with MCP servers
344
391
  async with agent.run_mcp_servers():
345
392
  result = await agent.run(
346
- task,
347
- message_history=get_message_history()
393
+ task, message_history=get_message_history()
348
394
  )
349
395
  return result
350
396
  except Exception as e:
@@ -358,10 +404,9 @@ async def interactive_mode(history_file_path: str) -> None:
358
404
  token_tracking_task.cancel()
359
405
  if not agent_task.done():
360
406
  set_message_history(
361
- message_history_processor(
362
- get_message_history()
363
- )
407
+ message_history_processor(get_message_history())
364
408
  )
409
+
365
410
  agent_task = asyncio.create_task(run_agent_task())
366
411
 
367
412
  import signal
@@ -371,6 +416,7 @@ async def interactive_mode(history_file_path: str) -> None:
371
416
 
372
417
  # Ensure the interrupt handler only acts once per task
373
418
  handled = False
419
+
374
420
  def keyboard_interrupt_handler(sig, frame):
375
421
  nonlocal local_cancelled
376
422
  nonlocal handled
@@ -381,7 +427,9 @@ async def interactive_mode(history_file_path: str) -> None:
381
427
  try:
382
428
  killed = kill_all_running_shell_processes()
383
429
  if killed:
384
- console.print(f"[yellow]Cancelled {killed} running shell process(es).[/yellow]")
430
+ console.print(
431
+ f"[yellow]Cancelled {killed} running shell process(es).[/yellow]"
432
+ )
385
433
  else:
386
434
  # Then cancel the agent task
387
435
  if not agent_task.done():
@@ -392,6 +440,7 @@ async def interactive_mode(history_file_path: str) -> None:
392
440
  # On Windows, we need to reset the signal handler to avoid weird terminal behavior
393
441
  if sys.platform.startswith("win"):
394
442
  signal.signal(signal.SIGINT, original_handler or signal.SIG_DFL)
443
+
395
444
  try:
396
445
  original_handler = signal.getsignal(signal.SIGINT)
397
446
  signal.signal(signal.SIGINT, keyboard_interrupt_handler)
@@ -412,13 +461,15 @@ async def interactive_mode(history_file_path: str) -> None:
412
461
  if status_display.is_active:
413
462
  status_display.stop()
414
463
  else:
415
- if result is not None and hasattr(result, 'output'):
464
+ if result is not None and hasattr(result, "output"):
416
465
  agent_response = result.output
417
466
  console.print(agent_response)
418
467
  filtered = message_history_processor(get_message_history())
419
468
  set_message_history(filtered)
420
469
  else:
421
- console.print("[yellow]No result received from the agent[/yellow]")
470
+ console.print(
471
+ "[yellow]No result received from the agent[/yellow]"
472
+ )
422
473
  # Still process history if possible
423
474
  filtered = message_history_processor(get_message_history())
424
475
  set_message_history(filtered)
@@ -20,6 +20,7 @@ from code_puppy.token_utils import estimate_tokens
20
20
  # Import the status display to get token rate info
21
21
  try:
22
22
  from code_puppy.status_display import StatusDisplay
23
+
23
24
  STATUS_DISPLAY_AVAILABLE = True
24
25
  except ImportError:
25
26
  STATUS_DISPLAY_AVAILABLE = False
@@ -160,9 +161,8 @@ def summarize_message(message: ModelMessage) -> ModelMessage:
160
161
  content_bits.append(s)
161
162
  if not content_bits:
162
163
  return message
163
- prompt = (
164
- "Please summarize the following user message:\n"
165
- + "\n".join(content_bits)
164
+ prompt = "Please summarize the following user message:\n" + "\n".join(
165
+ content_bits
166
166
  )
167
167
  agent = get_summarization_agent()
168
168
  result = agent.run_sync(prompt)
@@ -194,6 +194,7 @@ def get_model_context_length() -> int:
194
194
  # Reserve 10% of context for response
195
195
  return int(context_length)
196
196
 
197
+
197
198
  def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMessage]:
198
199
  """
199
200
  Remove any messages that participate in mismatched tool call sequences.
@@ -240,7 +241,9 @@ def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMess
240
241
  pruned.append(msg)
241
242
 
242
243
  if dropped_count:
243
- console.print(f"[yellow]Pruned {dropped_count} message(s) with mismatched tool_call_id pairs[/yellow]")
244
+ console.print(
245
+ f"[yellow]Pruned {dropped_count} message(s) with mismatched tool_call_id pairs[/yellow]"
246
+ )
244
247
  return pruned
245
248
 
246
249
 
@@ -251,7 +254,7 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
251
254
  model_max = get_model_context_length()
252
255
 
253
256
  proportion_used = total_current_tokens / model_max
254
-
257
+
255
258
  # Include token per second rate if available
256
259
  token_rate_info = ""
257
260
  if STATUS_DISPLAY_AVAILABLE:
@@ -262,12 +265,12 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
262
265
  token_rate_info = f", {current_rate:.0f} t/s"
263
266
  else:
264
267
  token_rate_info = f", {current_rate:.1f} t/s"
265
-
268
+
266
269
  # Print blue status bar - ALWAYS at top
267
270
  console.print(f"""
268
271
  [bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f}{token_rate_info}
269
272
  """)
270
-
273
+
271
274
  # Print extra line to ensure separation
272
275
  console.print("\n")
273
276
 
@@ -1,10 +1,7 @@
1
1
  import asyncio
2
- import random
3
2
  import time
4
- from datetime import datetime
5
- from typing import List, Optional
6
3
 
7
- from rich.console import Console, RenderableType
4
+ from rich.console import Console
8
5
  from rich.live import Live
9
6
  from rich.panel import Panel
10
7
  from rich.spinner import Spinner
@@ -45,7 +42,7 @@ class StatusDisplay:
45
42
  "Howling at the code...",
46
43
  "Snuggling up to the task...",
47
44
  "Bounding through data...",
48
- "Puppy pondering..."
45
+ "Puppy pondering...",
49
46
  ]
50
47
  self.current_message_index = 0
51
48
  self.spinner = Spinner("dots", text="")
@@ -63,21 +60,23 @@ class StatusDisplay:
63
60
  self.current_rate = (self.current_rate * 0.7) + (rate * 0.3)
64
61
  else:
65
62
  self.current_rate = rate
66
-
63
+
67
64
  # Only ensure rate is not negative
68
65
  self.current_rate = max(0, self.current_rate)
69
-
66
+
70
67
  # Update the global rate for other components to access
71
68
  global CURRENT_TOKEN_RATE
72
69
  CURRENT_TOKEN_RATE = self.current_rate
73
-
70
+
74
71
  self.last_update_time = current_time
75
72
  self.last_token_count = self.token_count
76
73
  return self.current_rate
77
-
78
- def update_rate_from_sse(self, completion_tokens: int, completion_time: float) -> None:
74
+
75
+ def update_rate_from_sse(
76
+ self, completion_tokens: int, completion_time: float
77
+ ) -> None:
79
78
  """Update the token rate directly using SSE time_info data
80
-
79
+
81
80
  Args:
82
81
  completion_tokens: Number of tokens in the completion (from SSE stream)
83
82
  completion_time: Time taken for completion in seconds (from SSE stream)
@@ -85,17 +84,19 @@ class StatusDisplay:
85
84
  if completion_time > 0:
86
85
  # Using the direct t/s formula: tokens / time
87
86
  rate = completion_tokens / completion_time
88
-
87
+
89
88
  # Use a lighter smoothing for this more accurate data
90
89
  if self.current_rate > 0:
91
- self.current_rate = (self.current_rate * 0.3) + (rate * 0.7) # Weight SSE data more heavily
90
+ self.current_rate = (self.current_rate * 0.3) + (
91
+ rate * 0.7
92
+ ) # Weight SSE data more heavily
92
93
  else:
93
94
  self.current_rate = rate
94
-
95
+
95
96
  # Update the global rate
96
97
  global CURRENT_TOKEN_RATE
97
98
  CURRENT_TOKEN_RATE = self.current_rate
98
-
99
+
99
100
  @staticmethod
100
101
  def get_current_rate() -> float:
101
102
  """Get the current token rate for use in other components"""
@@ -111,7 +112,7 @@ class StatusDisplay:
111
112
  # Reset token counters for new task
112
113
  self.last_token_count = 0
113
114
  self.current_rate = 0.0
114
-
115
+
115
116
  # Allow for incremental updates (common for streaming) or absolute updates
116
117
  if tokens > self.token_count or tokens < 0:
117
118
  # Incremental update or reset
@@ -120,64 +121,75 @@ class StatusDisplay:
120
121
  # If tokens <= current count but > 0, treat as incremental
121
122
  # This handles simulated token streaming
122
123
  self.token_count += tokens
123
-
124
+
124
125
  self._calculate_rate()
125
126
 
126
127
  def _get_status_panel(self) -> Panel:
127
128
  """Generate a status panel with current rate and animated message"""
128
- rate_text = f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
129
-
129
+ rate_text = (
130
+ f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
131
+ )
132
+
130
133
  # Update spinner
131
134
  self.spinner.update()
132
-
135
+
133
136
  # Rotate through loading messages every few updates
134
137
  if int(time.time() * 2) % 4 == 0:
135
- self.current_message_index = (self.current_message_index + 1) % len(self.loading_messages)
136
-
138
+ self.current_message_index = (self.current_message_index + 1) % len(
139
+ self.loading_messages
140
+ )
141
+
137
142
  # Create a highly visible status message
138
143
  status_text = Text.assemble(
139
144
  Text(f"⏳ {rate_text} ", style="bold cyan"),
140
145
  self.spinner,
141
- Text(f" {self.loading_messages[self.current_message_index]} ⏳", style="bold yellow")
146
+ Text(
147
+ f" {self.loading_messages[self.current_message_index]} ⏳",
148
+ style="bold yellow",
149
+ ),
142
150
  )
143
-
151
+
144
152
  # Use expanded panel with more visible formatting
145
153
  return Panel(
146
- status_text,
147
- title="[bold blue]Code Puppy Status[/bold blue]",
154
+ status_text,
155
+ title="[bold blue]Code Puppy Status[/bold blue]",
148
156
  border_style="bright_blue",
149
157
  expand=False,
150
- padding=(1, 2)
158
+ padding=(1, 2),
151
159
  )
152
160
 
153
161
  def _get_status_text(self) -> Text:
154
162
  """Generate a status text with current rate and animated message"""
155
- rate_text = f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
156
-
163
+ rate_text = (
164
+ f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
165
+ )
166
+
157
167
  # Update spinner
158
168
  self.spinner.update()
159
-
169
+
160
170
  # Rotate through loading messages
161
- self.current_message_index = (self.current_message_index + 1) % len(self.loading_messages)
171
+ self.current_message_index = (self.current_message_index + 1) % len(
172
+ self.loading_messages
173
+ )
162
174
  message = self.loading_messages[self.current_message_index]
163
-
175
+
164
176
  # Create a highly visible status text
165
177
  return Text.assemble(
166
178
  Text(f"⏳ {rate_text} 🐾", style="bold cyan"),
167
- Text(f" {message}", style="yellow")
179
+ Text(f" {message}", style="yellow"),
168
180
  )
169
-
181
+
170
182
  async def _update_display(self) -> None:
171
183
  """Update the display continuously while active using Rich Live display"""
172
184
  # Add a newline to ensure we're below the blue bar
173
185
  self.console.print("\n")
174
-
186
+
175
187
  # Create a Live display that will update in-place
176
188
  with Live(
177
- self._get_status_text(),
189
+ self._get_status_text(),
178
190
  console=self.console,
179
191
  refresh_per_second=2, # Update twice per second
180
- transient=False # Keep the final state visible
192
+ transient=False, # Keep the final state visible
181
193
  ) as live:
182
194
  # Keep updating the live display while active
183
195
  while self.is_active:
@@ -202,19 +214,21 @@ class StatusDisplay:
202
214
  if self.task:
203
215
  self.task.cancel()
204
216
  self.task = None
205
-
217
+
206
218
  # Print final stats
207
219
  elapsed = time.time() - self.start_time if self.start_time else 0
208
220
  avg_rate = self.token_count / elapsed if elapsed > 0 else 0
209
- self.console.print(f"[dim]Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)[/dim]")
210
-
221
+ self.console.print(
222
+ f"[dim]Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)[/dim]"
223
+ )
224
+
211
225
  # Reset state
212
226
  self.start_time = None
213
227
  self.token_count = 0
214
228
  self.last_update_time = None
215
229
  self.last_token_count = 0
216
230
  self.current_rate = 0
217
-
231
+
218
232
  # Reset global rate to 0 to avoid affecting subsequent tasks
219
233
  global CURRENT_TOKEN_RATE
220
234
  CURRENT_TOKEN_RATE = 0.0
@@ -1,6 +1,5 @@
1
1
  from code_puppy.tools.command_runner import (
2
2
  register_command_runner_tools,
3
- kill_all_running_shell_processes,
4
3
  )
5
4
  from code_puppy.tools.file_modifications import register_file_modifications_tools
6
5
  from code_puppy.tools.file_operations import register_file_operations_tools
@@ -23,6 +23,7 @@ _RUNNING_PROCESSES: Set[subprocess.Popen] = set()
23
23
  _RUNNING_PROCESSES_LOCK = threading.Lock()
24
24
  _USER_KILLED_PROCESSES = set()
25
25
 
26
+
26
27
  def _register_process(proc: subprocess.Popen) -> None:
27
28
  with _RUNNING_PROCESSES_LOCK:
28
29
  _RUNNING_PROCESSES.add(proc)
@@ -279,7 +280,7 @@ def run_shell_command_streaming(
279
280
  exit_code=exit_code,
280
281
  execution_time=execution_time,
281
282
  timeout=False,
282
- user_interrupted=process.pid in _USER_KILLED_PROCESSES
283
+ user_interrupted=process.pid in _USER_KILLED_PROCESSES,
283
284
  )
284
285
  return ShellCommandOutput(
285
286
  success=exit_code == 0,
@@ -380,7 +381,9 @@ def run_shell_command(
380
381
  )
381
382
  _register_process(process)
382
383
  try:
383
- return run_shell_command_streaming(process, timeout=timeout, command=command)
384
+ return run_shell_command_streaming(
385
+ process, timeout=timeout, command=command
386
+ )
384
387
  finally:
385
388
  # Ensure unregistration in case streaming returned early or raised
386
389
  _unregister_process(process)
@@ -9,6 +9,7 @@ from pydantic_ai import RunContext
9
9
  from code_puppy.tools.common import console
10
10
  from code_puppy.token_utils import estimate_tokens
11
11
  from code_puppy.tools.token_check import token_guard
12
+
12
13
  # ---------------------------------------------------------------------------
13
14
  # Module-level helper functions (exposed for unit tests _and_ used as tools)
14
15
  # ---------------------------------------------------------------------------
@@ -186,15 +187,20 @@ class ReadFileOutput(BaseModel):
186
187
  error: str | None = None
187
188
 
188
189
 
189
- def _read_file(context: RunContext, file_path: str, start_line: int | None = None, num_lines: int | None = None) -> ReadFileOutput:
190
+ def _read_file(
191
+ context: RunContext,
192
+ file_path: str,
193
+ start_line: int | None = None,
194
+ num_lines: int | None = None,
195
+ ) -> ReadFileOutput:
190
196
  file_path = os.path.abspath(file_path)
191
-
197
+
192
198
  # Build console message with optional parameters
193
199
  console_msg = f"\n[bold white on blue] READ FILE [/bold white on blue] \U0001f4c2 [bold cyan]{file_path}[/bold cyan]"
194
200
  if start_line is not None and num_lines is not None:
195
201
  console_msg += f" [dim](lines {start_line}-{start_line + num_lines - 1})[/dim]"
196
202
  console.print(console_msg)
197
-
203
+
198
204
  console.print("[dim]" + "-" * 60 + "[/dim]")
199
205
  if not os.path.exists(file_path):
200
206
  error_msg = f"File {file_path} does not exist"
@@ -213,14 +219,16 @@ def _read_file(context: RunContext, file_path: str, start_line: int | None = Non
213
219
  # Ensure indices are within bounds
214
220
  start_idx = max(0, start_idx)
215
221
  end_idx = min(len(lines), end_idx)
216
- content = ''.join(lines[start_idx:end_idx])
222
+ content = "".join(lines[start_idx:end_idx])
217
223
  else:
218
224
  # Read the entire file
219
225
  content = f.read()
220
226
 
221
227
  num_tokens = estimate_tokens(content)
222
228
  if num_tokens > 10000:
223
- raise ValueError("The file is massive, greater than 10,000 tokens which is dangerous to read entirely. Please read this file in chunks.")
229
+ raise ValueError(
230
+ "The file is massive, greater than 10,000 tokens which is dangerous to read entirely. Please read this file in chunks."
231
+ )
224
232
  token_guard(num_tokens)
225
233
  return ReadFileOutput(content=content, num_tokens=num_tokens)
226
234
  except (FileNotFoundError, PermissionError):
@@ -316,12 +324,17 @@ def list_files(
316
324
  if num_tokens > 10000:
317
325
  return ListFileOutput(
318
326
  files=[],
319
- error="Too many files - tokens exceeded. Try listing non-recursively"
327
+ error="Too many files - tokens exceeded. Try listing non-recursively",
320
328
  )
321
329
  return list_files_output
322
330
 
323
331
 
324
- def read_file(context: RunContext, file_path: str = "", start_line: int | None = None, num_lines: int | None = None) -> ReadFileOutput:
332
+ def read_file(
333
+ context: RunContext,
334
+ file_path: str = "",
335
+ start_line: int | None = None,
336
+ num_lines: int | None = None,
337
+ ) -> ReadFileOutput:
325
338
  return _read_file(context, file_path, start_line, num_lines)
326
339
 
327
340
 
@@ -4,8 +4,13 @@ from code_puppy.token_utils import estimate_tokens_for_message
4
4
 
5
5
  def token_guard(num_tokens: int):
6
6
  from code_puppy import state_management
7
+
7
8
  current_history = state_management.get_message_history()
8
- message_hist_tokens = sum(estimate_tokens_for_message(msg) for msg in current_history)
9
+ message_hist_tokens = sum(
10
+ estimate_tokens_for_message(msg) for msg in current_history
11
+ )
9
12
 
10
13
  if message_hist_tokens + num_tokens > (get_model_context_length() * 0.9):
11
- raise ValueError("Tokens produced by this tool call would exceed model capacity")
14
+ raise ValueError(
15
+ "Tokens produced by this tool call would exceed model capacity"
16
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.94"
7
+ version = "0.0.95"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes