code-puppy 0.0.369__py3-none-any.whl → 0.0.371__py3-none-any.whl
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.
- code_puppy/agents/__init__.py +6 -0
- code_puppy/agents/agent_manager.py +205 -1
- code_puppy/agents/base_agent.py +2 -12
- code_puppy/chatgpt_codex_client.py +53 -0
- code_puppy/command_line/agent_menu.py +281 -14
- code_puppy/command_line/core_commands.py +3 -3
- code_puppy/command_line/model_picker_completion.py +3 -20
- code_puppy/config.py +44 -8
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +1 -52
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
- {code_puppy-0.0.369.dist-info → code_puppy-0.0.371.dist-info}/METADATA +2 -2
- {code_puppy-0.0.369.dist-info → code_puppy-0.0.371.dist-info}/RECORD +20 -20
- code_puppy/prompts/codex_system_prompt.md +0 -310
- {code_puppy-0.0.369.data → code_puppy-0.0.371.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.369.data → code_puppy-0.0.371.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.369.dist-info → code_puppy-0.0.371.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.369.dist-info → code_puppy-0.0.371.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.369.dist-info → code_puppy-0.0.371.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,11 +16,22 @@ from prompt_toolkit.layout.controls import FormattedTextControl
|
|
|
16
16
|
from prompt_toolkit.widgets import Frame
|
|
17
17
|
|
|
18
18
|
from code_puppy.agents import (
|
|
19
|
+
clone_agent,
|
|
20
|
+
delete_clone_agent,
|
|
19
21
|
get_agent_descriptions,
|
|
20
22
|
get_available_agents,
|
|
21
23
|
get_current_agent,
|
|
24
|
+
is_clone_agent_name,
|
|
22
25
|
)
|
|
26
|
+
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
27
|
+
from code_puppy.config import (
|
|
28
|
+
clear_agent_pinned_model,
|
|
29
|
+
get_agent_pinned_model,
|
|
30
|
+
set_agent_pinned_model,
|
|
31
|
+
)
|
|
32
|
+
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
23
33
|
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
34
|
+
from code_puppy.tools.common import arrow_select_async
|
|
24
35
|
|
|
25
36
|
PAGE_SIZE = 10 # Agents per page
|
|
26
37
|
|
|
@@ -87,6 +98,166 @@ def _sanitize_display_text(text: str) -> str:
|
|
|
87
98
|
return cleaned
|
|
88
99
|
|
|
89
100
|
|
|
101
|
+
def _get_pinned_model(agent_name: str) -> Optional[str]:
|
|
102
|
+
"""Return the pinned model for an agent, if any.
|
|
103
|
+
|
|
104
|
+
Checks both built-in agent config and JSON agent files.
|
|
105
|
+
"""
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
# First check built-in agent config
|
|
109
|
+
try:
|
|
110
|
+
pinned = get_agent_pinned_model(agent_name)
|
|
111
|
+
if pinned:
|
|
112
|
+
return pinned
|
|
113
|
+
except Exception:
|
|
114
|
+
pass # Continue to check JSON agents
|
|
115
|
+
|
|
116
|
+
# Check if it's a JSON agent
|
|
117
|
+
try:
|
|
118
|
+
from code_puppy.agents.json_agent import discover_json_agents
|
|
119
|
+
|
|
120
|
+
json_agents = discover_json_agents()
|
|
121
|
+
if agent_name in json_agents:
|
|
122
|
+
agent_file_path = json_agents[agent_name]
|
|
123
|
+
with open(agent_file_path, "r", encoding="utf-8") as f:
|
|
124
|
+
agent_config = json.load(f)
|
|
125
|
+
model = agent_config.get("model")
|
|
126
|
+
return model if model else None
|
|
127
|
+
except Exception:
|
|
128
|
+
pass # Return None if we can't read the JSON file
|
|
129
|
+
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_model_picker_choices(
|
|
134
|
+
pinned_model: Optional[str],
|
|
135
|
+
model_names: List[str],
|
|
136
|
+
) -> List[str]:
|
|
137
|
+
"""Build model picker choices with pinned/unpin indicators."""
|
|
138
|
+
choices = ["✓ (unpin)" if not pinned_model else " (unpin)"]
|
|
139
|
+
|
|
140
|
+
for model_name in model_names:
|
|
141
|
+
if model_name == pinned_model:
|
|
142
|
+
choices.append(f"✓ {model_name} (pinned)")
|
|
143
|
+
else:
|
|
144
|
+
choices.append(f" {model_name}")
|
|
145
|
+
|
|
146
|
+
return choices
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _normalize_model_choice(choice: str) -> str:
|
|
150
|
+
"""Normalize a picker choice into a model name or '(unpin)' string."""
|
|
151
|
+
cleaned = choice.strip()
|
|
152
|
+
if cleaned.startswith("✓"):
|
|
153
|
+
cleaned = cleaned.lstrip("✓").strip()
|
|
154
|
+
if cleaned.endswith(" (pinned)"):
|
|
155
|
+
cleaned = cleaned[: -len(" (pinned)")].strip()
|
|
156
|
+
return cleaned
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def _select_pinned_model(agent_name: str) -> Optional[str]:
|
|
160
|
+
"""Prompt for a model to pin to the agent."""
|
|
161
|
+
try:
|
|
162
|
+
model_names = load_model_names() or []
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
emit_warning(f"Failed to load models: {exc}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
pinned_model = _get_pinned_model(agent_name)
|
|
168
|
+
choices = _build_model_picker_choices(pinned_model, model_names)
|
|
169
|
+
if not choices:
|
|
170
|
+
emit_warning("No models available to pin.")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
choice = await arrow_select_async(
|
|
175
|
+
f"Select a model to pin for '{agent_name}'",
|
|
176
|
+
choices,
|
|
177
|
+
)
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
emit_info("Model pinning cancelled")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return _normalize_model_choice(choice)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _reload_agent_if_current(
|
|
186
|
+
agent_name: str,
|
|
187
|
+
pinned_model: Optional[str],
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Reload the current agent when its pinned model changes."""
|
|
190
|
+
current_agent = get_current_agent()
|
|
191
|
+
if not current_agent or current_agent.name != agent_name:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
if hasattr(current_agent, "refresh_config"):
|
|
196
|
+
current_agent.refresh_config()
|
|
197
|
+
current_agent.reload_code_generation_agent()
|
|
198
|
+
if pinned_model:
|
|
199
|
+
emit_info(f"Active agent reloaded with pinned model '{pinned_model}'")
|
|
200
|
+
else:
|
|
201
|
+
emit_info("Active agent reloaded with default model")
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
emit_warning(f"Pinned model applied but reload failed: {exc}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _apply_pinned_model(agent_name: str, model_choice: str) -> None:
|
|
207
|
+
"""Persist a pinned model selection for an agent.
|
|
208
|
+
|
|
209
|
+
Handles both built-in agents (via config) and JSON agents (via JSON file).
|
|
210
|
+
"""
|
|
211
|
+
import json
|
|
212
|
+
|
|
213
|
+
# Check if this is a JSON agent or a built-in agent
|
|
214
|
+
try:
|
|
215
|
+
from code_puppy.agents.json_agent import discover_json_agents
|
|
216
|
+
|
|
217
|
+
json_agents = discover_json_agents()
|
|
218
|
+
is_json_agent = agent_name in json_agents
|
|
219
|
+
except Exception:
|
|
220
|
+
is_json_agent = False
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
if is_json_agent:
|
|
224
|
+
# Handle JSON agent - modify the JSON file
|
|
225
|
+
agent_file_path = json_agents[agent_name]
|
|
226
|
+
|
|
227
|
+
with open(agent_file_path, "r", encoding="utf-8") as f:
|
|
228
|
+
agent_config = json.load(f)
|
|
229
|
+
|
|
230
|
+
if model_choice == "(unpin)":
|
|
231
|
+
# Remove the model key if it exists
|
|
232
|
+
if "model" in agent_config:
|
|
233
|
+
del agent_config["model"]
|
|
234
|
+
emit_success(f"Model pin cleared for '{agent_name}'")
|
|
235
|
+
pinned_model = None
|
|
236
|
+
else:
|
|
237
|
+
# Set the model
|
|
238
|
+
agent_config["model"] = model_choice
|
|
239
|
+
emit_success(f"Pinned '{model_choice}' to '{agent_name}'")
|
|
240
|
+
pinned_model = model_choice
|
|
241
|
+
|
|
242
|
+
# Save the updated configuration
|
|
243
|
+
with open(agent_file_path, "w", encoding="utf-8") as f:
|
|
244
|
+
json.dump(agent_config, f, indent=2, ensure_ascii=False)
|
|
245
|
+
else:
|
|
246
|
+
# Handle built-in Python agent - use config functions
|
|
247
|
+
if model_choice == "(unpin)":
|
|
248
|
+
clear_agent_pinned_model(agent_name)
|
|
249
|
+
emit_success(f"Model pin cleared for '{agent_name}'")
|
|
250
|
+
pinned_model = None
|
|
251
|
+
else:
|
|
252
|
+
set_agent_pinned_model(agent_name, model_choice)
|
|
253
|
+
emit_success(f"Pinned '{model_choice}' to '{agent_name}'")
|
|
254
|
+
pinned_model = model_choice
|
|
255
|
+
|
|
256
|
+
_reload_agent_if_current(agent_name, pinned_model)
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
emit_warning(f"Failed to apply pinned model: {exc}")
|
|
259
|
+
|
|
260
|
+
|
|
90
261
|
def _get_agent_entries() -> List[Tuple[str, str, str]]:
|
|
91
262
|
"""Get all agents with their display names and descriptions.
|
|
92
263
|
|
|
@@ -141,6 +312,7 @@ def _render_menu_panel(
|
|
|
141
312
|
name, display_name, _ = entries[i]
|
|
142
313
|
is_selected = i == selected_idx
|
|
143
314
|
is_current = name == current_agent_name
|
|
315
|
+
pinned_model = _get_pinned_model(name)
|
|
144
316
|
|
|
145
317
|
# Sanitize display name to avoid emoji rendering issues
|
|
146
318
|
safe_display_name = _sanitize_display_text(display_name)
|
|
@@ -153,6 +325,10 @@ def _render_menu_panel(
|
|
|
153
325
|
lines.append(("", " "))
|
|
154
326
|
lines.append(("", safe_display_name))
|
|
155
327
|
|
|
328
|
+
if pinned_model:
|
|
329
|
+
safe_pinned_model = _sanitize_display_text(pinned_model)
|
|
330
|
+
lines.append(("fg:ansiyellow", f" → {safe_pinned_model}"))
|
|
331
|
+
|
|
156
332
|
# Add current marker
|
|
157
333
|
if is_current:
|
|
158
334
|
lines.append(("fg:ansicyan", " ← current"))
|
|
@@ -167,6 +343,12 @@ def _render_menu_panel(
|
|
|
167
343
|
lines.append(("", "Page\n"))
|
|
168
344
|
lines.append(("fg:green", " Enter "))
|
|
169
345
|
lines.append(("", "Select\n"))
|
|
346
|
+
lines.append(("fg:ansibrightblack", " P "))
|
|
347
|
+
lines.append(("", "Pin model\n"))
|
|
348
|
+
lines.append(("fg:ansibrightblack", " C "))
|
|
349
|
+
lines.append(("", "Clone\n"))
|
|
350
|
+
lines.append(("fg:ansibrightblack", " D "))
|
|
351
|
+
lines.append(("", "Delete clone\n"))
|
|
170
352
|
lines.append(("fg:ansibrightred", " Ctrl+C "))
|
|
171
353
|
lines.append(("", "Cancel"))
|
|
172
354
|
|
|
@@ -198,6 +380,7 @@ def _render_preview_panel(
|
|
|
198
380
|
|
|
199
381
|
name, display_name, description = entry
|
|
200
382
|
is_current = name == current_agent_name
|
|
383
|
+
pinned_model = _get_pinned_model(name)
|
|
201
384
|
|
|
202
385
|
# Sanitize text to avoid emoji rendering issues
|
|
203
386
|
safe_display_name = _sanitize_display_text(display_name)
|
|
@@ -213,6 +396,15 @@ def _render_preview_panel(
|
|
|
213
396
|
lines.append(("fg:ansicyan", safe_display_name))
|
|
214
397
|
lines.append(("", "\n\n"))
|
|
215
398
|
|
|
399
|
+
# Pinned model
|
|
400
|
+
lines.append(("bold", "Pinned Model: "))
|
|
401
|
+
if pinned_model:
|
|
402
|
+
safe_pinned_model = _sanitize_display_text(pinned_model)
|
|
403
|
+
lines.append(("fg:ansiyellow", safe_pinned_model))
|
|
404
|
+
else:
|
|
405
|
+
lines.append(("fg:ansibrightblack", "default"))
|
|
406
|
+
lines.append(("", "\n\n"))
|
|
407
|
+
|
|
216
408
|
# Description
|
|
217
409
|
lines.append(("bold", "Description:"))
|
|
218
410
|
lines.append(("", "\n"))
|
|
@@ -261,8 +453,6 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
261
453
|
current_agent_name = current_agent.name if current_agent else ""
|
|
262
454
|
|
|
263
455
|
if not entries:
|
|
264
|
-
from code_puppy.messaging import emit_info
|
|
265
|
-
|
|
266
456
|
emit_info("No agents found.")
|
|
267
457
|
return None
|
|
268
458
|
|
|
@@ -270,14 +460,38 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
270
460
|
selected_idx = [0] # Current selection (global index)
|
|
271
461
|
current_page = [0] # Current page
|
|
272
462
|
result = [None] # Selected agent name
|
|
463
|
+
pending_action = [None] # 'pin', 'clone', 'delete', or None
|
|
273
464
|
|
|
274
|
-
total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
465
|
+
total_pages = [max(1, (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE)]
|
|
275
466
|
|
|
276
467
|
def get_current_entry() -> Optional[Tuple[str, str, str]]:
|
|
277
468
|
if 0 <= selected_idx[0] < len(entries):
|
|
278
469
|
return entries[selected_idx[0]]
|
|
279
470
|
return None
|
|
280
471
|
|
|
472
|
+
def refresh_entries(selected_name: Optional[str] = None) -> None:
|
|
473
|
+
nonlocal entries
|
|
474
|
+
|
|
475
|
+
entries = _get_agent_entries()
|
|
476
|
+
total_pages[0] = max(1, (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE)
|
|
477
|
+
|
|
478
|
+
if not entries:
|
|
479
|
+
selected_idx[0] = 0
|
|
480
|
+
current_page[0] = 0
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
if selected_name:
|
|
484
|
+
for idx, (name, _, _) in enumerate(entries):
|
|
485
|
+
if name == selected_name:
|
|
486
|
+
selected_idx[0] = idx
|
|
487
|
+
break
|
|
488
|
+
else:
|
|
489
|
+
selected_idx[0] = min(selected_idx[0], len(entries) - 1)
|
|
490
|
+
else:
|
|
491
|
+
selected_idx[0] = min(selected_idx[0], len(entries) - 1)
|
|
492
|
+
|
|
493
|
+
current_page[0] = selected_idx[0] // PAGE_SIZE
|
|
494
|
+
|
|
281
495
|
# Build UI
|
|
282
496
|
menu_control = FormattedTextControl(text="")
|
|
283
497
|
preview_control = FormattedTextControl(text="")
|
|
@@ -336,11 +550,29 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
336
550
|
|
|
337
551
|
@kb.add("right")
|
|
338
552
|
def _(event):
|
|
339
|
-
if current_page[0] < total_pages - 1:
|
|
553
|
+
if current_page[0] < total_pages[0] - 1:
|
|
340
554
|
current_page[0] += 1
|
|
341
555
|
selected_idx[0] = current_page[0] * PAGE_SIZE
|
|
342
556
|
update_display()
|
|
343
557
|
|
|
558
|
+
@kb.add("p")
|
|
559
|
+
def _(event):
|
|
560
|
+
if get_current_entry():
|
|
561
|
+
pending_action[0] = "pin"
|
|
562
|
+
event.app.exit()
|
|
563
|
+
|
|
564
|
+
@kb.add("c")
|
|
565
|
+
def _(event):
|
|
566
|
+
if get_current_entry():
|
|
567
|
+
pending_action[0] = "clone"
|
|
568
|
+
event.app.exit()
|
|
569
|
+
|
|
570
|
+
@kb.add("d")
|
|
571
|
+
def _(event):
|
|
572
|
+
if get_current_entry():
|
|
573
|
+
pending_action[0] = "delete"
|
|
574
|
+
event.app.exit()
|
|
575
|
+
|
|
344
576
|
@kb.add("enter")
|
|
345
577
|
def _(event):
|
|
346
578
|
entry = get_current_entry()
|
|
@@ -370,15 +602,52 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
370
602
|
time.sleep(0.05)
|
|
371
603
|
|
|
372
604
|
try:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
sys.stdout.write("\033[2J\033[H")
|
|
378
|
-
sys.stdout.flush()
|
|
605
|
+
while True:
|
|
606
|
+
pending_action[0] = None
|
|
607
|
+
result[0] = None
|
|
608
|
+
update_display()
|
|
379
609
|
|
|
380
|
-
|
|
381
|
-
|
|
610
|
+
# Clear the current buffer
|
|
611
|
+
sys.stdout.write("\033[2J\033[H")
|
|
612
|
+
sys.stdout.flush()
|
|
613
|
+
|
|
614
|
+
# Run application
|
|
615
|
+
await app.run_async()
|
|
616
|
+
|
|
617
|
+
if pending_action[0] == "pin":
|
|
618
|
+
entry = get_current_entry()
|
|
619
|
+
if entry:
|
|
620
|
+
selected_model = await _select_pinned_model(entry[0])
|
|
621
|
+
if selected_model:
|
|
622
|
+
_apply_pinned_model(entry[0], selected_model)
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
if pending_action[0] == "clone":
|
|
626
|
+
entry = get_current_entry()
|
|
627
|
+
selected_name = None
|
|
628
|
+
if entry:
|
|
629
|
+
cloned_name = clone_agent(entry[0])
|
|
630
|
+
selected_name = cloned_name or entry[0]
|
|
631
|
+
refresh_entries(selected_name=selected_name)
|
|
632
|
+
continue
|
|
633
|
+
|
|
634
|
+
if pending_action[0] == "delete":
|
|
635
|
+
entry = get_current_entry()
|
|
636
|
+
selected_name = None
|
|
637
|
+
if entry:
|
|
638
|
+
agent_name = entry[0]
|
|
639
|
+
selected_name = agent_name
|
|
640
|
+
if not is_clone_agent_name(agent_name):
|
|
641
|
+
emit_warning("Only cloned agents can be deleted.")
|
|
642
|
+
elif agent_name == current_agent_name:
|
|
643
|
+
emit_warning("Cannot delete the active agent. Switch first.")
|
|
644
|
+
else:
|
|
645
|
+
if delete_clone_agent(agent_name):
|
|
646
|
+
selected_name = None
|
|
647
|
+
refresh_entries(selected_name=selected_name)
|
|
648
|
+
continue
|
|
649
|
+
|
|
650
|
+
break
|
|
382
651
|
|
|
383
652
|
finally:
|
|
384
653
|
# Exit alternate screen buffer once at end
|
|
@@ -388,8 +657,6 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
388
657
|
set_awaiting_user_input(False)
|
|
389
658
|
|
|
390
659
|
# Clear exit message
|
|
391
|
-
from code_puppy.messaging import emit_info
|
|
392
|
-
|
|
393
660
|
emit_info("✓ Exited agent picker")
|
|
394
661
|
|
|
395
662
|
return result[0]
|
|
@@ -169,7 +169,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
169
169
|
reset_onboarding,
|
|
170
170
|
run_onboarding_wizard,
|
|
171
171
|
)
|
|
172
|
-
from code_puppy.
|
|
172
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
173
173
|
|
|
174
174
|
# Always reset so user can re-run the tutorial anytime
|
|
175
175
|
reset_onboarding()
|
|
@@ -184,7 +184,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
184
184
|
from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
|
|
185
185
|
|
|
186
186
|
run_oauth_flow()
|
|
187
|
-
|
|
187
|
+
set_model_and_reload_agent("chatgpt-gpt-5.2-codex")
|
|
188
188
|
elif result == "claude":
|
|
189
189
|
emit_info("🔐 Starting Claude Code OAuth flow...")
|
|
190
190
|
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
@@ -192,7 +192,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
192
192
|
)
|
|
193
193
|
|
|
194
194
|
_perform_authentication()
|
|
195
|
-
|
|
195
|
+
set_model_and_reload_agent("claude-code-claude-opus-4-5-20251101")
|
|
196
196
|
elif result == "completed":
|
|
197
197
|
emit_info("🎉 Tutorial complete! Happy coding!")
|
|
198
198
|
elif result == "skipped":
|
|
@@ -6,8 +6,9 @@ from prompt_toolkit.completion import Completer, Completion
|
|
|
6
6
|
from prompt_toolkit.document import Document
|
|
7
7
|
from prompt_toolkit.history import FileHistory
|
|
8
8
|
|
|
9
|
-
from code_puppy.config import get_global_model_name
|
|
9
|
+
from code_puppy.config import get_global_model_name
|
|
10
10
|
from code_puppy.model_factory import ModelFactory
|
|
11
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def load_model_names():
|
|
@@ -28,25 +29,7 @@ def set_active_model(model_name: str):
|
|
|
28
29
|
"""
|
|
29
30
|
Sets the active model name by updating the config (for persistence).
|
|
30
31
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
set_model_name(model_name)
|
|
34
|
-
# Reload the currently active agent so the new model takes effect immediately
|
|
35
|
-
try:
|
|
36
|
-
from code_puppy.agents import get_current_agent
|
|
37
|
-
|
|
38
|
-
current_agent = get_current_agent()
|
|
39
|
-
# JSON agents may need to refresh their config before reload
|
|
40
|
-
if hasattr(current_agent, "refresh_config"):
|
|
41
|
-
try:
|
|
42
|
-
current_agent.refresh_config()
|
|
43
|
-
except Exception:
|
|
44
|
-
# Non-fatal, continue to reload
|
|
45
|
-
...
|
|
46
|
-
current_agent.reload_code_generation_agent()
|
|
47
|
-
emit_info("Active agent reloaded")
|
|
48
|
-
except Exception as e:
|
|
49
|
-
emit_warning(f"Model changed but agent reload failed: {e}")
|
|
32
|
+
set_model_and_reload_agent(model_name)
|
|
50
33
|
|
|
51
34
|
|
|
52
35
|
class ModelNameCompleter(Completer):
|
code_puppy/config.py
CHANGED
|
@@ -123,6 +123,9 @@ REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
|
123
123
|
# Runtime-only autosave session ID (per-process)
|
|
124
124
|
_CURRENT_AUTOSAVE_ID: Optional[str] = None
|
|
125
125
|
|
|
126
|
+
# Session-local model name (initialized from file on first access, then cached)
|
|
127
|
+
_SESSION_MODEL: Optional[str] = None
|
|
128
|
+
|
|
126
129
|
# Cache containers for model validation and defaults
|
|
127
130
|
_model_validation_cache = {}
|
|
128
131
|
_default_model_cache = None
|
|
@@ -419,6 +422,16 @@ def clear_model_cache():
|
|
|
419
422
|
_default_vision_model_cache = None
|
|
420
423
|
|
|
421
424
|
|
|
425
|
+
def reset_session_model():
|
|
426
|
+
"""Reset the session-local model cache.
|
|
427
|
+
|
|
428
|
+
This is primarily for testing purposes. In normal operation, the session
|
|
429
|
+
model is set once at startup and only changes via set_model_name().
|
|
430
|
+
"""
|
|
431
|
+
global _SESSION_MODEL
|
|
432
|
+
_SESSION_MODEL = None
|
|
433
|
+
|
|
434
|
+
|
|
422
435
|
def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
423
436
|
"""Check if a model supports a particular setting (e.g., 'temperature', 'seed').
|
|
424
437
|
|
|
@@ -459,26 +472,49 @@ def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
|
459
472
|
def get_global_model_name():
|
|
460
473
|
"""Return a valid model name for Code Puppy to use.
|
|
461
474
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
475
|
+
Uses session-local caching so that model changes in other terminals
|
|
476
|
+
don't affect this running instance. The file is only read once at startup.
|
|
477
|
+
|
|
478
|
+
1. If _SESSION_MODEL is set, return it (session cache)
|
|
479
|
+
2. Otherwise, look at ``model`` in *puppy.cfg*
|
|
480
|
+
3. If that value exists **and** is present in *models.json*, use it
|
|
481
|
+
4. Otherwise return the first model listed in *models.json*
|
|
482
|
+
5. As a last resort fall back to ``claude-4-0-sonnet``
|
|
483
|
+
|
|
484
|
+
The result is cached in _SESSION_MODEL for subsequent calls.
|
|
467
485
|
"""
|
|
486
|
+
global _SESSION_MODEL
|
|
487
|
+
|
|
488
|
+
# Return cached session model if already initialized
|
|
489
|
+
if _SESSION_MODEL is not None:
|
|
490
|
+
return _SESSION_MODEL
|
|
468
491
|
|
|
492
|
+
# First access - initialize from file
|
|
469
493
|
stored_model = get_value("model")
|
|
470
494
|
|
|
471
495
|
if stored_model:
|
|
472
496
|
# Use cached validation to avoid hitting ModelFactory every time
|
|
473
497
|
if _validate_model_exists(stored_model):
|
|
474
|
-
|
|
498
|
+
_SESSION_MODEL = stored_model
|
|
499
|
+
return _SESSION_MODEL
|
|
475
500
|
|
|
476
501
|
# Either no stored model or it's not valid – choose default from models.json
|
|
477
|
-
|
|
502
|
+
_SESSION_MODEL = _default_model_from_models_json()
|
|
503
|
+
return _SESSION_MODEL
|
|
478
504
|
|
|
479
505
|
|
|
480
506
|
def set_model_name(model: str):
|
|
481
|
-
"""Sets the model name in the persistent config file.
|
|
507
|
+
"""Sets the model name in both the session cache and persistent config file.
|
|
508
|
+
|
|
509
|
+
Updates _SESSION_MODEL immediately for this process, and writes to the
|
|
510
|
+
config file so new terminals will pick up this model as their default.
|
|
511
|
+
"""
|
|
512
|
+
global _SESSION_MODEL
|
|
513
|
+
|
|
514
|
+
# Update session cache immediately
|
|
515
|
+
_SESSION_MODEL = model
|
|
516
|
+
|
|
517
|
+
# Also persist to file for new terminal sessions
|
|
482
518
|
config = configparser.ConfigParser()
|
|
483
519
|
config.read(CONFIG_FILE)
|
|
484
520
|
if DEFAULT_SECTION not in config:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Shared helpers for switching models and reloading agents safely."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from code_puppy.config import set_model_name
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_effective_agent_model(agent) -> Optional[str]:
|
|
11
|
+
"""Safely fetch the effective model name for an agent."""
|
|
12
|
+
try:
|
|
13
|
+
return agent.get_model_name()
|
|
14
|
+
except Exception:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_model_and_reload_agent(
|
|
19
|
+
model_name: str,
|
|
20
|
+
*,
|
|
21
|
+
warn_on_pinned_mismatch: bool = True,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Set the global model and reload the active agent.
|
|
24
|
+
|
|
25
|
+
This keeps model switching consistent across commands while avoiding
|
|
26
|
+
direct imports that can trigger circular dependencies.
|
|
27
|
+
"""
|
|
28
|
+
from code_puppy.messaging import emit_info, emit_warning
|
|
29
|
+
|
|
30
|
+
set_model_name(model_name)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from code_puppy.agents import get_current_agent
|
|
34
|
+
|
|
35
|
+
current_agent = get_current_agent()
|
|
36
|
+
if current_agent is None:
|
|
37
|
+
emit_warning("Model changed but no active agent was found to reload")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# JSON agents may need to refresh their config before reload
|
|
41
|
+
if hasattr(current_agent, "refresh_config"):
|
|
42
|
+
try:
|
|
43
|
+
current_agent.refresh_config()
|
|
44
|
+
except Exception:
|
|
45
|
+
# Non-fatal, continue to reload
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
if warn_on_pinned_mismatch:
|
|
49
|
+
effective_model = _get_effective_agent_model(current_agent)
|
|
50
|
+
if effective_model and effective_model != model_name:
|
|
51
|
+
display_name = getattr(
|
|
52
|
+
current_agent, "display_name", current_agent.name
|
|
53
|
+
)
|
|
54
|
+
emit_warning(
|
|
55
|
+
"Active agent "
|
|
56
|
+
f"'{display_name}' is pinned to '{effective_model}', "
|
|
57
|
+
f"so '{model_name}' will not take effect until unpinned."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
current_agent.reload_code_generation_agent()
|
|
61
|
+
emit_info("Active agent reloaded")
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
emit_warning(f"Model changed but agent reload failed: {exc}")
|