code-puppy 0.0.84__tar.gz → 0.0.86__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 (30) hide show
  1. {code_puppy-0.0.84 → code_puppy-0.0.86}/PKG-INFO +1 -1
  2. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/__init__.py +1 -0
  3. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/agent.py +1 -1
  4. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/agent_prompts.py +1 -3
  5. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/command_line/motd.py +1 -1
  6. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/main.py +23 -12
  7. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/message_history_processor.py +126 -36
  8. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/model_factory.py +20 -14
  9. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/state_management.py +9 -5
  10. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/summarization_agent.py +2 -4
  11. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/tools/__init__.py +4 -1
  12. code_puppy-0.0.86/code_puppy/tools/command_runner.py +432 -0
  13. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/tools/file_modifications.py +3 -1
  14. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/tools/file_operations.py +30 -23
  15. {code_puppy-0.0.84 → code_puppy-0.0.86}/pyproject.toml +1 -1
  16. code_puppy-0.0.84/code_puppy/tools/command_runner.py +0 -193
  17. {code_puppy-0.0.84 → code_puppy-0.0.86}/.gitignore +0 -0
  18. {code_puppy-0.0.84 → code_puppy-0.0.86}/LICENSE +0 -0
  19. {code_puppy-0.0.84 → code_puppy-0.0.86}/README.md +0 -0
  20. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/command_line/__init__.py +0 -0
  21. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/command_line/file_path_completion.py +0 -0
  22. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/command_line/meta_command_handler.py +0 -0
  23. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/command_line/model_picker_completion.py +0 -0
  24. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  25. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/command_line/utils.py +0 -0
  26. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/config.py +0 -0
  27. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/models.json +0 -0
  28. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/tools/common.py +0 -0
  29. {code_puppy-0.0.84 → code_puppy-0.0.86}/code_puppy/tools/ts_code_map.py +0 -0
  30. {code_puppy-0.0.84 → code_puppy-0.0.86}/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.84
3
+ Version: 0.0.86
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -1,5 +1,6 @@
1
1
  try:
2
2
  import importlib.metadata
3
+
3
4
  __version__ = importlib.metadata.version("code-puppy")
4
5
  except importlib.metadata.PackageNotFoundError:
5
6
  __version__ = "0.0.1"
@@ -79,7 +79,7 @@ def reload_code_generation_agent():
79
79
  instructions=instructions,
80
80
  output_type=str,
81
81
  retries=3,
82
- history_processors=[message_history_accumulator]
82
+ history_processors=[message_history_accumulator],
83
83
  )
84
84
  register_all_tools(agent)
85
85
  _code_generation_agent = agent
@@ -101,9 +101,7 @@ Important rules:
101
101
 
102
102
  Your solutions should be production-ready, maintainable, and follow best practices for the chosen language.
103
103
 
104
- Return your final response as a structured output having the following fields:
105
- * output_message: The final output message to display to the user
106
- * awaiting_user_input: True if user input is needed to continue the task. If you get an error, you might consider asking the user for help.
104
+ Return your final response as a string output
107
105
  """
108
106
 
109
107
 
@@ -48,4 +48,4 @@ def print_motd(console, force: bool = False) -> bool:
48
48
  console.print(MOTD_MESSAGE)
49
49
  mark_motd_seen(MOTD_VERSION)
50
50
  return True
51
- return False
51
+ return False
@@ -21,7 +21,8 @@ from code_puppy.state_management import get_message_history, set_message_history
21
21
  # Initialize rich console for pretty output
22
22
  from code_puppy.tools.common import console
23
23
  from code_puppy.version_checker import fetch_latest_version
24
- from code_puppy.message_history_processor import message_history_processor
24
+ from code_puppy.message_history_processor import message_history_processor, prune_interrupted_tool_calls
25
+
25
26
 
26
27
  # from code_puppy.tools import * # noqa: F403
27
28
 
@@ -193,13 +194,13 @@ async def interactive_mode(history_file_path: str) -> None:
193
194
  try:
194
195
  prettier_code_blocks()
195
196
  local_cancelled = False
197
+
196
198
  async def run_agent_task():
197
199
  try:
198
200
  agent = get_code_generation_agent()
199
201
  async with agent.run_mcp_servers():
200
202
  return await agent.run(
201
- task,
202
- message_history=get_message_history()
203
+ task, message_history=get_message_history()
203
204
  )
204
205
  except Exception as e:
205
206
  console.log("Task failed", e)
@@ -207,20 +208,30 @@ async def interactive_mode(history_file_path: str) -> None:
207
208
  agent_task = asyncio.create_task(run_agent_task())
208
209
 
209
210
  import signal
211
+ from code_puppy.tools import kill_all_running_shell_processes
210
212
 
211
213
  original_handler = None
212
214
 
215
+ # Ensure the interrupt handler only acts once per task
216
+ handled = False
213
217
  def keyboard_interrupt_handler(sig, frame):
214
218
  nonlocal local_cancelled
215
- if not agent_task.done():
216
- set_message_history(
217
- message_history_processor(
218
- get_message_history()
219
- )
220
- )
221
- agent_task.cancel()
222
- local_cancelled = True
223
-
219
+ nonlocal handled
220
+ if handled:
221
+ return
222
+ handled = True
223
+ # First, nuke any running shell processes triggered by tools
224
+ try:
225
+ killed = kill_all_running_shell_processes()
226
+ if killed:
227
+ console.print(f"[yellow]Cancelled {killed} running shell process(es).[/yellow]")
228
+ else:
229
+ # Then cancel the agent task
230
+ if not agent_task.done():
231
+ agent_task.cancel()
232
+ local_cancelled = True
233
+ except Exception as e:
234
+ console.print(f"[dim]Shell kill error: {e}[/dim]")
224
235
  try:
225
236
  original_handler = signal.getsignal(signal.SIGINT)
226
237
  signal.signal(signal.SIGINT, keyboard_interrupt_handler)
@@ -1,30 +1,40 @@
1
1
  import json
2
- import queue
3
- from typing import List
2
+ from typing import List, Set
4
3
  import os
5
4
  from pathlib import Path
6
5
 
7
6
  import pydantic
8
7
  import tiktoken
9
- from pydantic_ai.messages import ModelMessage, ToolCallPart, ToolReturnPart, UserPromptPart, TextPart, ModelRequest, ModelResponse
8
+ from pydantic_ai.messages import (
9
+ ModelMessage,
10
+ TextPart,
11
+ ModelResponse,
12
+ ModelRequest,
13
+ ToolCallPart,
14
+ )
10
15
 
11
- from code_puppy.config import get_message_history_limit
12
16
  from code_puppy.tools.common import console
13
17
  from code_puppy.model_factory import ModelFactory
14
18
  from code_puppy.config import get_model_name
15
19
 
16
20
  # Import summarization agent
17
21
  try:
18
- from code_puppy.summarization_agent import get_summarization_agent as _get_summarization_agent
22
+ from code_puppy.summarization_agent import (
23
+ get_summarization_agent as _get_summarization_agent,
24
+ )
25
+
19
26
  SUMMARIZATION_AVAILABLE = True
20
-
27
+
21
28
  # Make the function available in this module's namespace for mocking
22
29
  def get_summarization_agent():
23
30
  return _get_summarization_agent()
24
-
31
+
25
32
  except ImportError:
26
33
  SUMMARIZATION_AVAILABLE = False
27
- console.print("[yellow]Warning: Summarization agent not available. Message history will be truncated instead of summarized.[/yellow]")
34
+ console.print(
35
+ "[yellow]Warning: Summarization agent not available. Message history will be truncated instead of summarized.[/yellow]"
36
+ )
37
+
28
38
  def get_summarization_agent():
29
39
  return None
30
40
 
@@ -40,10 +50,10 @@ def get_tokenizer_for_model(model_name: str):
40
50
  def stringify_message_part(part) -> str:
41
51
  """
42
52
  Convert a message part to a string representation for token estimation or other uses.
43
-
53
+
44
54
  Args:
45
55
  part: A message part that may contain content or be a tool call
46
-
56
+
47
57
  Returns:
48
58
  String representation of the message part
49
59
  """
@@ -54,7 +64,7 @@ def stringify_message_part(part) -> str:
54
64
  result += str(type(part)) + ": "
55
65
 
56
66
  # Handle content
57
- if hasattr(part, 'content') and part.content:
67
+ if hasattr(part, "content") and part.content:
58
68
  # Handle different content types
59
69
  if isinstance(part.content, str):
60
70
  result = part.content
@@ -64,16 +74,16 @@ def stringify_message_part(part) -> str:
64
74
  result = json.dumps(part.content)
65
75
  else:
66
76
  result = str(part.content)
67
-
77
+
68
78
  # Handle tool calls which may have additional token costs
69
79
  # If part also has content, we'll process tool calls separately
70
- if hasattr(part, 'tool_name') and part.tool_name:
80
+ if hasattr(part, "tool_name") and part.tool_name:
71
81
  # Estimate tokens for tool name and parameters
72
82
  tool_text = part.tool_name
73
83
  if hasattr(part, "args"):
74
84
  tool_text += f" {str(part.args)}"
75
85
  result += tool_text
76
-
86
+
77
87
  return result
78
88
 
79
89
 
@@ -84,27 +94,22 @@ def estimate_tokens_for_message(message: ModelMessage) -> int:
84
94
  """
85
95
  tokenizer = get_tokenizer_for_model(get_model_name())
86
96
  total_tokens = 0
87
-
97
+
88
98
  for part in message.parts:
89
99
  part_str = stringify_message_part(part)
90
100
  if part_str:
91
101
  tokens = tokenizer.encode(part_str)
92
102
  total_tokens += len(tokens)
93
-
103
+
94
104
  return max(1, total_tokens)
95
105
 
96
106
 
97
107
  def summarize_messages(messages: List[ModelMessage]) -> ModelMessage:
98
-
99
- # Get the summarization agent
100
108
  summarization_agent = get_summarization_agent()
101
- message_strings = []
102
-
109
+ message_strings: List[str] = []
103
110
  for message in messages:
104
111
  for part in message.parts:
105
112
  message_strings.append(stringify_message_part(part))
106
-
107
-
108
113
  summary_string = "\n".join(message_strings)
109
114
  instructions = (
110
115
  "Above I've given you a log of Agentic AI steps that have been taken"
@@ -116,19 +121,53 @@ def summarize_messages(messages: List[ModelMessage]) -> ModelMessage:
116
121
  "\n Make sure your result is a bulleted list of all steps and interactions."
117
122
  )
118
123
  try:
119
- # Run the summarization agent
120
124
  result = summarization_agent.run_sync(f"{summary_string}\n{instructions}")
121
-
122
- # Create a new message with the summarized content
123
- summarized_parts = [TextPart(result.output)]
124
- summarized_message = ModelResponse(parts=summarized_parts)
125
- return summarized_message
125
+ return ModelResponse(parts=[TextPart(result.output)])
126
126
  except Exception as e:
127
127
  console.print(f"Summarization failed during compaction: {e}")
128
- # Return original message if summarization fails
129
128
  return None
130
129
 
131
130
 
131
+ # New: single-message summarization helper used by tests
132
+ # - If the message has a ToolCallPart, return original message (no summarization)
133
+ # - If the message has system/instructions, return original message
134
+ # - Otherwise, summarize and return a new ModelRequest with the summarized content
135
+ # - On any error, return the original message
136
+
137
+
138
+ def summarize_message(message: ModelMessage) -> ModelMessage:
139
+ if not SUMMARIZATION_AVAILABLE:
140
+ return message
141
+ try:
142
+ # If the message looks like a system/instructions message, skip summarization
143
+ instructions = getattr(message, "instructions", None)
144
+ if instructions:
145
+ return message
146
+ # If any part is a tool call, skip summarization
147
+ for part in message.parts:
148
+ if isinstance(part, ToolCallPart) or getattr(part, "tool_name", None):
149
+ return message
150
+ # Build prompt from textual content parts
151
+ content_bits: List[str] = []
152
+ for part in message.parts:
153
+ s = stringify_message_part(part)
154
+ if s:
155
+ content_bits.append(s)
156
+ if not content_bits:
157
+ return message
158
+ prompt = (
159
+ "Please summarize the following user message:\n"
160
+ + "\n".join(content_bits)
161
+ )
162
+ agent = get_summarization_agent()
163
+ result = agent.run_sync(prompt)
164
+ summarized = ModelRequest([TextPart(result.output)])
165
+ return summarized
166
+ except Exception as e:
167
+ console.print(f"Summarization failed: {e}")
168
+ return message
169
+
170
+
132
171
  def get_model_context_length() -> int:
133
172
  """
134
173
  Get the context length for the currently configured model from models.json
@@ -139,33 +178,84 @@ def get_model_context_length() -> int:
139
178
  models_path = Path(__file__).parent / "models.json"
140
179
  else:
141
180
  models_path = Path(models_path)
142
-
181
+
143
182
  model_configs = ModelFactory.load_config(str(models_path))
144
183
  model_name = get_model_name()
145
-
184
+
146
185
  # Get context length from model config
147
186
  model_config = model_configs.get(model_name, {})
148
187
  context_length = model_config.get("context_length", 128000) # Default value
149
-
188
+
150
189
  # Reserve 10% of context for response
151
190
  return int(context_length)
152
191
 
192
+ def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMessage]:
193
+ """
194
+ Remove any messages that participate in mismatched tool call sequences.
195
+
196
+ A mismatched tool call id is one that appears in a ToolCall (model/tool request)
197
+ without a corresponding tool return, or vice versa. We preserve original order
198
+ and only drop messages that contain parts referencing mismatched tool_call_ids.
199
+ """
200
+ if not messages:
201
+ return messages
153
202
 
154
- def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
203
+ tool_call_ids: Set[str] = set()
204
+ tool_return_ids: Set[str] = set()
205
+
206
+ # First pass: collect ids for calls vs returns
207
+ for msg in messages:
208
+ for part in getattr(msg, "parts", []) or []:
209
+ tool_call_id = getattr(part, "tool_call_id", None)
210
+ if not tool_call_id:
211
+ continue
212
+ # Heuristic: if it's an explicit ToolCallPart or has a tool_name/args,
213
+ # consider it a call; otherwise it's a return/result.
214
+ if part.part_kind == "tool-call":
215
+ tool_call_ids.add(tool_call_id)
216
+ else:
217
+ tool_return_ids.add(tool_call_id)
218
+
219
+ mismatched: Set[str] = tool_call_ids.symmetric_difference(tool_return_ids)
220
+ if not mismatched:
221
+ return messages
155
222
 
223
+ pruned: List[ModelMessage] = []
224
+ dropped_count = 0
225
+ for msg in messages:
226
+ has_mismatched = False
227
+ for part in getattr(msg, "parts", []) or []:
228
+ tcid = getattr(part, "tool_call_id", None)
229
+ if tcid and tcid in mismatched:
230
+ has_mismatched = True
231
+ break
232
+ if has_mismatched:
233
+ dropped_count += 1
234
+ continue
235
+ pruned.append(msg)
236
+
237
+ if dropped_count:
238
+ console.print(f"[yellow]Pruned {dropped_count} message(s) with mismatched tool_call_id pairs[/yellow]")
239
+ return pruned
240
+
241
+
242
+ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
243
+ # First, prune any interrupted/mismatched tool-call conversations
156
244
  total_current_tokens = sum(estimate_tokens_for_message(msg) for msg in messages)
157
245
 
158
246
  model_max = get_model_context_length()
159
247
 
160
248
  proportion_used = total_current_tokens / model_max
161
249
  console.print(f"""
162
- [bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used}
250
+ [bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f}
163
251
  """)
164
252
 
165
253
  if proportion_used > 0.9:
166
254
  summary = summarize_messages(messages)
167
255
  result_messages = [messages[0], summary]
168
- final_token_count = sum(estimate_tokens_for_message(msg) for msg in result_messages)
256
+ final_token_count = sum(
257
+ estimate_tokens_for_message(msg) for msg in result_messages
258
+ )
169
259
  console.print(f"Final token count after processing: {final_token_count}")
170
260
  return result_messages
171
- return messages
261
+ return messages
@@ -42,15 +42,17 @@ def build_httpx_proxy(proxy):
42
42
  """Build an httpx.Proxy object from a proxy string in format ip:port:username:password"""
43
43
  proxy_tokens = proxy.split(":")
44
44
  if len(proxy_tokens) != 4:
45
- raise ValueError(f"Invalid proxy format: {proxy}. Expected format: ip:port:username:password")
46
-
45
+ raise ValueError(
46
+ f"Invalid proxy format: {proxy}. Expected format: ip:port:username:password"
47
+ )
48
+
47
49
  ip, port, username, password = proxy_tokens
48
50
  proxy_url = f"http://{ip}:{port}"
49
51
  proxy_auth = (username, password)
50
-
52
+
51
53
  # Log the proxy being used
52
54
  console.log(f"Using proxy: {proxy_url} with username: {username}")
53
-
55
+
54
56
  return httpx.Proxy(url=proxy_url, auth=proxy_auth)
55
57
 
56
58
 
@@ -58,18 +60,22 @@ def get_random_proxy_from_file(file_path):
58
60
  """Reads proxy file and returns a random proxy formatted for httpx.AsyncClient"""
59
61
  if not os.path.exists(file_path):
60
62
  raise ValueError(f"Proxy file '{file_path}' not found.")
61
-
63
+
62
64
  with open(file_path, "r") as f:
63
65
  proxies = [line.strip() for line in f.readlines() if line.strip()]
64
-
66
+
65
67
  if not proxies:
66
- raise ValueError(f"Proxy file '{file_path}' is empty or contains only whitespace.")
67
-
68
+ raise ValueError(
69
+ f"Proxy file '{file_path}' is empty or contains only whitespace."
70
+ )
71
+
68
72
  selected_proxy = random.choice(proxies)
69
73
  try:
70
74
  return build_httpx_proxy(selected_proxy)
71
- except ValueError as e:
72
- console.log(f"Warning: Malformed proxy '{selected_proxy}' found in file '{file_path}', ignoring and continuing without proxy.")
75
+ except ValueError:
76
+ console.log(
77
+ f"Warning: Malformed proxy '{selected_proxy}' found in file '{file_path}', ignoring and continuing without proxy."
78
+ )
73
79
  return None
74
80
 
75
81
 
@@ -147,13 +153,13 @@ class ModelFactory:
147
153
 
148
154
  elif model_type == "custom_anthropic":
149
155
  url, headers, ca_certs_path, api_key = get_custom_config(model_config)
150
-
156
+
151
157
  # Check for proxy configuration
152
158
  proxy_file_path = os.environ.get("CODE_PUPPY_PROXIES")
153
159
  proxy = None
154
160
  if proxy_file_path:
155
161
  proxy = get_random_proxy_from_file(proxy_file_path)
156
-
162
+
157
163
  # Only pass proxy to client if it's valid
158
164
  client_args = {"headers": headers, "verify": ca_certs_path}
159
165
  if proxy is not None:
@@ -223,13 +229,13 @@ class ModelFactory:
223
229
 
224
230
  elif model_type == "custom_openai":
225
231
  url, headers, ca_certs_path, api_key = get_custom_config(model_config)
226
-
232
+
227
233
  # Check for proxy configuration
228
234
  proxy_file_path = os.environ.get("CODE_PUPPY_PROXIES")
229
235
  proxy = None
230
236
  if proxy_file_path:
231
237
  proxy = get_random_proxy_from_file(proxy_file_path)
232
-
238
+
233
239
  # Only pass proxy to client if it's valid
234
240
  client_args = {"headers": headers, "verify": ca_certs_path}
235
241
  if proxy is not None:
@@ -1,24 +1,28 @@
1
1
  from typing import Any, List
2
2
 
3
- from code_puppy.tools.common import console
4
3
  from code_puppy.message_history_processor import message_history_processor
5
4
 
6
5
  _message_history: List[Any] = []
7
6
 
7
+
8
8
  def get_message_history() -> List[Any]:
9
9
  return _message_history
10
10
 
11
+
11
12
  def set_message_history(history: List[Any]) -> None:
12
13
  global _message_history
13
14
  _message_history = history
14
15
 
16
+
15
17
  def clear_message_history() -> None:
16
18
  global _message_history
17
19
  _message_history = []
18
20
 
21
+
19
22
  def append_to_message_history(message: Any) -> None:
20
23
  _message_history.append(message)
21
24
 
25
+
22
26
  def extend_message_history(history: List[Any]) -> None:
23
27
  _message_history.extend(history)
24
28
 
@@ -37,18 +41,18 @@ def hash_message(message):
37
41
 
38
42
  def message_history_accumulator(messages: List[Any]):
39
43
  global _message_history
40
-
44
+
41
45
  message_history_hashes = set([hash_message(m) for m in _message_history])
42
46
  for msg in messages:
43
47
  if hash_message(msg) not in message_history_hashes:
44
48
  _message_history.append(msg)
45
-
49
+
46
50
  # Apply message history trimming using the main processor
47
51
  # This ensures we maintain global state while still managing context limits
48
52
  trimmed_messages = message_history_processor(_message_history)
49
-
53
+
50
54
  # Update our global state with the trimmed version
51
55
  # This preserves the state but keeps us within token limits
52
56
  _message_history = trimmed_messages
53
-
57
+
54
58
  return _message_history
@@ -1,9 +1,7 @@
1
1
  import os
2
2
  from pathlib import Path
3
3
 
4
- import pydantic
5
4
  from pydantic_ai import Agent
6
- from pydantic_ai.mcp import MCPServerSSE
7
5
 
8
6
  from code_puppy.model_factory import ModelFactory
9
7
  from code_puppy.tools.common import console
@@ -33,7 +31,7 @@ def reload_summarization_agent():
33
31
  else Path(__file__).parent / "models.json"
34
32
  )
35
33
  model = ModelFactory.get_model(model_name, ModelFactory.load_config(models_path))
36
-
34
+
37
35
  # Specialized instructions for summarization
38
36
  instructions = """You are a message summarization expert. Your task is to summarize conversation messages
39
37
  while preserving important context and information. The summaries should be concise but capture the essential
@@ -51,7 +49,7 @@ When summarizing:
51
49
  model=model,
52
50
  instructions=instructions,
53
51
  output_type=str,
54
- retries=1 # Fewer retries for summarization
52
+ retries=1, # Fewer retries for summarization
55
53
  )
56
54
  _summarization_agent = agent
57
55
  _LAST_MODEL_NAME = model_name
@@ -1,4 +1,7 @@
1
- from code_puppy.tools.command_runner import register_command_runner_tools
1
+ from code_puppy.tools.command_runner import (
2
+ register_command_runner_tools,
3
+ kill_all_running_shell_processes,
4
+ )
2
5
  from code_puppy.tools.file_modifications import register_file_modifications_tools
3
6
  from code_puppy.tools.file_operations import register_file_operations_tools
4
7