code-puppy 0.0.123__tar.gz → 0.0.124__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 (85) hide show
  1. {code_puppy-0.0.123 → code_puppy-0.0.124}/PKG-INFO +1 -1
  2. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/callbacks.py +18 -0
  3. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/command_handler.py +35 -9
  4. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/config.py +56 -10
  5. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/main.py +7 -3
  6. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/message_history_processor.py +54 -7
  7. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tools/command_runner.py +3 -1
  8. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tools/file_modifications.py +3 -0
  9. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/app.py +8 -2
  10. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/status_bar.py +4 -4
  11. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/screens/settings.py +53 -18
  12. {code_puppy-0.0.123 → code_puppy-0.0.124}/pyproject.toml +1 -1
  13. {code_puppy-0.0.123 → code_puppy-0.0.124}/.gitignore +0 -0
  14. {code_puppy-0.0.123 → code_puppy-0.0.124}/LICENSE +0 -0
  15. {code_puppy-0.0.123 → code_puppy-0.0.124}/README.md +0 -0
  16. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/__init__.py +0 -0
  17. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/__main__.py +0 -0
  18. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/agent.py +0 -0
  19. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/agent_prompts.py +0 -0
  20. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/__init__.py +0 -0
  21. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/file_path_completion.py +0 -0
  22. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/load_context_completion.py +0 -0
  23. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/meta_command_handler.py +0 -0
  24. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/model_picker_completion.py +0 -0
  25. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/motd.py +0 -0
  26. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  27. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/command_line/utils.py +0 -0
  28. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/http_utils.py +0 -0
  29. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/__init__.py +0 -0
  30. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/message_queue.py +0 -0
  31. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/queue_console.py +0 -0
  32. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/renderers.py +0 -0
  33. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/spinner/__init__.py +0 -0
  34. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  35. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  36. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  37. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/model_factory.py +0 -0
  38. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/models.json +0 -0
  39. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/plugins/__init__.py +0 -0
  40. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/reopenable_async_client.py +0 -0
  41. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/state_management.py +0 -0
  42. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/status_display.py +0 -0
  43. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/summarization_agent.py +0 -0
  44. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/token_utils.py +0 -0
  45. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tools/__init__.py +0 -0
  46. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tools/common.py +0 -0
  47. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tools/file_operations.py +0 -0
  48. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tools/token_check.py +0 -0
  49. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tools/tools_content.py +0 -0
  50. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/__init__.py +0 -0
  51. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/__init__.py +0 -0
  52. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/chat_view.py +0 -0
  53. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/command_history_modal.py +0 -0
  54. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/copy_button.py +0 -0
  55. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/custom_widgets.py +0 -0
  56. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/input_area.py +0 -0
  57. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/components/sidebar.py +0 -0
  58. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/messages.py +0 -0
  59. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/models/__init__.py +0 -0
  60. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/models/chat_message.py +0 -0
  61. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/models/command_history.py +0 -0
  62. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/models/enums.py +0 -0
  63. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/screens/__init__.py +0 -0
  64. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/screens/help.py +0 -0
  65. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/screens/tools.py +0 -0
  66. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/__init__.py +0 -0
  67. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_chat_message.py +0 -0
  68. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_chat_view.py +0 -0
  69. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_command_history.py +0 -0
  70. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_copy_button.py +0 -0
  71. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_custom_widgets.py +0 -0
  72. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_disclaimer.py +0 -0
  73. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_enums.py +0 -0
  74. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_file_browser.py +0 -0
  75. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_help.py +0 -0
  76. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_history_file_reader.py +0 -0
  77. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_input_area.py +0 -0
  78. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_settings.py +0 -0
  79. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_sidebar.py +0 -0
  80. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_sidebar_history.py +0 -0
  81. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_sidebar_history_navigation.py +0 -0
  82. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_status_bar.py +0 -0
  83. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_timestamped_history.py +0 -0
  84. {code_puppy-0.0.123 → code_puppy-0.0.124}/code_puppy/tui/tests/test_tools.py +0 -0
  85. {code_puppy-0.0.123 → code_puppy-0.0.124}/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.123
3
+ Version: 0.0.124
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -9,6 +9,9 @@ PhaseType = Literal[
9
9
  "invoke_agent",
10
10
  "agent_exception",
11
11
  "version_check",
12
+ "edit_file",
13
+ "delete_file",
14
+ "run_shell_command",
12
15
  "load_model_config",
13
16
  "load_prompt",
14
17
  ]
@@ -20,6 +23,9 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
20
23
  "invoke_agent": [],
21
24
  "agent_exception": [],
22
25
  "version_check": [],
26
+ "edit_file": [],
27
+ "delete_file": [],
28
+ "run_shell_command": [],
23
29
  "load_model_config": [],
24
30
  "load_prompt": [],
25
31
  }
@@ -148,5 +154,17 @@ def on_load_model_config(*args, **kwargs) -> List[Any]:
148
154
  return _trigger_callbacks_sync("load_model_config", *args, **kwargs)
149
155
 
150
156
 
157
+ def on_edit_file(*args, **kwargs) -> Any:
158
+ return _trigger_callbacks_sync("edit_file", *args, **kwargs)
159
+
160
+
161
+ def on_delete_file(*args, **kwargs) -> Any:
162
+ return _trigger_callbacks_sync("delete_file", *args, **kwargs)
163
+
164
+
165
+ def on_run_shell_command(*args, **kwargs) -> Any:
166
+ return _trigger_callbacks_sync("run_shell_command", *args, **kwargs)
167
+
168
+
151
169
  def on_load_prompt():
152
170
  return _trigger_callbacks_sync("load_prompt")
@@ -22,7 +22,7 @@ COMMANDS_HELP = """
22
22
  /compact Summarize and compact current chat history
23
23
  /dump_context <name> Save current message history to file
24
24
  /load_context <name> Load message history from file
25
- /set Set puppy config key-values (e.g., /set yolo_mode true)
25
+ /set Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)
26
26
  /tools Show available tools and capabilities
27
27
  /<unknown> Show unknown command warning
28
28
  """
@@ -47,9 +47,12 @@ def handle_command(command: str):
47
47
  return True
48
48
 
49
49
  if command.strip().startswith("/compact"):
50
+ from code_puppy.config import get_compaction_strategy
50
51
  from code_puppy.message_history_processor import (
51
52
  estimate_tokens_for_message,
52
53
  summarize_messages,
54
+ truncation,
55
+ get_protected_token_count,
53
56
  )
54
57
  from code_puppy.messaging import (
55
58
  emit_error,
@@ -66,13 +69,23 @@ def handle_command(command: str):
66
69
  return True
67
70
 
68
71
  before_tokens = sum(estimate_tokens_for_message(m) for m in history)
72
+ compaction_strategy = get_compaction_strategy()
69
73
  emit_info(
70
- f"🤔 Compacting {len(history)} messages... (~{before_tokens} tokens)"
74
+ f"🤔 Compacting {len(history)} messages using {compaction_strategy} strategy... (~{before_tokens} tokens)"
71
75
  )
72
76
 
73
- compacted, _ = summarize_messages(history, with_protection=False)
77
+ if compaction_strategy == "truncation":
78
+ protected_tokens = get_protected_token_count()
79
+ compacted = truncation(history, protected_tokens)
80
+ summarized_messages = [] # No summarization in truncation mode
81
+ else:
82
+ # Default to summarization
83
+ compacted, summarized_messages = summarize_messages(
84
+ history, with_protection=False
85
+ )
86
+
74
87
  if not compacted:
75
- emit_error("Summarization failed. History unchanged.")
88
+ emit_error("Compaction failed. History unchanged.")
76
89
  return True
77
90
 
78
91
  set_message_history(compacted)
@@ -83,8 +96,14 @@ def handle_command(command: str):
83
96
  if before_tokens > 0
84
97
  else 0
85
98
  )
99
+
100
+ strategy_info = (
101
+ f"using {compaction_strategy} strategy"
102
+ if compaction_strategy == "truncation"
103
+ else "via summarization"
104
+ )
86
105
  emit_success(
87
- f"✨ Done! History: {len(history)} → {len(compacted)} messages\n"
106
+ f"✨ Done! History: {len(history)} → {len(compacted)} messages {strategy_info}\n"
88
107
  f"🏦 Tokens: {before_tokens:,} → {after_tokens:,} ({reduction_pct:.1f}% reduction)"
89
108
  )
90
109
  return True
@@ -119,16 +138,19 @@ def handle_command(command: str):
119
138
  get_owner_name,
120
139
  get_protected_token_count,
121
140
  get_puppy_name,
122
- get_summarization_threshold,
141
+ get_compaction_threshold,
123
142
  get_yolo_mode,
124
143
  )
125
144
 
145
+ from code_puppy.config import get_compaction_strategy
146
+
126
147
  puppy_name = get_puppy_name()
127
148
  owner_name = get_owner_name()
128
149
  model = get_active_model()
129
150
  yolo_mode = get_yolo_mode()
130
151
  protected_tokens = get_protected_token_count()
131
- summary_threshold = get_summarization_threshold()
152
+ compaction_threshold = get_compaction_threshold()
153
+ compaction_strategy = get_compaction_strategy()
132
154
 
133
155
  status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
134
156
 
@@ -137,7 +159,8 @@ def handle_command(command: str):
137
159
  [bold]model:[/bold] [green]{model}[/green]
138
160
  [bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
139
161
  [bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
140
- [bold]summary_threshold:[/bold] [cyan]{summary_threshold:.1%}[/cyan] context usage triggers summarization
162
+ [bold]compaction_threshold:[/bold] [cyan]{compaction_threshold:.1%}[/cyan] context usage triggers compaction
163
+ [bold]compaction_strategy:[/bold] [cyan]{compaction_strategy}[/cyan] (summarization or truncation)
141
164
 
142
165
  """
143
166
  emit_info(status_msg)
@@ -162,8 +185,11 @@ def handle_command(command: str):
162
185
  key = tokens[1]
163
186
  value = ""
164
187
  else:
188
+ config_keys = get_config_keys()
189
+ if "compaction_strategy" not in config_keys:
190
+ config_keys.append("compaction_strategy")
165
191
  emit_warning(
166
- f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(get_config_keys())}"
192
+ f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]"
167
193
  )
168
194
  return True
169
195
  if key:
@@ -68,13 +68,33 @@ def get_owner_name():
68
68
  # using get_protected_token_count() and get_summarization_threshold()
69
69
 
70
70
 
71
+ def get_model_context_length() -> int:
72
+ """
73
+ Get the context length for the currently configured model from models.json
74
+ """
75
+ try:
76
+ from code_puppy.model_factory import ModelFactory
77
+
78
+ model_configs = ModelFactory.load_config()
79
+ model_name = get_model_name()
80
+
81
+ # Get context length from model config
82
+ model_config = model_configs.get(model_name, {})
83
+ context_length = model_config.get("context_length", 128000) # Default value
84
+
85
+ return int(context_length)
86
+ except Exception:
87
+ # Fallback to default context length if anything goes wrong
88
+ return 128000
89
+
90
+
71
91
  # --- CONFIG SETTER STARTS HERE ---
72
92
  def get_config_keys():
73
93
  """
74
94
  Returns the list of all config keys currently in puppy.cfg,
75
- plus certain preset expected keys (e.g. "yolo_mode", "model").
95
+ plus certain preset expected keys (e.g. "yolo_mode", "model", "compaction_strategy").
76
96
  """
77
- default_keys = ["yolo_mode", "model"]
97
+ default_keys = ["yolo_mode", "model", "compaction_strategy"]
78
98
  config = configparser.ConfigParser()
79
99
  config.read(CONFIG_FILE)
80
100
  keys = set(config[DEFAULT_SECTION].keys()) if DEFAULT_SECTION in config else set()
@@ -354,30 +374,56 @@ def get_protected_token_count():
354
374
  This is the number of tokens in recent messages that won't be summarized.
355
375
  Defaults to 50000 if unset or misconfigured.
356
376
  Configurable by 'protected_token_count' key.
377
+ Enforces that protected tokens don't exceed 75% of model context length.
357
378
  """
358
379
  val = get_value("protected_token_count")
359
380
  try:
360
- return max(1000, int(val)) if val else 50000 # Minimum 1000 tokens
381
+ # Get the model context length to enforce the 75% limit
382
+ model_context_length = get_model_context_length()
383
+ max_protected_tokens = int(model_context_length * 0.75)
384
+
385
+ # Parse the configured value
386
+ configured_value = int(val) if val else 50000
387
+
388
+ # Apply constraints: minimum 1000, maximum 75% of context length
389
+ return max(1000, min(configured_value, max_protected_tokens))
361
390
  except (ValueError, TypeError):
362
- return 50000
391
+ # If parsing fails, return a reasonable default that respects the 75% limit
392
+ model_context_length = get_model_context_length()
393
+ max_protected_tokens = int(model_context_length * 0.75)
394
+ return min(50000, max_protected_tokens)
363
395
 
364
396
 
365
- def get_summarization_threshold():
397
+ def get_compaction_threshold():
366
398
  """
367
- Returns the user-configured summarization threshold as a float between 0.0 and 1.0.
368
- This is the proportion of model context that triggers summarization.
399
+ Returns the user-configured compaction threshold as a float between 0.0 and 1.0.
400
+ This is the proportion of model context that triggers compaction.
369
401
  Defaults to 0.85 (85%) if unset or misconfigured.
370
- Configurable by 'summarization_threshold' key.
402
+ Configurable by 'compaction_threshold' key.
371
403
  """
372
- val = get_value("summarization_threshold")
404
+ val = get_value("compaction_threshold")
373
405
  try:
374
406
  threshold = float(val) if val else 0.85
375
407
  # Clamp between reasonable bounds
376
- return max(0.1, min(0.95, threshold))
408
+ return max(0.8, min(0.95, threshold))
377
409
  except (ValueError, TypeError):
378
410
  return 0.85
379
411
 
380
412
 
413
+ def get_compaction_strategy() -> str:
414
+ """
415
+ Returns the user-configured compaction strategy.
416
+ Options are 'summarization' or 'truncation'.
417
+ Defaults to 'summarization' if not set or misconfigured.
418
+ Configurable by 'compaction_strategy' key.
419
+ """
420
+ val = get_value("compaction_strategy")
421
+ if val and val.lower() in ["summarization", "truncation"]:
422
+ return val.lower()
423
+ # Default to summarization
424
+ return "summarization"
425
+
426
+
381
427
  def save_command_to_history(command: str):
382
428
  """Save a command to the history file with an ISO format timestamp.
383
429
 
@@ -321,12 +321,14 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
321
321
  initial_command, usage_limits=get_custom_usage_limits()
322
322
  )
323
323
  finally:
324
- set_message_history(prune_interrupted_tool_calls(get_message_history()))
324
+ set_message_history(
325
+ prune_interrupted_tool_calls(get_message_history())
326
+ )
325
327
 
326
328
  agent_response = response.output
327
329
 
328
330
  emit_system_message(
329
- f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response.output_message}"
331
+ f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response}"
330
332
  )
331
333
  new_msgs = response.all_messages()
332
334
  message_history_accumulator(new_msgs)
@@ -466,7 +468,9 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
466
468
  usage_limits=get_custom_usage_limits(),
467
469
  )
468
470
  finally:
469
- set_message_history(prune_interrupted_tool_calls(get_message_history()))
471
+ set_message_history(
472
+ prune_interrupted_tool_calls(get_message_history())
473
+ )
470
474
 
471
475
  # Create the task
472
476
  agent_task = asyncio.create_task(run_agent_task())
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import queue
2
3
  from typing import Any, List, Set, Tuple
3
4
 
4
5
  import pydantic
@@ -7,7 +8,8 @@ from pydantic_ai.messages import ModelMessage, ModelRequest, TextPart, ToolCallP
7
8
  from code_puppy.config import (
8
9
  get_model_name,
9
10
  get_protected_token_count,
10
- get_summarization_threshold,
11
+ get_compaction_threshold,
12
+ get_compaction_strategy,
11
13
  )
12
14
  from code_puppy.messaging import emit_error, emit_info, emit_warning
13
15
  from code_puppy.model_factory import ModelFactory
@@ -87,6 +89,12 @@ def estimate_tokens_for_message(message: ModelMessage) -> int:
87
89
  return max(1, total_tokens)
88
90
 
89
91
 
92
+ def filter_huge_messages(messages: List[ModelMessage]) -> List[ModelMessage]:
93
+ filtered = [m for m in messages if estimate_tokens_for_message(m) < 50000]
94
+ pruned = prune_interrupted_tool_calls(filtered)
95
+ return pruned
96
+
97
+
90
98
  def split_messages_for_protected_summarization(
91
99
  messages: List[ModelMessage],
92
100
  ) -> Tuple[List[ModelMessage], List[ModelMessage]]:
@@ -306,7 +314,8 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
306
314
  status_bar.update_token_info(
307
315
  total_current_tokens, model_max, proportion_used
308
316
  )
309
- except Exception:
317
+ except Exception as e:
318
+ emit_error(e)
310
319
  # Fallback to chat message if status bar update fails
311
320
  emit_info(
312
321
  f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n",
@@ -323,12 +332,26 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
323
332
  emit_info(
324
333
  f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n"
325
334
  )
335
+ # Get the configured compaction threshold
336
+ compaction_threshold = get_compaction_threshold()
337
+
338
+ # Get the configured compaction strategy
339
+ compaction_strategy = get_compaction_strategy()
340
+
341
+ if proportion_used > compaction_threshold:
342
+ if compaction_strategy == "truncation":
343
+ # Use truncation instead of summarization
344
+ protected_tokens = get_protected_token_count()
345
+ result_messages = truncation(
346
+ filter_huge_messages(messages), protected_tokens
347
+ )
348
+ summarized_messages = [] # No summarization in truncation mode
349
+ else:
350
+ # Default to summarization
351
+ result_messages, summarized_messages = summarize_messages(
352
+ filter_huge_messages(messages)
353
+ )
326
354
 
327
- # Get the configured summarization threshold
328
- summarization_threshold = get_summarization_threshold()
329
-
330
- if proportion_used > summarization_threshold:
331
- result_messages, summarized_messages = summarize_messages(messages)
332
355
  final_token_count = sum(
333
356
  estimate_tokens_for_message(msg) for msg in result_messages
334
357
  )
@@ -360,6 +383,30 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
360
383
  return messages
361
384
 
362
385
 
386
+ def truncation(
387
+ messages: List[ModelMessage], protected_tokens: int
388
+ ) -> List[ModelMessage]:
389
+ emit_info("Truncating message history to manage token usage")
390
+ result = [messages[0]] # Always keep the first message (system prompt)
391
+ num_tokens = 0
392
+ stack = queue.LifoQueue()
393
+
394
+ # Put messages in reverse order (most recent first) into the stack
395
+ # but break when we exceed protected_tokens
396
+ for idx, msg in enumerate(reversed(messages[1:])): # Skip the first message
397
+ num_tokens += estimate_tokens_for_message(msg)
398
+ if num_tokens > protected_tokens:
399
+ break
400
+ stack.put(msg)
401
+
402
+ # Pop messages from stack to get them in chronological order
403
+ while not stack.empty():
404
+ result.append(stack.get())
405
+
406
+ result = prune_interrupted_tool_calls(result)
407
+ return result
408
+
409
+
363
410
  def message_history_accumulator(messages: List[Any]):
364
411
  _message_history = get_message_history()
365
412
  message_history_hashes = set([hash_message(m) for m in _message_history])
@@ -12,6 +12,7 @@ from pydantic_ai import RunContext
12
12
  from rich.markdown import Markdown
13
13
  from rich.text import Text
14
14
 
15
+ from code_puppy.callbacks import on_run_shell_command
15
16
  from code_puppy.messaging import (
16
17
  emit_divider,
17
18
  emit_error,
@@ -543,7 +544,8 @@ def register_command_runner_tools(agent):
543
544
  This tool can execute arbitrary shell commands. Exercise caution when
544
545
  running untrusted commands, especially those that modify system state.
545
546
  """
546
- return run_shell_command(context, command, cwd, timeout)
547
+ result = run_shell_command(context, command, cwd, timeout)
548
+ on_run_shell_command(result)
547
549
 
548
550
  @agent.tool
549
551
  def agent_share_your_reasoning(
@@ -20,6 +20,7 @@ import json_repair
20
20
  from pydantic import BaseModel
21
21
  from pydantic_ai import RunContext
22
22
 
23
+ from code_puppy.callbacks import on_delete_file, on_edit_file
23
24
  from code_puppy.messaging import emit_error, emit_info, emit_warning
24
25
  from code_puppy.tools.common import _find_best_window, generate_group_id
25
26
 
@@ -542,6 +543,7 @@ def register_file_modifications_tools(agent):
542
543
  }
543
544
  group_id = generate_group_id("edit_file", payload.file_path)
544
545
  result = _edit_file(context, payload, group_id)
546
+ on_edit_file(result)
545
547
  if "diff" in result:
546
548
  del result["diff"]
547
549
  return result
@@ -600,6 +602,7 @@ def register_file_modifications_tools(agent):
600
602
  # Generate group_id for delete_file tool execution
601
603
  group_id = generate_group_id("delete_file", file_path)
602
604
  result = _delete_file(context, file_path, message_group=group_id)
605
+ on_delete_file(result)
603
606
  if "diff" in result:
604
607
  del result["diff"]
605
608
  return result
@@ -27,7 +27,11 @@ from code_puppy.message_history_processor import (
27
27
 
28
28
  # Import our message queue system
29
29
  from code_puppy.messaging import TUIRenderer, get_global_queue
30
- from code_puppy.state_management import clear_message_history, get_message_history, set_message_history
30
+ from code_puppy.state_management import (
31
+ clear_message_history,
32
+ get_message_history,
33
+ set_message_history,
34
+ )
31
35
  from code_puppy.tui.components import (
32
36
  ChatView,
33
37
  CustomTextArea,
@@ -499,7 +503,9 @@ class CodePuppyTUI(App):
499
503
  # Handle regular exceptions
500
504
  self.add_error_message(f"MCP/Agent error: {str(eg)}")
501
505
  finally:
502
- set_message_history(prune_interrupted_tool_calls(get_message_history()))
506
+ set_message_history(
507
+ prune_interrupted_tool_calls(get_message_history())
508
+ )
503
509
  except Exception as agent_error:
504
510
  # Handle any other errors in agent processing
505
511
  self.add_error_message(
@@ -101,15 +101,15 @@ class StatusBar(Static):
101
101
  token_color = "green"
102
102
  if self.token_count > 0 and self.token_capacity > 0:
103
103
  # Import here to avoid circular import
104
- from code_puppy.config import get_summarization_threshold
104
+ from code_puppy.config import get_compaction_threshold
105
105
 
106
- summarization_threshold = get_summarization_threshold()
106
+ get_compaction_threshold = get_compaction_threshold()
107
107
 
108
- if self.token_proportion > summarization_threshold:
108
+ if self.token_proportion > get_compaction_threshold:
109
109
  token_color = "red"
110
110
  token_status = f"🔴 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
111
111
  elif self.token_proportion > (
112
- summarization_threshold - 0.15
112
+ get_compaction_threshold - 0.15
113
113
  ): # 15% before summarization threshold
114
114
  token_color = "yellow"
115
115
  token_status = f"🟡 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
@@ -100,9 +100,20 @@ class SettingsScreen(ModalScreen):
100
100
  )
101
101
 
102
102
  with Container(classes="setting-row"):
103
- yield Static("Summary Threshold:", classes="setting-label")
103
+ yield Static("Compaction Strategy:", classes="setting-label")
104
+ yield Select(
105
+ [
106
+ ("Summarization", "summarization"),
107
+ ("Truncation", "truncation"),
108
+ ],
109
+ id="compaction-strategy-select",
110
+ classes="setting-input",
111
+ )
112
+
113
+ with Container(classes="setting-row"):
114
+ yield Static("Compaction Threshold:", classes="setting-label")
104
115
  yield Input(
105
- id="summary-threshold-input",
116
+ id="compaction-threshold-input",
106
117
  classes="setting-input",
107
118
  placeholder="e.g., 0.85",
108
119
  )
@@ -118,7 +129,8 @@ class SettingsScreen(ModalScreen):
118
129
  get_owner_name,
119
130
  get_protected_token_count,
120
131
  get_puppy_name,
121
- get_summarization_threshold,
132
+ get_compaction_strategy,
133
+ get_compaction_threshold,
122
134
  )
123
135
 
124
136
  # Load current values
@@ -126,12 +138,18 @@ class SettingsScreen(ModalScreen):
126
138
  owner_name_input = self.query_one("#owner-name-input", Input)
127
139
  model_select = self.query_one("#model-select", Select)
128
140
  protected_tokens_input = self.query_one("#protected-tokens-input", Input)
129
- summary_threshold_input = self.query_one("#summary-threshold-input", Input)
141
+ compaction_threshold_input = self.query_one(
142
+ "#compaction-threshold-input", Input
143
+ )
144
+ compaction_strategy_select = self.query_one(
145
+ "#compaction-strategy-select", Select
146
+ )
130
147
 
131
148
  puppy_name_input.value = get_puppy_name() or ""
132
149
  owner_name_input.value = get_owner_name() or ""
133
150
  protected_tokens_input.value = str(get_protected_token_count())
134
- summary_threshold_input.value = str(get_summarization_threshold())
151
+ compaction_threshold_input.value = str(get_compaction_threshold())
152
+ compaction_strategy_select.value = get_compaction_strategy()
135
153
 
136
154
  # Load available models
137
155
  self.load_model_options(model_select)
@@ -146,9 +164,7 @@ class SettingsScreen(ModalScreen):
146
164
  """Load available models into the model select widget."""
147
165
  try:
148
166
  # Use the same method that interactive mode uses to load models
149
- import os
150
167
 
151
- from code_puppy.config import CONFIG_DIR
152
168
  from code_puppy.model_factory import ModelFactory
153
169
 
154
170
  # Load models using the same path and method as interactive mode
@@ -171,7 +187,11 @@ class SettingsScreen(ModalScreen):
171
187
  @on(Button.Pressed, "#save-button")
172
188
  def save_settings(self) -> None:
173
189
  """Save the modified settings."""
174
- from code_puppy.config import set_config_value, set_model_name
190
+ from code_puppy.config import (
191
+ set_config_value,
192
+ set_model_name,
193
+ get_model_context_length,
194
+ )
175
195
 
176
196
  try:
177
197
  # Get values from inputs
@@ -182,8 +202,8 @@ class SettingsScreen(ModalScreen):
182
202
  protected_tokens = self.query_one(
183
203
  "#protected-tokens-input", Input
184
204
  ).value.strip()
185
- summary_threshold = self.query_one(
186
- "#summary-threshold-input", Input
205
+ compaction_threshold = self.query_one(
206
+ "#compaction-threshold-input", Input
187
207
  ).value.strip()
188
208
 
189
209
  # Validate and save
@@ -201,31 +221,46 @@ class SettingsScreen(ModalScreen):
201
221
  # Validate and save protected tokens
202
222
  if protected_tokens.isdigit():
203
223
  tokens_value = int(protected_tokens)
224
+ model_context_length = get_model_context_length()
225
+ max_protected_tokens = int(model_context_length * 0.75)
226
+
204
227
  if tokens_value >= 1000: # Minimum validation
205
- set_config_value("protected_token_count", protected_tokens)
228
+ if tokens_value <= max_protected_tokens: # Maximum validation
229
+ set_config_value("protected_token_count", protected_tokens)
230
+ else:
231
+ raise ValueError(
232
+ f"Protected tokens must not exceed 75% of model context length ({max_protected_tokens} tokens for current model)"
233
+ )
206
234
  else:
207
235
  raise ValueError("Protected tokens must be at least 1000")
208
236
  elif protected_tokens: # If not empty but not digit
209
237
  raise ValueError("Protected tokens must be a valid number")
210
238
 
211
- # Validate and save summary threshold
212
- if summary_threshold:
239
+ # Validate and save compaction threshold
240
+ if compaction_threshold:
213
241
  try:
214
- threshold_value = float(summary_threshold)
215
- if 0.1 <= threshold_value <= 0.95: # Same bounds as config function
216
- set_config_value("summarization_threshold", summary_threshold)
242
+ threshold_value = float(compaction_threshold)
243
+ if 0.8 <= threshold_value <= 0.95: # Same bounds as config function
244
+ set_config_value("compaction_threshold", compaction_threshold)
217
245
  else:
218
246
  raise ValueError(
219
- "Summary threshold must be between 0.1 and 0.95"
247
+ "Compaction threshold must be between 0.8 and 0.95"
220
248
  )
221
249
  except ValueError as ve:
222
250
  if "must be between" in str(ve):
223
251
  raise ve
224
252
  else:
225
253
  raise ValueError(
226
- "Summary threshold must be a valid decimal number"
254
+ "Compaction threshold must be a valid decimal number"
227
255
  )
228
256
 
257
+ # Save compaction strategy
258
+ compaction_strategy = self.query_one(
259
+ "#compaction-strategy-select", Select
260
+ ).value
261
+ if compaction_strategy in ["summarization", "truncation"]:
262
+ set_config_value("compaction_strategy", compaction_strategy)
263
+
229
264
  # Return success message with model change info
230
265
  message = "Settings saved successfully!"
231
266
  if selected_model:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.123"
7
+ version = "0.0.124"
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