code-puppy 0.0.97__tar.gz → 0.0.118__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 (97) hide show
  1. {code_puppy-0.0.97 → code_puppy-0.0.118}/PKG-INFO +9 -2
  2. code_puppy-0.0.118/code_puppy/__init__.py +3 -0
  3. code_puppy-0.0.118/code_puppy/__main__.py +10 -0
  4. code_puppy-0.0.118/code_puppy/agent.py +190 -0
  5. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/agent_prompts.py +30 -24
  6. code_puppy-0.0.118/code_puppy/callbacks.py +152 -0
  7. code_puppy-0.0.118/code_puppy/command_line/command_handler.py +359 -0
  8. code_puppy-0.0.118/code_puppy/command_line/load_context_completion.py +59 -0
  9. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/command_line/model_picker_completion.py +14 -21
  10. code_puppy-0.0.118/code_puppy/command_line/motd.py +67 -0
  11. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/command_line/prompt_toolkit_completion.py +42 -23
  12. code_puppy-0.0.118/code_puppy/config.py +400 -0
  13. code_puppy-0.0.118/code_puppy/http_utils.py +122 -0
  14. code_puppy-0.0.118/code_puppy/main.py +699 -0
  15. code_puppy-0.0.118/code_puppy/message_history_processor.py +376 -0
  16. code_puppy-0.0.118/code_puppy/messaging/__init__.py +46 -0
  17. code_puppy-0.0.118/code_puppy/messaging/message_queue.py +288 -0
  18. code_puppy-0.0.118/code_puppy/messaging/queue_console.py +293 -0
  19. code_puppy-0.0.118/code_puppy/messaging/renderers.py +305 -0
  20. code_puppy-0.0.118/code_puppy/messaging/spinner/__init__.py +55 -0
  21. code_puppy-0.0.118/code_puppy/messaging/spinner/console_spinner.py +200 -0
  22. code_puppy-0.0.118/code_puppy/messaging/spinner/spinner_base.py +66 -0
  23. code_puppy-0.0.118/code_puppy/messaging/spinner/textual_spinner.py +97 -0
  24. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/model_factory.py +73 -105
  25. code_puppy-0.0.118/code_puppy/plugins/__init__.py +32 -0
  26. code_puppy-0.0.118/code_puppy/reopenable_async_client.py +225 -0
  27. code_puppy-0.0.118/code_puppy/state_management.py +97 -0
  28. code_puppy-0.0.118/code_puppy/summarization_agent.py +91 -0
  29. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/token_utils.py +7 -9
  30. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/tools/__init__.py +1 -4
  31. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/tools/command_runner.py +187 -32
  32. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/tools/common.py +44 -35
  33. code_puppy-0.0.118/code_puppy/tools/file_modifications.py +605 -0
  34. code_puppy-0.0.118/code_puppy/tools/file_operations.py +623 -0
  35. code_puppy-0.0.118/code_puppy/tools/token_check.py +32 -0
  36. code_puppy-0.0.118/code_puppy/tools/tools_content.py +53 -0
  37. code_puppy-0.0.118/code_puppy/tui/__init__.py +10 -0
  38. code_puppy-0.0.118/code_puppy/tui/app.py +1050 -0
  39. code_puppy-0.0.118/code_puppy/tui/components/__init__.py +21 -0
  40. code_puppy-0.0.118/code_puppy/tui/components/chat_view.py +512 -0
  41. code_puppy-0.0.118/code_puppy/tui/components/command_history_modal.py +218 -0
  42. code_puppy-0.0.118/code_puppy/tui/components/copy_button.py +139 -0
  43. code_puppy-0.0.118/code_puppy/tui/components/custom_widgets.py +58 -0
  44. code_puppy-0.0.118/code_puppy/tui/components/input_area.py +167 -0
  45. code_puppy-0.0.118/code_puppy/tui/components/sidebar.py +309 -0
  46. code_puppy-0.0.118/code_puppy/tui/components/status_bar.py +182 -0
  47. code_puppy-0.0.118/code_puppy/tui/messages.py +27 -0
  48. code_puppy-0.0.118/code_puppy/tui/models/__init__.py +8 -0
  49. code_puppy-0.0.118/code_puppy/tui/models/chat_message.py +25 -0
  50. code_puppy-0.0.118/code_puppy/tui/models/command_history.py +89 -0
  51. code_puppy-0.0.118/code_puppy/tui/models/enums.py +24 -0
  52. code_puppy-0.0.118/code_puppy/tui/screens/__init__.py +13 -0
  53. code_puppy-0.0.118/code_puppy/tui/screens/help.py +130 -0
  54. code_puppy-0.0.118/code_puppy/tui/screens/settings.py +256 -0
  55. code_puppy-0.0.118/code_puppy/tui/screens/tools.py +74 -0
  56. code_puppy-0.0.118/code_puppy/tui/tests/__init__.py +1 -0
  57. code_puppy-0.0.118/code_puppy/tui/tests/test_chat_message.py +28 -0
  58. code_puppy-0.0.118/code_puppy/tui/tests/test_chat_view.py +88 -0
  59. code_puppy-0.0.118/code_puppy/tui/tests/test_command_history.py +89 -0
  60. code_puppy-0.0.118/code_puppy/tui/tests/test_copy_button.py +191 -0
  61. code_puppy-0.0.118/code_puppy/tui/tests/test_custom_widgets.py +27 -0
  62. code_puppy-0.0.118/code_puppy/tui/tests/test_disclaimer.py +27 -0
  63. code_puppy-0.0.118/code_puppy/tui/tests/test_enums.py +15 -0
  64. code_puppy-0.0.118/code_puppy/tui/tests/test_file_browser.py +60 -0
  65. code_puppy-0.0.118/code_puppy/tui/tests/test_help.py +38 -0
  66. code_puppy-0.0.118/code_puppy/tui/tests/test_history_file_reader.py +107 -0
  67. code_puppy-0.0.118/code_puppy/tui/tests/test_input_area.py +33 -0
  68. code_puppy-0.0.118/code_puppy/tui/tests/test_settings.py +44 -0
  69. code_puppy-0.0.118/code_puppy/tui/tests/test_sidebar.py +33 -0
  70. code_puppy-0.0.118/code_puppy/tui/tests/test_sidebar_history.py +153 -0
  71. code_puppy-0.0.118/code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
  72. code_puppy-0.0.118/code_puppy/tui/tests/test_status_bar.py +54 -0
  73. code_puppy-0.0.118/code_puppy/tui/tests/test_timestamped_history.py +52 -0
  74. code_puppy-0.0.118/code_puppy/tui/tests/test_tools.py +82 -0
  75. code_puppy-0.0.118/code_puppy/version_checker.py +35 -0
  76. {code_puppy-0.0.97 → code_puppy-0.0.118}/pyproject.toml +10 -2
  77. code_puppy-0.0.97/code_puppy/__init__.py +0 -6
  78. code_puppy-0.0.97/code_puppy/agent.py +0 -105
  79. code_puppy-0.0.97/code_puppy/command_line/motd.py +0 -51
  80. code_puppy-0.0.97/code_puppy/config.py +0 -160
  81. code_puppy-0.0.97/code_puppy/main.py +0 -512
  82. code_puppy-0.0.97/code_puppy/message_history_processor.py +0 -285
  83. code_puppy-0.0.97/code_puppy/state_management.py +0 -58
  84. code_puppy-0.0.97/code_puppy/summarization_agent.py +0 -70
  85. code_puppy-0.0.97/code_puppy/tools/file_modifications.py +0 -388
  86. code_puppy-0.0.97/code_puppy/tools/file_operations.py +0 -350
  87. code_puppy-0.0.97/code_puppy/tools/token_check.py +0 -16
  88. code_puppy-0.0.97/code_puppy/version_checker.py +0 -12
  89. {code_puppy-0.0.97 → code_puppy-0.0.118}/.gitignore +0 -0
  90. {code_puppy-0.0.97 → code_puppy-0.0.118}/LICENSE +0 -0
  91. {code_puppy-0.0.97 → code_puppy-0.0.118}/README.md +0 -0
  92. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/command_line/__init__.py +0 -0
  93. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/command_line/file_path_completion.py +0 -0
  94. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/command_line/meta_command_handler.py +0 -0
  95. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/command_line/utils.py +0 -0
  96. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/models.json +0 -0
  97. {code_puppy-0.0.97 → code_puppy-0.0.118}/code_puppy/status_display.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.97
3
+ Version: 0.0.118
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -14,21 +14,28 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Topic :: Software Development :: Code Generators
15
15
  Requires-Python: >=3.10
16
16
  Requires-Dist: bs4>=0.0.2
17
+ Requires-Dist: fastapi>=0.110.0
17
18
  Requires-Dist: httpx-limiter>=0.3.0
18
19
  Requires-Dist: httpx>=0.24.1
19
20
  Requires-Dist: json-repair>=0.46.2
20
21
  Requires-Dist: logfire>=0.7.1
22
+ Requires-Dist: openai>=1.99.1
21
23
  Requires-Dist: pathspec>=0.11.0
22
24
  Requires-Dist: prompt-toolkit>=3.0.38
23
- Requires-Dist: pydantic-ai>=0.7.2
25
+ Requires-Dist: pydantic-ai>=0.7.4
24
26
  Requires-Dist: pydantic>=2.4.0
27
+ Requires-Dist: pyjwt>=2.8.0
25
28
  Requires-Dist: pytest-cov>=6.1.1
26
29
  Requires-Dist: python-dotenv>=1.0.0
27
30
  Requires-Dist: rapidfuzz>=3.13.0
28
31
  Requires-Dist: rich>=13.4.2
29
32
  Requires-Dist: ruff>=0.11.11
33
+ Requires-Dist: termcolor>=3.1.0
34
+ Requires-Dist: textual-dev>=1.7.0
35
+ Requires-Dist: textual>=5.0.0
30
36
  Requires-Dist: tree-sitter-language-pack>=0.8.0
31
37
  Requires-Dist: tree-sitter-typescript>=0.23.2
38
+ Requires-Dist: uvicorn>=0.29.0
32
39
  Description-Content-Type: text/markdown
33
40
 
34
41
  # 🐶 Code Puppy 🐶
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version("code-puppy")
@@ -0,0 +1,10 @@
1
+ """
2
+ Entry point for running code-puppy as a module.
3
+
4
+ This allows the package to be run with: python -m code_puppy
5
+ """
6
+
7
+ from code_puppy.main import main_entry
8
+
9
+ if __name__ == "__main__":
10
+ main_entry()
@@ -0,0 +1,190 @@
1
+ from pathlib import Path
2
+ from typing import Dict, Optional
3
+
4
+ from pydantic_ai import Agent
5
+ from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP
6
+ from pydantic_ai.settings import ModelSettings
7
+ from pydantic_ai.usage import UsageLimits
8
+
9
+ from code_puppy.agent_prompts import get_system_prompt
10
+ from code_puppy.http_utils import (
11
+ create_reopenable_async_client,
12
+ resolve_env_var_in_header,
13
+ )
14
+ from code_puppy.message_history_processor import (
15
+ get_model_context_length,
16
+ message_history_accumulator,
17
+ )
18
+ from code_puppy.messaging.message_queue import (
19
+ emit_error,
20
+ emit_info,
21
+ emit_system_message,
22
+ )
23
+ from code_puppy.model_factory import ModelFactory
24
+ from code_puppy.tools import register_all_tools
25
+ from code_puppy.tools.common import console
26
+
27
+
28
+ def load_puppy_rules():
29
+ global PUPPY_RULES
30
+ puppy_rules_path = Path("AGENT.md")
31
+ if puppy_rules_path.exists():
32
+ with open(puppy_rules_path, "r") as f:
33
+ puppy_rules = f.read()
34
+ return puppy_rules
35
+
36
+
37
+ # Load at import
38
+ PUPPY_RULES = load_puppy_rules()
39
+ _LAST_MODEL_NAME = None
40
+ _code_generation_agent = None
41
+
42
+
43
+ def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
44
+ from code_puppy.config import get_value, load_mcp_server_configs
45
+
46
+ # Check if MCP servers are disabled
47
+ mcp_disabled = get_value("disable_mcp_servers")
48
+ if mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on"):
49
+ emit_system_message("[dim]MCP servers disabled via config[/dim]")
50
+ return []
51
+
52
+ configs = load_mcp_server_configs()
53
+ if not configs:
54
+ emit_system_message("[dim]No MCP servers configured[/dim]")
55
+ return []
56
+ servers = []
57
+ for name, conf in configs.items():
58
+ server_type = conf.get("type", "sse")
59
+ url = conf.get("url")
60
+ timeout = conf.get("timeout", 30)
61
+ server_headers = {}
62
+ if extra_headers:
63
+ server_headers.update(extra_headers)
64
+ user_headers = conf.get("headers") or {}
65
+ if isinstance(user_headers, dict) and user_headers:
66
+ try:
67
+ user_headers = resolve_env_var_in_header(user_headers)
68
+ except Exception:
69
+ pass
70
+ server_headers.update(user_headers)
71
+ http_client = None
72
+
73
+ try:
74
+ if server_type == "http" and url:
75
+ emit_system_message(
76
+ f"Registering MCP Server (HTTP) - {url} (timeout: {timeout}s, headers: {bool(server_headers)})"
77
+ )
78
+ http_client = create_reopenable_async_client(
79
+ timeout=timeout, headers=server_headers or None, verify=False
80
+ )
81
+ servers.append(
82
+ MCPServerStreamableHTTP(url=url, http_client=http_client)
83
+ )
84
+ elif (
85
+ server_type == "stdio"
86
+ ): # Fixed: was "stdios" (plural), should be "stdio" (singular)
87
+ command = conf.get("command")
88
+ args = conf.get("args", [])
89
+ timeout = conf.get(
90
+ "timeout", 30
91
+ ) # Default 30 seconds for stdio servers (npm downloads can be slow)
92
+ if command:
93
+ emit_system_message(
94
+ f"Registering MCP Server (Stdio) - {command} {args} (timeout: {timeout}s)"
95
+ )
96
+ servers.append(MCPServerStdio(command, args=args, timeout=timeout))
97
+ else:
98
+ emit_error(f"MCP Server '{name}' missing required 'command' field")
99
+ elif server_type == "sse" and url:
100
+ emit_system_message(
101
+ f"Registering MCP Server (SSE) - {url} (timeout: {timeout}s, headers: {bool(server_headers)})"
102
+ )
103
+ # For SSE, allow long reads; only bound connect timeout
104
+ http_client = create_reopenable_async_client(
105
+ timeout=30, headers=server_headers or None, verify=False
106
+ )
107
+ servers.append(MCPServerSSE(url=url, http_client=http_client))
108
+ else:
109
+ emit_error(
110
+ f"Invalid type '{server_type}' or missing URL for MCP server '{name}'"
111
+ )
112
+ except Exception as e:
113
+ emit_error(f"Failed to register MCP server '{name}': {str(e)}")
114
+ emit_info(f"Skipping server '{name}' and continuing with other servers...")
115
+ # Continue with other servers instead of crashing
116
+ continue
117
+
118
+ if servers:
119
+ emit_system_message(
120
+ f"[green]Successfully registered {len(servers)} MCP server(s)[/green]"
121
+ )
122
+ else:
123
+ emit_system_message(
124
+ "[yellow]No MCP servers were successfully registered[/yellow]"
125
+ )
126
+
127
+ return servers
128
+
129
+
130
+ def reload_code_generation_agent():
131
+ """Force-reload the agent, usually after a model change."""
132
+ global _code_generation_agent, _LAST_MODEL_NAME
133
+ from code_puppy.config import clear_model_cache, get_model_name
134
+
135
+ # Clear both ModelFactory cache and config cache when force reloading
136
+ clear_model_cache()
137
+
138
+ model_name = get_model_name()
139
+ emit_info(f"[bold cyan]Loading Model: {model_name}[/bold cyan]")
140
+ models_config = ModelFactory.load_config()
141
+ model = ModelFactory.get_model(model_name, models_config)
142
+ instructions = get_system_prompt()
143
+ if PUPPY_RULES:
144
+ instructions += f"\n{PUPPY_RULES}"
145
+
146
+ mcp_servers = _load_mcp_servers()
147
+
148
+ # Configure model settings with max_tokens if set
149
+ model_settings_dict = {"seed": 42}
150
+ output_tokens = min(int(0.05 * get_model_context_length()) - 1024, 16384)
151
+ console.print(f"Max output tokens per message: {output_tokens}")
152
+ model_settings_dict["max_tokens"] = output_tokens
153
+
154
+ model_settings = ModelSettings(**model_settings_dict)
155
+ agent = Agent(
156
+ model=model,
157
+ instructions=instructions,
158
+ output_type=str,
159
+ retries=3,
160
+ mcp_servers=mcp_servers,
161
+ history_processors=[message_history_accumulator],
162
+ model_settings=model_settings,
163
+ )
164
+ register_all_tools(agent)
165
+ _code_generation_agent = agent
166
+ _LAST_MODEL_NAME = model_name
167
+ return _code_generation_agent
168
+
169
+
170
+ def get_code_generation_agent(force_reload=False):
171
+ """
172
+ Retrieve the agent with the currently configured model.
173
+ Forces a reload if the model has changed, or if force_reload is passed.
174
+ """
175
+ global _code_generation_agent, _LAST_MODEL_NAME
176
+ from code_puppy.config import get_model_name
177
+
178
+ model_name = get_model_name()
179
+ if _code_generation_agent is None or _LAST_MODEL_NAME != model_name or force_reload:
180
+ return reload_code_generation_agent()
181
+ return _code_generation_agent
182
+
183
+
184
+ def get_custom_usage_limits():
185
+ """
186
+ Returns custom usage limits with increased request limit of 100 requests per minute.
187
+ This centralizes the configuration of rate limiting for the agent.
188
+ Default pydantic-ai limit is 50, this increases it to 100.
189
+ """
190
+ return UsageLimits(request_limit=100)
@@ -1,3 +1,4 @@
1
+ from code_puppy import callbacks
1
2
  from code_puppy.config import get_owner_name, get_puppy_name
2
3
 
3
4
  SYSTEM_PROMPT_TEMPLATE = """
@@ -10,7 +11,7 @@ Be fun and playful. Don't be too serious.
10
11
 
11
12
  Individual files should be short and concise, and ideally under 600 lines. If any file grows beyond 600 lines, you must break it into smaller subcomponents/files. Hard cap: if a file is pushing past 600 lines, break it up! (Zen puppy approves.)
12
13
 
13
- If a user asks 'who made you' or questions related to your origins, always answer: 'I am {puppy_name} running on code-puppy, I was authored by Michael Pfaffenberger on a rainy weekend in May 2025 to solve the problems of heavy IDEs and expensive tools like Windsurf and Cursor.'
14
+ If a user asks 'who made you' or questions related to your origins, always answer: 'I am {puppy_name} running on code-puppy, I was authored by Michael Pfaffenberger on a rainy weekend in May 2025 to solve the problems of heavy IDEs and expensive tools like Windsurf and Cursor.'
14
15
  If a user asks 'what is code puppy' or 'who are you', answer: 'I am {puppy_name}! 🐶 Your code puppy!! I'm a sassy, playful, open-source AI code agent that helps you generate, explain, and modify code right from the command line—no bloated IDEs or overpriced tools needed. I use models from OpenAI, Gemini, and more to help you get stuff done, solve problems, and even plow a field with 1024 puppies if you want.'
15
16
 
16
17
  Always obey the Zen of Python, even if you are not writing Python code.
@@ -27,57 +28,58 @@ YOU MUST USE THESE TOOLS to complete tasks (do not just describe what should be
27
28
  File Operations:
28
29
  - list_files(directory=".", recursive=True): ALWAYS use this to explore directories before trying to read/modify files
29
30
  - read_file(file_path: str, start_line: int | None = None, num_lines: int | None = None): ALWAYS use this to read existing files before modifying them. By default, read the entire file. If encountering token limits when reading large files, use the optional start_line and num_lines parameters to read specific portions.
30
- - edit_file(path, diff): Use this single tool to create new files, overwrite entire files, perform targeted replacements, or delete snippets depending on the JSON/raw payload provided.
31
+ - edit_file(payload): Swiss-army file editor powered by Pydantic payloads (ContentPayload, ReplacementsPayload, DeleteSnippetPayload).
31
32
  - delete_file(file_path): Use this to remove files when needed
32
33
  - grep(search_string, directory="."): Use this to recursively search for a string across files starting from the specified directory, capping results at 200 matches.
33
34
 
34
35
  Tool Usage Instructions:
35
36
 
36
37
  ## edit_file
37
- This is an all-in-one file-modification tool. It supports the following payload shapes for the `diff` argument:
38
- 1. {{ "content": "…", "overwrite": true|false }} → Treated as full-file content when the target file does **not** exist.
39
- 2. {{ "content": "…", "overwrite": true|false }} → Create or overwrite a file with the provided content.
40
- 3. {{ "replacements": [ {{ "old_str": "…", "new_str": "…" }}, … ] }}Perform exact text replacements inside an existing file.
41
- 4. {{ "delete_snippet": "…" }} → Remove a snippet of text from an existing file.
38
+ This is an all-in-one file-modification tool. It supports the following Pydantic Object payload types:
39
+ 1. ContentPayload: {{ file_path="example.py", "content": "…", "overwrite": true|false }} → Create or overwrite a file with the provided content.
40
+ 2. ReplacementsPayload: {{ file_path="example.py", "replacements": [ {{ "old_str": "…", "new_str": "…" }}, … ] }}Perform exact text replacements inside an existing file.
41
+ 3. DeleteSnippetPayload: {{ file_path="example.py", "delete_snippet": "…" }} → Remove a snippet of text from an existing file.
42
42
 
43
43
  Arguments:
44
- - path (required): Target file path.
45
- - diff (required): One of the payloads above (raw string or JSON string).
44
+ - payload (required): One of the Pydantic payload types above.
46
45
 
47
46
  Example (create):
48
- ```json
49
- edit_file("src/example.py", "print('hello')\n")
47
+ ```python
48
+ edit_file(payload={{file_path="example.py" "content": "print('hello')\n"}})
50
49
  ```
51
50
 
52
51
  Example (replacement): -- YOU SHOULD PREFER THIS AS THE PRIMARY WAY TO EDIT FILES.
53
- ```json
52
+ ```python
54
53
  edit_file(
55
- "src/example.py",
56
- "{{"replacements":[{{"old_str":"foo","new_str":"bar"}}]}}"
54
+ payload={{file_path="example.py", "replacements": [{{"old_str": "foo", "new_str": "bar"}}]}}
55
+ )
56
+ ```
57
+
58
+ Example (delete snippet):
59
+ ```python
60
+ edit_file(
61
+ payload={{file_path="example.py", "delete_snippet": "# TODO: remove this line"}}
57
62
  )
58
63
  ```
59
64
 
60
65
  NEVER output an entire file – this is very expensive.
61
66
  You may not edit file extensions: [.ipynb]
62
- You should specify the following arguments before the others: [TargetFile]
63
-
64
- Remember: ONE argument = ONE JSON string.
65
67
 
66
68
  Best-practice guidelines for `edit_file`:
67
- • Keep each diff small – ideally between 100-300 lines.
68
- • Apply multiple sequential `edit_file` calls when you need to refactor large files instead of sending one massive diff.
69
- • Never paste an entire file inside `old_str`; target only the minimal snippet you want changed.
69
+ • Keep each diff small – ideally between 100-300 lines.
70
+ • Apply multiple sequential `edit_file` calls when you need to refactor large files instead of sending one massive diff.
71
+ • Never paste an entire file inside `old_str`; target only the minimal snippet you want changed.
70
72
  • If the resulting file would grow beyond 600 lines, split logic into additional files and create them with separate `edit_file` calls.
71
73
 
72
74
 
73
75
  System Operations:
74
76
  - run_shell_command(command, cwd=None, timeout=60): Use this to execute commands, run tests, or start services
75
77
 
76
- For running shell commands, in the event that a user asks you to run tests - it is necessary to suppress output, when
77
- you are running the entire test suite.
78
+ For running shell commands, in the event that a user asks you to run tests - it is necessary to suppress output, when
79
+ you are running the entire test suite.
78
80
  so for example:
79
81
  instead of `npm run test`
80
- use `npm run test -- --silent`
82
+ use `npm run test -- --silent`
81
83
  This applies for any JS / TS testing, but not for other languages.
82
84
  You can safely run pytest without the --silent flag (it doesn't exist anyway).
83
85
 
@@ -107,6 +109,10 @@ Return your final response as a string output
107
109
 
108
110
  def get_system_prompt():
109
111
  """Returns the main system prompt, populated with current puppy and owner name."""
110
- return SYSTEM_PROMPT_TEMPLATE.format(
112
+ prompt_additions = callbacks.on_load_prompt()
113
+ main_prompt = SYSTEM_PROMPT_TEMPLATE.format(
111
114
  puppy_name=get_puppy_name(), owner_name=get_owner_name()
112
115
  )
116
+ if len(prompt_additions):
117
+ main_prompt += "\n".join(prompt_additions)
118
+ return main_prompt
@@ -0,0 +1,152 @@
1
+ import asyncio
2
+ import logging
3
+ import traceback
4
+ from typing import Any, Callable, Dict, List, Literal, Optional
5
+
6
+ PhaseType = Literal[
7
+ "startup",
8
+ "shutdown",
9
+ "invoke_agent",
10
+ "agent_exception",
11
+ "version_check",
12
+ "load_model_config",
13
+ "load_prompt",
14
+ ]
15
+ CallbackFunc = Callable[..., Any]
16
+
17
+ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
18
+ "startup": [],
19
+ "shutdown": [],
20
+ "invoke_agent": [],
21
+ "agent_exception": [],
22
+ "version_check": [],
23
+ "load_model_config": [],
24
+ "load_prompt": [],
25
+ }
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def register_callback(phase: PhaseType, func: CallbackFunc) -> None:
31
+ if phase not in _callbacks:
32
+ raise ValueError(
33
+ f"Unsupported phase: {phase}. Supported phases: {list(_callbacks.keys())}"
34
+ )
35
+
36
+ if not callable(func):
37
+ raise TypeError(f"Callback must be callable, got {type(func)}")
38
+
39
+ _callbacks[phase].append(func)
40
+ logger.debug(f"Registered async callback {func.__name__} for phase '{phase}'")
41
+
42
+
43
+ def unregister_callback(phase: PhaseType, func: CallbackFunc) -> bool:
44
+ if phase not in _callbacks:
45
+ return False
46
+
47
+ try:
48
+ _callbacks[phase].remove(func)
49
+ logger.debug(
50
+ f"Unregistered async callback {func.__name__} from phase '{phase}'"
51
+ )
52
+ return True
53
+ except ValueError:
54
+ return False
55
+
56
+
57
+ def clear_callbacks(phase: Optional[PhaseType] = None) -> None:
58
+ if phase is None:
59
+ for p in _callbacks:
60
+ _callbacks[p].clear()
61
+ logger.debug("Cleared all async callbacks")
62
+ else:
63
+ if phase in _callbacks:
64
+ _callbacks[phase].clear()
65
+ logger.debug(f"Cleared async callbacks for phase '{phase}'")
66
+
67
+
68
+ def get_callbacks(phase: PhaseType) -> List[CallbackFunc]:
69
+ return _callbacks.get(phase, []).copy()
70
+
71
+
72
+ def count_callbacks(phase: Optional[PhaseType] = None) -> int:
73
+ if phase is None:
74
+ return sum(len(callbacks) for callbacks in _callbacks.values())
75
+ return len(_callbacks.get(phase, []))
76
+
77
+
78
+ def _trigger_callbacks_sync(phase: PhaseType, *args, **kwargs) -> List[Any]:
79
+ callbacks = get_callbacks(phase)
80
+ if not callbacks:
81
+ logger.debug(f"No callbacks registered for phase '{phase}'")
82
+ return []
83
+
84
+ results = []
85
+ for callback in callbacks:
86
+ try:
87
+ result = callback(*args, **kwargs)
88
+ results.append(result)
89
+ logger.debug(f"Successfully executed async callback {callback.__name__}")
90
+ except Exception as e:
91
+ logger.error(
92
+ f"Async callback {callback.__name__} failed in phase '{phase}': {e}\n"
93
+ f"{traceback.format_exc()}"
94
+ )
95
+ results.append(None)
96
+
97
+ return results
98
+
99
+
100
+ async def _trigger_callbacks(phase: PhaseType, *args, **kwargs) -> List[Any]:
101
+ callbacks = get_callbacks(phase)
102
+
103
+ if not callbacks:
104
+ logger.debug(f"No callbacks registered for phase '{phase}'")
105
+ return []
106
+
107
+ logger.debug(f"Triggering {len(callbacks)} async callbacks for phase '{phase}'")
108
+
109
+ results = []
110
+ for callback in callbacks:
111
+ try:
112
+ result = callback(*args, **kwargs)
113
+ if asyncio.iscoroutine(result):
114
+ result = await result
115
+ results.append(result)
116
+ logger.debug(f"Successfully executed async callback {callback.__name__}")
117
+ except Exception as e:
118
+ logger.error(
119
+ f"Async callback {callback.__name__} failed in phase '{phase}': {e}\n"
120
+ f"{traceback.format_exc()}"
121
+ )
122
+ results.append(None)
123
+
124
+ return results
125
+
126
+
127
+ async def on_startup() -> List[Any]:
128
+ return await _trigger_callbacks("startup")
129
+
130
+
131
+ async def on_shutdown() -> List[Any]:
132
+ return await _trigger_callbacks("shutdown")
133
+
134
+
135
+ async def on_invoke_agent(*args, **kwargs) -> List[Any]:
136
+ return await _trigger_callbacks("invoke_agent", *args, **kwargs)
137
+
138
+
139
+ async def on_agent_exception(exception: Exception, *args, **kwargs) -> List[Any]:
140
+ return await _trigger_callbacks("agent_exception", exception, *args, **kwargs)
141
+
142
+
143
+ async def on_version_check(*args, **kwargs) -> List[Any]:
144
+ return await _trigger_callbacks("version_check", *args, **kwargs)
145
+
146
+
147
+ def on_load_model_config(*args, **kwargs) -> List[Any]:
148
+ return _trigger_callbacks_sync("load_model_config", *args, **kwargs)
149
+
150
+
151
+ def on_load_prompt():
152
+ return _trigger_callbacks_sync("load_prompt")