code-puppy 0.0.81__tar.gz → 0.0.82__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 (32) hide show
  1. {code_puppy-0.0.81 → code_puppy-0.0.82}/PKG-INFO +6 -17
  2. {code_puppy-0.0.81 → code_puppy-0.0.82}/README.md +3 -15
  3. code_puppy-0.0.82/code_puppy/__init__.py +5 -0
  4. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/agent.py +1 -1
  5. code_puppy-0.0.82/code_puppy/message_history_processor.py +169 -0
  6. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/models.json +73 -73
  7. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/state_management.py +13 -1
  8. code_puppy-0.0.82/code_puppy/summarization_agent.py +72 -0
  9. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/tools/command_runner.py +4 -6
  10. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/tools/file_modifications.py +25 -4
  11. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/tools/file_operations.py +22 -18
  12. {code_puppy-0.0.81 → code_puppy-0.0.82}/pyproject.toml +3 -2
  13. code_puppy-0.0.81/code_puppy/__init__.py +0 -3
  14. code_puppy-0.0.81/code_puppy/message_history_processor.py +0 -78
  15. {code_puppy-0.0.81 → code_puppy-0.0.82}/.gitignore +0 -0
  16. {code_puppy-0.0.81 → code_puppy-0.0.82}/LICENSE +0 -0
  17. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/agent_prompts.py +0 -0
  18. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/command_line/__init__.py +0 -0
  19. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/command_line/file_path_completion.py +0 -0
  20. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/command_line/meta_command_handler.py +0 -0
  21. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/command_line/model_picker_completion.py +0 -0
  22. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/command_line/motd.py +0 -0
  23. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  24. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/command_line/utils.py +0 -0
  25. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/config.py +0 -0
  26. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/main.py +0 -0
  27. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/model_factory.py +0 -0
  28. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/session_memory.py +0 -0
  29. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/tools/__init__.py +0 -0
  30. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/tools/common.py +0 -0
  31. {code_puppy-0.0.81 → code_puppy-0.0.82}/code_puppy/tools/ts_code_map.py +0 -0
  32. {code_puppy-0.0.81 → code_puppy-0.0.82}/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.81
3
+ Version: 0.0.82
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -20,13 +20,14 @@ Requires-Dist: json-repair>=0.46.2
20
20
  Requires-Dist: logfire>=0.7.1
21
21
  Requires-Dist: pathspec>=0.11.0
22
22
  Requires-Dist: prompt-toolkit>=3.0.38
23
- Requires-Dist: pydantic-ai>=0.4.8
23
+ Requires-Dist: pydantic-ai>=0.7.2
24
24
  Requires-Dist: pydantic>=2.4.0
25
25
  Requires-Dist: pytest-cov>=6.1.1
26
26
  Requires-Dist: python-dotenv>=1.0.0
27
27
  Requires-Dist: rapidfuzz>=3.13.0
28
28
  Requires-Dist: rich>=13.4.2
29
29
  Requires-Dist: ruff>=0.11.11
30
+ Requires-Dist: tiktoken>=0.11.0
30
31
  Requires-Dist: tree-sitter-language-pack>=0.8.0
31
32
  Requires-Dist: tree-sitter-typescript>=0.23.2
32
33
  Description-Content-Type: text/markdown
@@ -136,22 +137,10 @@ code-puppy "write me a C++ hello world program in /tmp/main.cpp then compile it
136
137
 
137
138
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
138
139
 
139
- ## Puppy Rules
140
- Puppy rules allow you to define and enforce coding standards and styles that your code should comply with. These rules can cover various aspects such as formatting, naming conventions, and even design guidelines.
140
+ ## Agent Rules
141
+ We support AGENT.md files for defining coding standards and styles that your code should comply with. These rules can cover various aspects such as formatting, naming conventions, and even design guidelines.
141
142
 
142
- ### Example of a Puppy Rule
143
- For instance, if you want to ensure that your application follows a specific design guideline, like using a dark mode theme with teal accents, you can define a puppy rule like this:
144
-
145
- ```plaintext
146
- # Puppy Rule: Dark Mode with Teal Accents
147
-
148
- - theme: dark
149
- - accent-color: teal
150
- - background-color: #121212
151
- - text-color: #e0e0e0
152
-
153
- Ensure that all components follow these color schemes to promote consistency in design.
154
- ```
143
+ For examples and more information about agent rules, visit [https://agent.md](https://agent.md)
155
144
 
156
145
  ## Using MCP Servers for External Tools
157
146
 
@@ -103,22 +103,10 @@ code-puppy "write me a C++ hello world program in /tmp/main.cpp then compile it
103
103
 
104
104
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
105
105
 
106
- ## Puppy Rules
107
- Puppy rules allow you to define and enforce coding standards and styles that your code should comply with. These rules can cover various aspects such as formatting, naming conventions, and even design guidelines.
106
+ ## Agent Rules
107
+ We support AGENT.md files for defining coding standards and styles that your code should comply with. These rules can cover various aspects such as formatting, naming conventions, and even design guidelines.
108
108
 
109
- ### Example of a Puppy Rule
110
- For instance, if you want to ensure that your application follows a specific design guideline, like using a dark mode theme with teal accents, you can define a puppy rule like this:
111
-
112
- ```plaintext
113
- # Puppy Rule: Dark Mode with Teal Accents
114
-
115
- - theme: dark
116
- - accent-color: teal
117
- - background-color: #121212
118
- - text-color: #e0e0e0
119
-
120
- Ensure that all components follow these color schemes to promote consistency in design.
121
- ```
109
+ For examples and more information about agent rules, visit [https://agent.md](https://agent.md)
122
110
 
123
111
  ## Using MCP Servers for External Tools
124
112
 
@@ -0,0 +1,5 @@
1
+ try:
2
+ import importlib.metadata
3
+ __version__ = importlib.metadata.version("code-puppy")
4
+ except importlib.metadata.PackageNotFoundError:
5
+ __version__ = "0.0.1"
@@ -21,7 +21,7 @@ from code_puppy.tools.common import console
21
21
  MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH", None)
22
22
 
23
23
  # Puppy rules loader
24
- PUPPY_RULES_PATH = Path(".puppy_rules")
24
+ PUPPY_RULES_PATH = Path("AGENT.md")
25
25
  PUPPY_RULES = None
26
26
 
27
27
 
@@ -0,0 +1,169 @@
1
+ import json
2
+ import queue
3
+ from typing import List
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import pydantic
8
+ import tiktoken
9
+ from pydantic_ai.messages import ModelMessage, ToolCallPart, ToolReturnPart, UserPromptPart, TextPart, ModelRequest, ModelResponse
10
+
11
+ from code_puppy.config import get_message_history_limit
12
+ from code_puppy.tools.common import console
13
+ from code_puppy.model_factory import ModelFactory
14
+ from code_puppy.config import get_model_name
15
+
16
+ # Import summarization agent
17
+ try:
18
+ from code_puppy.summarization_agent import get_summarization_agent as _get_summarization_agent
19
+ SUMMARIZATION_AVAILABLE = True
20
+
21
+ # Make the function available in this module's namespace for mocking
22
+ def get_summarization_agent():
23
+ return _get_summarization_agent()
24
+
25
+ except ImportError:
26
+ SUMMARIZATION_AVAILABLE = False
27
+ console.print("[yellow]Warning: Summarization agent not available. Message history will be truncated instead of summarized.[/yellow]")
28
+ def get_summarization_agent():
29
+ return None
30
+
31
+
32
+ def get_tokenizer_for_model(model_name: str):
33
+ """
34
+ Always use cl100k_base tokenizer regardless of model type.
35
+ This is a simple approach that works reasonably well for most models.
36
+ """
37
+ return tiktoken.get_encoding("cl100k_base")
38
+
39
+
40
+ def stringify_message_part(part) -> str:
41
+ """
42
+ Convert a message part to a string representation for token estimation or other uses.
43
+
44
+ Args:
45
+ part: A message part that may contain content or be a tool call
46
+
47
+ Returns:
48
+ String representation of the message part
49
+ """
50
+ result = ""
51
+ if hasattr(part, "part_kind"):
52
+ result += part.part_kind + ": "
53
+ else:
54
+ result += str(type(part)) + ": "
55
+
56
+ # Handle content
57
+ if hasattr(part, 'content') and part.content:
58
+ # Handle different content types
59
+ if isinstance(part.content, str):
60
+ result = part.content
61
+ elif isinstance(part.content, pydantic.BaseModel):
62
+ result = json.dumps(part.content.model_dump())
63
+ elif isinstance(part.content, dict):
64
+ result = json.dumps(part.content)
65
+ else:
66
+ result = str(part.content)
67
+
68
+ # Handle tool calls which may have additional token costs
69
+ # If part also has content, we'll process tool calls separately
70
+ if hasattr(part, 'tool_name') and part.tool_name:
71
+ # Estimate tokens for tool name and parameters
72
+ tool_text = part.tool_name
73
+ if hasattr(part, "args"):
74
+ tool_text += f" {str(part.args)}"
75
+ result += tool_text
76
+
77
+ return result
78
+
79
+
80
+ def estimate_tokens_for_message(message: ModelMessage) -> int:
81
+ """
82
+ Estimate the number of tokens in a message using tiktoken with cl100k_base encoding.
83
+ This is more accurate than character-based estimation.
84
+ """
85
+ tokenizer = get_tokenizer_for_model(get_model_name())
86
+ total_tokens = 0
87
+
88
+ for part in message.parts:
89
+ part_str = stringify_message_part(part)
90
+ if part_str:
91
+ tokens = tokenizer.encode(part_str)
92
+ total_tokens += len(tokens)
93
+
94
+ return max(1, total_tokens)
95
+
96
+
97
+ def summarize_messages(messages: List[ModelMessage]) -> ModelMessage:
98
+
99
+ # Get the summarization agent
100
+ summarization_agent = get_summarization_agent()
101
+ message_strings = []
102
+
103
+ for message in messages:
104
+ for part in message.parts:
105
+ message_strings.append(stringify_message_part(part))
106
+
107
+
108
+ summary_string = "\n".join(message_strings)
109
+ instructions = (
110
+ "Above I've given you a log of Agentic AI steps that have been taken"
111
+ " as well as user queries, etc. Summarize the contents of these steps."
112
+ " The high level details should remain but the bulk of the content from tool-call"
113
+ " responses should be compacted and summarized. For example if you see a tool-call"
114
+ " reading a file, and the file contents are large, then in your summary you might just"
115
+ " write: * used read_file on space_invaders.cpp - contents removed."
116
+ "\n Make sure your result is a bulleted list of all steps and interactions."
117
+ )
118
+ try:
119
+ # Run the summarization agent
120
+ 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
126
+ except Exception as e:
127
+ console.print(f"Summarization failed during compaction: {e}")
128
+ # Return original message if summarization fails
129
+ return None
130
+
131
+
132
+ def get_model_context_length() -> int:
133
+ """
134
+ Get the context length for the currently configured model from models.json
135
+ """
136
+ # Load model configuration
137
+ models_path = os.environ.get("MODELS_JSON_PATH")
138
+ if not models_path:
139
+ models_path = Path(__file__).parent / "models.json"
140
+ else:
141
+ models_path = Path(models_path)
142
+
143
+ model_configs = ModelFactory.load_config(str(models_path))
144
+ model_name = get_model_name()
145
+
146
+ # Get context length from model config
147
+ model_config = model_configs.get(model_name, {})
148
+ context_length = model_config.get("context_length", 128000) # Default value
149
+
150
+ # Reserve 10% of context for response
151
+ return int(context_length)
152
+
153
+
154
+ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
155
+
156
+ total_current_tokens = sum(estimate_tokens_for_message(msg) for msg in messages)
157
+
158
+ model_max = get_model_context_length()
159
+
160
+ proportion_used = total_current_tokens / model_max
161
+ console.print(f"[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used}")
162
+
163
+ if proportion_used > 0.9:
164
+ summary = summarize_messages(messages)
165
+ result_messages = [messages[0], summary]
166
+ final_token_count = sum(estimate_tokens_for_message(msg) for msg in result_messages)
167
+ console.print(f"Final token count after processing: {final_token_count}")
168
+ return result_messages
169
+ return messages
@@ -1,45 +1,59 @@
1
1
  {
2
- "gemini-2.5-flash-preview-05-20": {
3
- "type": "gemini",
4
- "name": "gemini-2.5-flash-preview-05-20"
5
- },
6
- "gpt-4.1": {
7
- "type": "openai",
8
- "name": "gpt-4.1"
9
- },
10
- "gpt-4.1-mini": {
11
- "type": "openai",
12
- "name": "gpt-4.1-mini"
13
- },
14
2
  "gpt-5": {
15
3
  "type": "openai",
16
- "name": "gpt-5"
4
+ "name": "gpt-5",
5
+ "context_length": 400000
17
6
  },
18
- "gpt-4.1-nano": {
19
- "type": "openai",
20
- "name": "gpt-4.1-nano"
7
+ "Cerebras-Qwen3-Coder-480b": {
8
+ "type": "custom_openai",
9
+ "name": "qwen-3-coder-480b",
10
+ "custom_endpoint": {
11
+ "url": "https://api.cerebras.ai/v1",
12
+ "api_key": "$CEREBRAS_API_KEY"
13
+ },
14
+ "context_length": 131072
21
15
  },
22
- "o3": {
23
- "type": "openai",
24
- "name": "o3"
16
+ "Cerebras-Qwen3-235b-a22b-instruct-2507": {
17
+ "type": "custom_openai",
18
+ "name": "qwen-3-235b-a22b-instruct-2507",
19
+ "custom_endpoint": {
20
+ "url": "https://api.cerebras.ai/v1",
21
+ "api_key": "$CEREBRAS_API_KEY"
22
+ },
23
+ "context_length": 64000
25
24
  },
26
- "gpt-4.1-custom": {
25
+ "Cerebras-gpt-oss-120b": {
27
26
  "type": "custom_openai",
28
- "name": "gpt-4.1-custom",
27
+ "name": "gpt-oss-120b",
29
28
  "custom_endpoint": {
30
- "url": "https://my.cute.endpoint:8080",
31
- "headers": {
32
- "X-Api-Key": "$OPENAI_API_KEY"
33
- },
34
- "ca_certs_path": "/path/to/cert.pem"
35
- }
29
+ "url": "https://api.cerebras.ai/v1",
30
+ "api_key": "$CEREBRAS_API_KEY"
31
+ },
32
+ "context_length": 131072
36
33
  },
37
- "ollama-llama3.3": {
34
+ "Cerebras-Qwen-3-32b": {
38
35
  "type": "custom_openai",
39
- "name": "llama3.3",
36
+ "name": "qwen-3-32b",
40
37
  "custom_endpoint": {
41
- "url": "http://localhost:11434/v1"
42
- }
38
+ "url": "https://api.cerebras.ai/v1",
39
+ "api_key": "$CEREBRAS_API_KEY"
40
+ },
41
+ "context_length": 65536
42
+ },
43
+ "o3": {
44
+ "type": "openai",
45
+ "name": "o3",
46
+ "context_length": 200000
47
+ },
48
+ "gemini-2.5-flash-preview-05-20": {
49
+ "type": "gemini",
50
+ "name": "gemini-2.5-flash-preview-05-20",
51
+ "context_length": 1048576
52
+ },
53
+ "gpt-4.1": {
54
+ "type": "openai",
55
+ "name": "gpt-4.1",
56
+ "context_length": 1000000
43
57
  },
44
58
  "Qwen/Qwen3-235B-A22B-fp8-tput": {
45
59
  "type": "custom_openai",
@@ -47,65 +61,51 @@
47
61
  "custom_endpoint": {
48
62
  "url": "https://api.together.xyz/v1",
49
63
  "api_key": "$TOGETHER_API_KEY"
50
- }
51
- },
52
- "grok-3-mini-fast": {
53
- "type": "custom_openai",
54
- "name": "grok-3-mini-fast",
55
- "custom_endpoint": {
56
- "url": "https://api.x.ai/v1",
57
- "api_key": "$XAI_API_KEY"
58
- }
64
+ },
65
+ "context_length": 64000
59
66
  },
60
67
  "openrouter": {
61
68
  "type": "openrouter",
62
69
  "name": "meta-llama/llama-4-maverick:free",
63
- "api_key": "$OPENROUTER_API_KEY"
70
+ "api_key": "$OPENROUTER_API_KEY",
71
+ "context_length": 131072
64
72
  },
65
73
  "azure-gpt-4.1": {
66
74
  "type": "azure_openai",
67
75
  "name": "gpt-4.1",
68
76
  "api_version": "2024-12-01-preview",
69
77
  "api_key": "$AZURE_OPENAI_API_KEY",
70
- "azure_endpoint": "$AZURE_OPENAI_ENDPOINT"
78
+ "azure_endpoint": "$AZURE_OPENAI_ENDPOINT",
79
+ "context_length": 128000
71
80
  },
72
- "Llama-4-Scout-17B-16E-Instruct": {
73
- "type": "azure_openai",
74
- "name": "Llama-4-Scout-17B-16E-Instruct",
75
- "api_version": "2024-12-01-preview",
76
- "api_key": "$AZURE_OPENAI_API_KEY",
77
- "azure_endpoint": "$AZURE_OPENAI_ENDPOINT"
78
- },
79
- "Cerebras-Qwen3-Coder-480b": {
80
- "type": "custom_openai",
81
- "name": "qwen-3-coder-480b",
82
- "custom_endpoint": {
83
- "url": "https://api.cerebras.ai/v1",
84
- "api_key": "$CEREBRAS_API_KEY"
85
- }
81
+ "gpt-4.1-mini": {
82
+ "type": "openai",
83
+ "name": "gpt-4.1-mini",
84
+ "context_length": 128000
86
85
  },
87
- "Cerebras-Qwen3-235b-a22b-instruct-2507": {
88
- "type": "custom_openai",
89
- "name": "qwen-3-235b-a22b-instruct-2507",
90
- "custom_endpoint": {
91
- "url": "https://api.cerebras.ai/v1",
92
- "api_key": "$CEREBRAS_API_KEY"
93
- }
86
+ "gpt-4.1-nano": {
87
+ "type": "openai",
88
+ "name": "gpt-4.1-nano",
89
+ "context_length": 128000
94
90
  },
95
- "Cerebras-gpt-oss-120b": {
91
+ "gpt-4.1-custom": {
96
92
  "type": "custom_openai",
97
- "name": "gpt-oss-120b",
93
+ "name": "gpt-4.1-custom",
98
94
  "custom_endpoint": {
99
- "url": "https://api.cerebras.ai/v1",
100
- "api_key": "$CEREBRAS_API_KEY"
101
- }
95
+ "url": "https://my.cute.endpoint:8080",
96
+ "headers": {
97
+ "X-Api-Key": "$OPENAI_API_KEY"
98
+ },
99
+ "ca_certs_path": "/path/to/cert.pem"
100
+ },
101
+ "context_length": 128000
102
102
  },
103
- "Cerebras-Qwen-3-32b": {
103
+ "ollama-llama3.3": {
104
104
  "type": "custom_openai",
105
- "name": "qwen-3-32b",
105
+ "name": "llama3.3",
106
106
  "custom_endpoint": {
107
- "url": "https://api.cerebras.ai/v1",
108
- "api_key": "$CEREBRAS_API_KEY"
109
- }
107
+ "url": "http://localhost:11434/v1"
108
+ },
109
+ "context_length": 8192
110
110
  }
111
111
  }
@@ -1,6 +1,7 @@
1
1
  from typing import Any, List
2
2
 
3
3
  from code_puppy.tools.common import console
4
+ from code_puppy.message_history_processor import message_history_processor
4
5
 
5
6
  _message_history: List[Any] = []
6
7
 
@@ -35,8 +36,19 @@ def hash_message(message):
35
36
 
36
37
 
37
38
  def message_history_accumulator(messages: List[Any]):
39
+ global _message_history
40
+
38
41
  message_history_hashes = set([hash_message(m) for m in _message_history])
39
42
  for msg in messages:
40
43
  if hash_message(msg) not in message_history_hashes:
41
44
  _message_history.append(msg)
42
- return messages
45
+
46
+ # Apply message history trimming using the main processor
47
+ # This ensures we maintain global state while still managing context limits
48
+ trimmed_messages = message_history_processor(_message_history)
49
+
50
+ # Update our global state with the trimmed version
51
+ # This preserves the state but keeps us within token limits
52
+ _message_history = trimmed_messages
53
+
54
+ return _message_history
@@ -0,0 +1,72 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import pydantic
5
+ from pydantic_ai import Agent
6
+ from pydantic_ai.mcp import MCPServerSSE
7
+
8
+ from code_puppy.model_factory import ModelFactory
9
+ from code_puppy.tools.common import console
10
+
11
+ # Environment variables used in this module:
12
+ # - MODELS_JSON_PATH: Optional path to a custom models.json configuration file.
13
+ # If not set, uses the default file in the package directory.
14
+ # - MODEL_NAME: The model to use for code generation. Defaults to "gpt-4o".
15
+ # Must match a key in the models.json configuration.
16
+
17
+ MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH", None)
18
+
19
+ _LAST_MODEL_NAME = None
20
+ _summarization_agent = None
21
+
22
+
23
+ def reload_summarization_agent():
24
+ """Create a specialized agent for summarizing messages when context limit is reached."""
25
+ global _summarization_agent, _LAST_MODEL_NAME
26
+ from code_puppy.config import get_model_name
27
+
28
+ model_name = get_model_name()
29
+ console.print(f"[bold cyan]Loading Summarization Model: {model_name}[/bold cyan]")
30
+ models_path = (
31
+ Path(MODELS_JSON_PATH)
32
+ if MODELS_JSON_PATH
33
+ else Path(__file__).parent / "models.json"
34
+ )
35
+ model = ModelFactory.get_model(model_name, ModelFactory.load_config(models_path))
36
+
37
+ # Specialized instructions for summarization
38
+ instructions = """You are a message summarization expert. Your task is to summarize conversation messages
39
+ while preserving important context and information. The summaries should be concise but capture the essential
40
+ content and intent of the original messages. This is to help manage token usage in a conversation history
41
+ while maintaining context for the AI to continue the conversation effectively.
42
+
43
+ When summarizing:
44
+ 1. Keep summary brief but informative
45
+ 2. Preserve key information and decisions
46
+ 3. Keep any important technical details
47
+ 4. Don't summarize the system message
48
+ 5. Make sure all tool calls and responses are summarized, as they are vital"""
49
+
50
+ agent = Agent(
51
+ model=model,
52
+ instructions=instructions,
53
+ output_type=str,
54
+ retries=1 # Fewer retries for summarization
55
+ )
56
+ _summarization_agent = agent
57
+ _LAST_MODEL_NAME = model_name
58
+ return _summarization_agent
59
+
60
+
61
+ def get_summarization_agent(force_reload=False):
62
+ """
63
+ Retrieve the summarization agent with the currently set MODEL_NAME.
64
+ Forces a reload if the model has changed, or if force_reload is passed.
65
+ """
66
+ global _summarization_agent, _LAST_MODEL_NAME
67
+ from code_puppy.config import get_model_name
68
+
69
+ model_name = get_model_name()
70
+ if _summarization_agent is None or _LAST_MODEL_NAME != model_name or force_reload:
71
+ return reload_summarization_agent()
72
+ return _summarization_agent
@@ -164,21 +164,19 @@ def run_shell_command(
164
164
 
165
165
  class ReasoningOutput(BaseModel):
166
166
  success: bool = True
167
- reasoning: str = ""
168
- next_steps: str = ""
169
167
 
170
168
 
171
169
  def share_your_reasoning(
172
- context: RunContext, reasoning: str, next_steps: str = None
170
+ context: RunContext, reasoning: str, next_steps: str | None = None
173
171
  ) -> ReasoningOutput:
174
172
  console.print("\n[bold white on purple] AGENT REASONING [/bold white on purple]")
175
173
  console.print("[bold cyan]Current reasoning:[/bold cyan]")
176
174
  console.print(Markdown(reasoning))
177
- if next_steps and next_steps.strip():
175
+ if next_steps is not None and next_steps.strip():
178
176
  console.print("\n[bold cyan]Planned next steps:[/bold cyan]")
179
177
  console.print(Markdown(next_steps))
180
178
  console.print("[dim]" + "-" * 60 + "[/dim]\n")
181
- return ReasoningOutput(**{"success": True, "reasoning": reasoning, "next_steps": next_steps})
179
+ return ReasoningOutput(**{"success": True})
182
180
 
183
181
 
184
182
  def register_command_runner_tools(agent):
@@ -190,6 +188,6 @@ def register_command_runner_tools(agent):
190
188
 
191
189
  @agent.tool
192
190
  def agent_share_your_reasoning(
193
- context: RunContext, reasoning: str, next_steps: str = None
191
+ context: RunContext, reasoning: str, next_steps: str | None = None
194
192
  ) -> ReasoningOutput:
195
193
  return share_your_reasoning(context, reasoning, next_steps)
@@ -58,12 +58,21 @@ def _delete_snippet_from_file(
58
58
  diff_text = ""
59
59
  try:
60
60
  if not os.path.exists(file_path) or not os.path.isfile(file_path):
61
- return {"error": f"File '{file_path}' does not exist.", "diff": diff_text}
61
+ return {
62
+ "success": False,
63
+ "path": file_path,
64
+ "message": f"File '{file_path}' does not exist.",
65
+ "changed": False,
66
+ "diff": diff_text,
67
+ }
62
68
  with open(file_path, "r", encoding="utf-8") as f:
63
69
  original = f.read()
64
70
  if snippet not in original:
65
71
  return {
66
- "error": f"Snippet not found in file '{file_path}'.",
72
+ "success": False,
73
+ "path": file_path,
74
+ "message": f"Snippet not found in file '{file_path}'.",
75
+ "changed": False,
67
76
  "diff": diff_text,
68
77
  }
69
78
  modified = original.replace(snippet, "")
@@ -317,7 +326,13 @@ def _delete_file(context: RunContext, file_path: str = "") -> Dict[str, Any]:
317
326
  file_path = os.path.abspath(file_path)
318
327
  try:
319
328
  if not os.path.exists(file_path) or not os.path.isfile(file_path):
320
- res = {"error": f"File '{file_path}' does not exist.", "diff": ""}
329
+ res = {
330
+ "success": False,
331
+ "path": file_path,
332
+ "message": f"File '{file_path}' does not exist.",
333
+ "changed": False,
334
+ "diff": "",
335
+ }
321
336
  else:
322
337
  with open(file_path, "r", encoding="utf-8") as f:
323
338
  original = f.read()
@@ -340,7 +355,13 @@ def _delete_file(context: RunContext, file_path: str = "") -> Dict[str, Any]:
340
355
  }
341
356
  except Exception as exc:
342
357
  _log_error("Unhandled exception in delete_file", exc)
343
- res = {"error": str(exc), "diff": ""}
358
+ res = {
359
+ "success": False,
360
+ "path": file_path,
361
+ "message": str(exc),
362
+ "changed": False,
363
+ "diff": "",
364
+ }
344
365
  _print_diff(res.get("diff", ""))
345
366
  return res
346
367
 
@@ -41,11 +41,11 @@ def _list_files(
41
41
  f"[bold red]Error:[/bold red] Directory '{directory}' does not exist"
42
42
  )
43
43
  console.print("[dim]" + "-" * 60 + "[/dim]\n")
44
- return ListFileOutput(files=[ListedFile(**{"error": f"Directory '{directory}' does not exist"})])
44
+ return ListFileOutput(files=[ListedFile(path=None, type=None, full_path=None, depth=None)])
45
45
  if not os.path.isdir(directory):
46
46
  console.print(f"[bold red]Error:[/bold red] '{directory}' is not a directory")
47
47
  console.print("[dim]" + "-" * 60 + "[/dim]\n")
48
- return ListFileOutput(files=[ListedFile(**{"error": f"'{directory}' is not a directory"})])
48
+ return ListFileOutput(files=[ListedFile(path=None, type=None, full_path=None, depth=None)])
49
49
  folder_structure = {}
50
50
  file_list = []
51
51
  for root, dirs, files in os.walk(directory):
@@ -266,22 +266,26 @@ def _grep(
266
266
  f"[green]Found {len(matches)} match(es) for '{search_string}' in {directory}[/green]"
267
267
  )
268
268
 
269
- return GrepOutput(matches=[])
269
+ return GrepOutput(matches=matches)
270
+
271
+
272
+ def list_files(
273
+ context: RunContext, directory: str = ".", recursive: bool = True
274
+ ) -> ListFileOutput:
275
+ return _list_files(context, directory, recursive)
276
+
277
+
278
+ def read_file(context: RunContext, file_path: str = "") -> ReadFileOutput:
279
+ return _read_file(context, file_path)
280
+
281
+
282
+ def grep(
283
+ context: RunContext, search_string: str = "", directory: str = "."
284
+ ) -> GrepOutput:
285
+ return _grep(context, search_string, directory)
270
286
 
271
287
 
272
288
  def register_file_operations_tools(agent):
273
- @agent.tool
274
- def list_files(
275
- context: RunContext, directory: str = ".", recursive: bool = True
276
- ) -> ListFileOutput:
277
- return _list_files(context, directory, recursive)
278
-
279
- @agent.tool
280
- def read_file(context: RunContext, file_path: str = "") -> ReadFileOutput:
281
- return _read_file(context, file_path)
282
-
283
- @agent.tool
284
- def grep(
285
- context: RunContext, search_string: str = "", directory: str = "."
286
- ) -> GrepOutput:
287
- return _grep(context, search_string, directory)
289
+ agent.tool(list_files)
290
+ agent.tool(read_file)
291
+ agent.tool(grep)
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.81"
7
+ version = "0.0.82"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "pydantic-ai>=0.4.8",
12
+ "pydantic-ai>=0.7.2",
13
13
  "httpx>=0.24.1",
14
14
  "rich>=13.4.2",
15
15
  "logfire>=0.7.1",
@@ -25,6 +25,7 @@ dependencies = [
25
25
  "json-repair>=0.46.2",
26
26
  "tree-sitter-language-pack>=0.8.0",
27
27
  "tree-sitter-typescript>=0.23.2",
28
+ "tiktoken>=0.11.0",
28
29
  ]
29
30
  dev-dependencies = [
30
31
  "pytest>=8.3.4",
@@ -1,3 +0,0 @@
1
- import importlib.metadata
2
-
3
- __version__ = importlib.metadata.version("code-puppy")
@@ -1,78 +0,0 @@
1
- import queue
2
- from typing import List
3
-
4
- from pydantic_ai.messages import ModelMessage, ToolCallPart, ToolReturnPart
5
-
6
- from code_puppy.config import get_message_history_limit
7
- from code_puppy.tools.common import console
8
-
9
-
10
- def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
11
- """
12
- Truncate message history to manage token usage while preserving context.
13
-
14
- This implementation:
15
- - Uses the configurable message_history_limit from puppy.cfg (defaults to 40)
16
- - Preserves system messages at the beginning
17
- - Maintains tool call/response pairs together
18
- - Follows PydanticAI best practices for message ordering
19
-
20
- Args:
21
- messages: List of ModelMessage objects from conversation history
22
-
23
- Returns:
24
- Truncated list of ModelMessage objects
25
- """
26
- if not messages:
27
- return messages
28
-
29
- # Get the configurable limit from puppy.cfg
30
- max_messages = get_message_history_limit()
31
- # If we have max_messages or fewer, no truncation needed
32
- if len(messages) <= max_messages:
33
- return messages
34
-
35
- console.print(
36
- f"Truncating message history to manage token usage: {max_messages}"
37
- )
38
- result = []
39
- result.append(messages[0]) # this is the system prompt
40
- remaining_messages_to_fill = max_messages - 1
41
- stack = queue.LifoQueue()
42
- count = 0
43
- tool_call_parts = set()
44
- tool_return_parts = set()
45
- for message in reversed(messages):
46
- stack.put(message)
47
- count += 1
48
- if count >= remaining_messages_to_fill:
49
- break
50
-
51
- while not stack.empty():
52
- item = stack.get()
53
- for part in item.parts:
54
- if hasattr(part, "tool_call_id") and part.tool_call_id:
55
- if isinstance(part, ToolCallPart):
56
- tool_call_parts.add(part.tool_call_id)
57
- if isinstance(part, ToolReturnPart):
58
- tool_return_parts.add(part.tool_call_id)
59
-
60
- result.append(item)
61
-
62
- missmatched_tool_call_ids = (tool_call_parts.union(tool_return_parts)) - (
63
- tool_call_parts.intersection(tool_return_parts)
64
- )
65
- # trust...
66
- final_result = result
67
- if missmatched_tool_call_ids:
68
- final_result = []
69
- for msg in result:
70
- is_missmatched = False
71
- for part in msg.parts:
72
- if hasattr(part, "tool_call_id"):
73
- if part.tool_call_id in missmatched_tool_call_ids:
74
- is_missmatched = True
75
- if is_missmatched:
76
- continue
77
- final_result.append(msg)
78
- return final_result
File without changes
File without changes