code-puppy 0.0.348__py3-none-any.whl → 0.0.372__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 +8 -0
- code_puppy/agents/agent_manager.py +272 -1
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +11 -8
- code_puppy/agents/event_stream_handler.py +101 -8
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/chatgpt_codex_client.py +53 -0
- code_puppy/claude_cache_client.py +294 -41
- code_puppy/command_line/add_model_menu.py +13 -4
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/core_commands.py +89 -112
- code_puppy/command_line/model_picker_completion.py +3 -20
- code_puppy/command_line/model_settings_menu.py +21 -3
- code_puppy/config.py +145 -70
- code_puppy/gemini_model.py +706 -0
- code_puppy/http_utils.py +6 -3
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +50 -16
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +27 -24
- code_puppy/models.json +12 -12
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/antigravity_oauth/transport.py +236 -45
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
- code_puppy/plugins/claude_code_oauth/utils.py +4 -1
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +52 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +83 -33
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
- code_puppy/prompts/codex_system_prompt.md +0 -310
- code_puppy/tools/browser/camoufox_manager.py +0 -235
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ discovered by the command registry system.
|
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
|
|
9
|
+
from code_puppy.command_line.agent_menu import interactive_agent_picker
|
|
9
10
|
from code_puppy.command_line.command_registry import register_command
|
|
10
11
|
from code_puppy.command_line.model_picker_completion import update_model_in_input
|
|
11
12
|
from code_puppy.command_line.motd import print_motd
|
|
@@ -168,7 +169,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
168
169
|
reset_onboarding,
|
|
169
170
|
run_onboarding_wizard,
|
|
170
171
|
)
|
|
171
|
-
from code_puppy.
|
|
172
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
172
173
|
|
|
173
174
|
# Always reset so user can re-run the tutorial anytime
|
|
174
175
|
reset_onboarding()
|
|
@@ -183,7 +184,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
183
184
|
from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
|
|
184
185
|
|
|
185
186
|
run_oauth_flow()
|
|
186
|
-
|
|
187
|
+
set_model_and_reload_agent("chatgpt-gpt-5.2-codex")
|
|
187
188
|
elif result == "claude":
|
|
188
189
|
emit_info("🔐 Starting Claude Code OAuth flow...")
|
|
189
190
|
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
@@ -191,7 +192,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
191
192
|
)
|
|
192
193
|
|
|
193
194
|
_perform_authentication()
|
|
194
|
-
|
|
195
|
+
set_model_and_reload_agent("claude-code-claude-opus-4-5-20251101")
|
|
195
196
|
elif result == "completed":
|
|
196
197
|
emit_info("🎉 Tutorial complete! Happy coding!")
|
|
197
198
|
elif result == "skipped":
|
|
@@ -398,115 +399,6 @@ def handle_agent_command(command: str) -> bool:
|
|
|
398
399
|
return True
|
|
399
400
|
|
|
400
401
|
|
|
401
|
-
async def interactive_agent_picker() -> str | None:
|
|
402
|
-
"""Show an interactive arrow-key selector to pick an agent (async version).
|
|
403
|
-
|
|
404
|
-
Returns:
|
|
405
|
-
The selected agent name, or None if cancelled
|
|
406
|
-
"""
|
|
407
|
-
import sys
|
|
408
|
-
import time
|
|
409
|
-
|
|
410
|
-
from rich.console import Console
|
|
411
|
-
from rich.panel import Panel
|
|
412
|
-
from rich.text import Text
|
|
413
|
-
|
|
414
|
-
from code_puppy.agents import (
|
|
415
|
-
get_agent_descriptions,
|
|
416
|
-
get_available_agents,
|
|
417
|
-
get_current_agent,
|
|
418
|
-
)
|
|
419
|
-
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
420
|
-
from code_puppy.tools.common import arrow_select_async
|
|
421
|
-
|
|
422
|
-
# Load available agents
|
|
423
|
-
available_agents = get_available_agents()
|
|
424
|
-
descriptions = get_agent_descriptions()
|
|
425
|
-
current_agent = get_current_agent()
|
|
426
|
-
|
|
427
|
-
# Build choices with current agent indicator and keep track of agent names
|
|
428
|
-
choices = []
|
|
429
|
-
agent_names = list(available_agents.keys())
|
|
430
|
-
for agent_name in agent_names:
|
|
431
|
-
display_name = available_agents[agent_name]
|
|
432
|
-
if agent_name == current_agent.name:
|
|
433
|
-
choices.append(f"✓ {agent_name} - {display_name} (current)")
|
|
434
|
-
else:
|
|
435
|
-
choices.append(f" {agent_name} - {display_name}")
|
|
436
|
-
|
|
437
|
-
# Create preview callback to show agent description
|
|
438
|
-
def get_preview(index: int) -> str:
|
|
439
|
-
"""Get the description for the agent at the given index."""
|
|
440
|
-
agent_name = agent_names[index]
|
|
441
|
-
description = descriptions.get(agent_name, "No description available")
|
|
442
|
-
return description
|
|
443
|
-
|
|
444
|
-
# Create panel content
|
|
445
|
-
panel_content = Text()
|
|
446
|
-
panel_content.append("🐶 Select an agent to use\n", style="bold cyan")
|
|
447
|
-
panel_content.append("Current agent: ", style="dim")
|
|
448
|
-
panel_content.append(f"{current_agent.name}", style="bold green")
|
|
449
|
-
panel_content.append(" - ", style="dim")
|
|
450
|
-
panel_content.append(current_agent.display_name, style="bold green")
|
|
451
|
-
panel_content.append("\n", style="dim")
|
|
452
|
-
panel_content.append(current_agent.description, style="dim italic")
|
|
453
|
-
|
|
454
|
-
# Display panel
|
|
455
|
-
panel = Panel(
|
|
456
|
-
panel_content,
|
|
457
|
-
title="[bold white]Agent Selection[/bold white]",
|
|
458
|
-
border_style="cyan",
|
|
459
|
-
padding=(1, 2),
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
# Pause spinners BEFORE showing panel
|
|
463
|
-
set_awaiting_user_input(True)
|
|
464
|
-
time.sleep(0.3) # Let spinners fully stop
|
|
465
|
-
|
|
466
|
-
local_console = Console()
|
|
467
|
-
emit_info("")
|
|
468
|
-
local_console.print(panel)
|
|
469
|
-
emit_info("")
|
|
470
|
-
|
|
471
|
-
# Flush output before prompt_toolkit takes control
|
|
472
|
-
sys.stdout.flush()
|
|
473
|
-
sys.stderr.flush()
|
|
474
|
-
time.sleep(0.1)
|
|
475
|
-
|
|
476
|
-
selected_agent = None
|
|
477
|
-
|
|
478
|
-
try:
|
|
479
|
-
# Final flush
|
|
480
|
-
sys.stdout.flush()
|
|
481
|
-
|
|
482
|
-
# Show arrow-key selector with preview (async version)
|
|
483
|
-
choice = await arrow_select_async(
|
|
484
|
-
"💭 Which agent would you like to use?",
|
|
485
|
-
choices,
|
|
486
|
-
preview_callback=get_preview,
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
# Extract agent name from choice (remove prefix and suffix)
|
|
490
|
-
if choice:
|
|
491
|
-
# Remove the "✓ " or " " prefix and extract agent name (before " - ")
|
|
492
|
-
choice_stripped = choice.strip().lstrip("✓").strip()
|
|
493
|
-
# Split on " - " and take the first part (agent name)
|
|
494
|
-
agent_name = choice_stripped.split(" - ")[0].strip()
|
|
495
|
-
# Remove " (current)" suffix if present
|
|
496
|
-
if agent_name.endswith(" (current)"):
|
|
497
|
-
agent_name = agent_name[:-10].strip()
|
|
498
|
-
selected_agent = agent_name
|
|
499
|
-
|
|
500
|
-
except (KeyboardInterrupt, EOFError):
|
|
501
|
-
emit_error("Cancelled by user")
|
|
502
|
-
selected_agent = None
|
|
503
|
-
|
|
504
|
-
finally:
|
|
505
|
-
set_awaiting_user_input(False)
|
|
506
|
-
|
|
507
|
-
return selected_agent
|
|
508
|
-
|
|
509
|
-
|
|
510
402
|
async def interactive_model_picker() -> str | None:
|
|
511
403
|
"""Show an interactive arrow-key selector to pick a model (async version).
|
|
512
404
|
|
|
@@ -772,6 +664,91 @@ def handle_mcp_command(command: str) -> bool:
|
|
|
772
664
|
return handler.handle_mcp_command(command)
|
|
773
665
|
|
|
774
666
|
|
|
667
|
+
@register_command(
|
|
668
|
+
name="api",
|
|
669
|
+
description="Manage the Code Puppy API server",
|
|
670
|
+
usage="/api [start|stop|status]",
|
|
671
|
+
category="core",
|
|
672
|
+
detailed_help="Start, stop, or check status of the local FastAPI server for GUI integration.",
|
|
673
|
+
)
|
|
674
|
+
def handle_api_command(command: str) -> bool:
|
|
675
|
+
"""Handle the /api command."""
|
|
676
|
+
import os
|
|
677
|
+
import signal
|
|
678
|
+
import subprocess
|
|
679
|
+
import sys
|
|
680
|
+
from pathlib import Path
|
|
681
|
+
|
|
682
|
+
from code_puppy.config import STATE_DIR
|
|
683
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success
|
|
684
|
+
|
|
685
|
+
parts = command.split()
|
|
686
|
+
subcommand = parts[1] if len(parts) > 1 else "status"
|
|
687
|
+
|
|
688
|
+
pid_file = Path(STATE_DIR) / "api_server.pid"
|
|
689
|
+
|
|
690
|
+
if subcommand == "start":
|
|
691
|
+
# Check if already running
|
|
692
|
+
if pid_file.exists():
|
|
693
|
+
try:
|
|
694
|
+
pid = int(pid_file.read_text().strip())
|
|
695
|
+
os.kill(pid, 0) # Check if process exists
|
|
696
|
+
emit_info(f"API server already running (PID {pid})")
|
|
697
|
+
return True
|
|
698
|
+
except (OSError, ValueError):
|
|
699
|
+
pid_file.unlink(missing_ok=True) # Stale PID file
|
|
700
|
+
|
|
701
|
+
# Start the server in background
|
|
702
|
+
emit_info("Starting API server on http://127.0.0.1:8765 ...")
|
|
703
|
+
proc = subprocess.Popen(
|
|
704
|
+
[sys.executable, "-m", "code_puppy.api.main"],
|
|
705
|
+
stdout=subprocess.DEVNULL,
|
|
706
|
+
stderr=subprocess.DEVNULL,
|
|
707
|
+
start_new_session=True,
|
|
708
|
+
)
|
|
709
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
710
|
+
pid_file.write_text(str(proc.pid))
|
|
711
|
+
emit_success(f"API server started (PID {proc.pid})")
|
|
712
|
+
emit_info("Docs available at http://127.0.0.1:8765/docs")
|
|
713
|
+
return True
|
|
714
|
+
|
|
715
|
+
elif subcommand == "stop":
|
|
716
|
+
if not pid_file.exists():
|
|
717
|
+
emit_info("API server is not running")
|
|
718
|
+
return True
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
pid = int(pid_file.read_text().strip())
|
|
722
|
+
os.kill(pid, signal.SIGTERM)
|
|
723
|
+
pid_file.unlink()
|
|
724
|
+
emit_success(f"API server stopped (PID {pid})")
|
|
725
|
+
except (OSError, ValueError) as e:
|
|
726
|
+
pid_file.unlink(missing_ok=True)
|
|
727
|
+
emit_error(f"Error stopping server: {e}")
|
|
728
|
+
return True
|
|
729
|
+
|
|
730
|
+
elif subcommand == "status":
|
|
731
|
+
if not pid_file.exists():
|
|
732
|
+
emit_info("API server is not running")
|
|
733
|
+
return True
|
|
734
|
+
|
|
735
|
+
try:
|
|
736
|
+
pid = int(pid_file.read_text().strip())
|
|
737
|
+
os.kill(pid, 0) # Check if process exists
|
|
738
|
+
emit_success(f"API server is running (PID {pid})")
|
|
739
|
+
emit_info("URL: http://127.0.0.1:8765")
|
|
740
|
+
emit_info("Docs: http://127.0.0.1:8765/docs")
|
|
741
|
+
except (OSError, ValueError):
|
|
742
|
+
pid_file.unlink(missing_ok=True)
|
|
743
|
+
emit_info("API server is not running (stale PID file removed)")
|
|
744
|
+
return True
|
|
745
|
+
|
|
746
|
+
else:
|
|
747
|
+
emit_error(f"Unknown subcommand: {subcommand}")
|
|
748
|
+
emit_info("Usage: /api [start|stop|status]")
|
|
749
|
+
return True
|
|
750
|
+
|
|
751
|
+
|
|
775
752
|
@register_command(
|
|
776
753
|
name="generate-pr-description",
|
|
777
754
|
description="Generate comprehensive PR description",
|
|
@@ -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):
|
|
@@ -39,10 +39,10 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
|
|
|
39
39
|
"description": "Controls randomness. Lower = more deterministic, higher = more creative.",
|
|
40
40
|
"type": "numeric",
|
|
41
41
|
"min": 0.0,
|
|
42
|
-
"max": 1.0,
|
|
43
|
-
"step": 0.
|
|
42
|
+
"max": 1.0,
|
|
43
|
+
"step": 0.05,
|
|
44
44
|
"default": None, # None means use model default
|
|
45
|
-
"format": "{:.
|
|
45
|
+
"format": "{:.2f}",
|
|
46
46
|
},
|
|
47
47
|
"seed": {
|
|
48
48
|
"name": "Seed",
|
|
@@ -54,6 +54,16 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
|
|
|
54
54
|
"default": None,
|
|
55
55
|
"format": "{:.0f}",
|
|
56
56
|
},
|
|
57
|
+
"top_p": {
|
|
58
|
+
"name": "Top-P (Nucleus Sampling)",
|
|
59
|
+
"description": "Controls token diversity. 0.0 = least random (only most likely tokens), 1.0 = most random (sample from all tokens).",
|
|
60
|
+
"type": "numeric",
|
|
61
|
+
"min": 0.0,
|
|
62
|
+
"max": 1.0,
|
|
63
|
+
"step": 0.05,
|
|
64
|
+
"default": None,
|
|
65
|
+
"format": "{:.2f}",
|
|
66
|
+
},
|
|
57
67
|
"reasoning_effort": {
|
|
58
68
|
"name": "Reasoning Effort",
|
|
59
69
|
"description": "Controls how much effort GPT-5 models spend on reasoning. Higher = more thorough but slower.",
|
|
@@ -90,6 +100,12 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
|
|
|
90
100
|
"type": "boolean",
|
|
91
101
|
"default": False,
|
|
92
102
|
},
|
|
103
|
+
"clear_thinking": {
|
|
104
|
+
"name": "Clear Thinking",
|
|
105
|
+
"description": "False = Preserved Thinking (keep <think> blocks visible). True = strip thinking from responses.",
|
|
106
|
+
"type": "boolean",
|
|
107
|
+
"default": False,
|
|
108
|
+
},
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
|
|
@@ -569,6 +585,8 @@ class ModelSettingsMenu:
|
|
|
569
585
|
# Default to a sensible starting point for numeric
|
|
570
586
|
if setting_key == "temperature":
|
|
571
587
|
self.edit_value = 0.7
|
|
588
|
+
elif setting_key == "top_p":
|
|
589
|
+
self.edit_value = 0.9 # Common default for top_p
|
|
572
590
|
elif setting_key == "seed":
|
|
573
591
|
self.edit_value = 42
|
|
574
592
|
elif setting_key == "budget_tokens":
|
code_puppy/config.py
CHANGED
|
@@ -75,17 +75,61 @@ def get_use_dbos() -> bool:
|
|
|
75
75
|
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def get_subagent_verbose() -> bool:
|
|
79
|
+
"""Return True if sub-agent verbose output is enabled (default False).
|
|
80
|
+
|
|
81
|
+
When False (default), sub-agents produce quiet, sparse output suitable
|
|
82
|
+
for parallel execution. When True, sub-agents produce full verbose output
|
|
83
|
+
like the main agent (useful for debugging).
|
|
84
|
+
"""
|
|
85
|
+
cfg_val = get_value("subagent_verbose")
|
|
86
|
+
if cfg_val is None:
|
|
87
|
+
return False
|
|
88
|
+
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Pack agents - the specialized sub-agents coordinated by Pack Leader
|
|
92
|
+
PACK_AGENT_NAMES = frozenset(
|
|
93
|
+
[
|
|
94
|
+
"pack-leader",
|
|
95
|
+
"bloodhound",
|
|
96
|
+
"husky",
|
|
97
|
+
"shepherd",
|
|
98
|
+
"terrier",
|
|
99
|
+
"watchdog",
|
|
100
|
+
"retriever",
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_pack_agents_enabled() -> bool:
|
|
106
|
+
"""Return True if pack agents are enabled (default False).
|
|
107
|
+
|
|
108
|
+
When False (default), pack agents (pack-leader, bloodhound, husky, shepherd,
|
|
109
|
+
terrier, watchdog, retriever) are hidden from `list_agents` tool and `/agents`
|
|
110
|
+
command. They cannot be invoked by other agents or selected by users.
|
|
111
|
+
|
|
112
|
+
When True, pack agents are available for use.
|
|
113
|
+
"""
|
|
114
|
+
cfg_val = get_value("enable_pack_agents")
|
|
115
|
+
if cfg_val is None:
|
|
116
|
+
return False
|
|
117
|
+
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
118
|
+
|
|
119
|
+
|
|
78
120
|
DEFAULT_SECTION = "puppy"
|
|
79
121
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
80
122
|
|
|
81
123
|
# Runtime-only autosave session ID (per-process)
|
|
82
124
|
_CURRENT_AUTOSAVE_ID: Optional[str] = None
|
|
83
125
|
|
|
126
|
+
# Session-local model name (initialized from file on first access, then cached)
|
|
127
|
+
_SESSION_MODEL: Optional[str] = None
|
|
128
|
+
|
|
84
129
|
# Cache containers for model validation and defaults
|
|
85
130
|
_model_validation_cache = {}
|
|
86
131
|
_default_model_cache = None
|
|
87
132
|
_default_vision_model_cache = None
|
|
88
|
-
_default_vqa_model_cache = None
|
|
89
133
|
|
|
90
134
|
|
|
91
135
|
def ensure_config_exists():
|
|
@@ -208,9 +252,14 @@ def get_config_keys():
|
|
|
208
252
|
"diff_context_lines",
|
|
209
253
|
"default_agent",
|
|
210
254
|
"temperature",
|
|
255
|
+
"frontend_emitter_enabled",
|
|
256
|
+
"frontend_emitter_max_recent_events",
|
|
257
|
+
"frontend_emitter_queue_size",
|
|
211
258
|
]
|
|
212
259
|
# Add DBOS control key
|
|
213
260
|
default_keys.append("enable_dbos")
|
|
261
|
+
# Add pack agents control key
|
|
262
|
+
default_keys.append("enable_pack_agents")
|
|
214
263
|
# Add cancel agent key configuration
|
|
215
264
|
default_keys.append("cancel_agent_key")
|
|
216
265
|
# Add banner color keys
|
|
@@ -237,6 +286,22 @@ def set_config_value(key: str, value: str):
|
|
|
237
286
|
config.write(f)
|
|
238
287
|
|
|
239
288
|
|
|
289
|
+
# Alias for API compatibility
|
|
290
|
+
def set_value(key: str, value: str) -> None:
|
|
291
|
+
"""Set a config value. Alias for set_config_value."""
|
|
292
|
+
set_config_value(key, value)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def reset_value(key: str) -> None:
|
|
296
|
+
"""Remove a key from the config file, resetting it to default."""
|
|
297
|
+
config = configparser.ConfigParser()
|
|
298
|
+
config.read(CONFIG_FILE)
|
|
299
|
+
if DEFAULT_SECTION in config and key in config[DEFAULT_SECTION]:
|
|
300
|
+
del config[DEFAULT_SECTION][key]
|
|
301
|
+
with open(CONFIG_FILE, "w") as f:
|
|
302
|
+
config.write(f)
|
|
303
|
+
|
|
304
|
+
|
|
240
305
|
# --- MODEL STICKY EXTENSION STARTS HERE ---
|
|
241
306
|
def load_mcp_server_configs():
|
|
242
307
|
"""
|
|
@@ -326,47 +391,6 @@ def _default_vision_model_from_models_json() -> str:
|
|
|
326
391
|
return "gpt-4.1"
|
|
327
392
|
|
|
328
393
|
|
|
329
|
-
def _default_vqa_model_from_models_json() -> str:
|
|
330
|
-
"""Select a default VQA-capable model, preferring vision-ready options."""
|
|
331
|
-
global _default_vqa_model_cache
|
|
332
|
-
|
|
333
|
-
if _default_vqa_model_cache is not None:
|
|
334
|
-
return _default_vqa_model_cache
|
|
335
|
-
|
|
336
|
-
try:
|
|
337
|
-
from code_puppy.model_factory import ModelFactory
|
|
338
|
-
|
|
339
|
-
models_config = ModelFactory.load_config()
|
|
340
|
-
if models_config:
|
|
341
|
-
# Allow explicit VQA hints if present
|
|
342
|
-
for name, config in models_config.items():
|
|
343
|
-
if config.get("supports_vqa"):
|
|
344
|
-
_default_vqa_model_cache = name
|
|
345
|
-
return name
|
|
346
|
-
|
|
347
|
-
# Reuse multimodal heuristics before falling back to generic default
|
|
348
|
-
preferred_candidates = (
|
|
349
|
-
"gpt-4.1",
|
|
350
|
-
"gpt-4.1-mini",
|
|
351
|
-
"claude-4-0-sonnet",
|
|
352
|
-
"gemini-2.5-flash-preview-05-20",
|
|
353
|
-
"gpt-4.1-nano",
|
|
354
|
-
)
|
|
355
|
-
for candidate in preferred_candidates:
|
|
356
|
-
if candidate in models_config:
|
|
357
|
-
_default_vqa_model_cache = candidate
|
|
358
|
-
return candidate
|
|
359
|
-
|
|
360
|
-
_default_vqa_model_cache = _default_model_from_models_json()
|
|
361
|
-
return _default_vqa_model_cache
|
|
362
|
-
|
|
363
|
-
_default_vqa_model_cache = "gpt-4.1"
|
|
364
|
-
return "gpt-4.1"
|
|
365
|
-
except Exception:
|
|
366
|
-
_default_vqa_model_cache = "gpt-4.1"
|
|
367
|
-
return "gpt-4.1"
|
|
368
|
-
|
|
369
|
-
|
|
370
394
|
def _validate_model_exists(model_name: str) -> bool:
|
|
371
395
|
"""Check if a model exists in models.json with caching to avoid redundant calls."""
|
|
372
396
|
global _model_validation_cache
|
|
@@ -392,15 +416,20 @@ def _validate_model_exists(model_name: str) -> bool:
|
|
|
392
416
|
|
|
393
417
|
def clear_model_cache():
|
|
394
418
|
"""Clear the model validation cache. Call this when models.json changes."""
|
|
395
|
-
global
|
|
396
|
-
_model_validation_cache, \
|
|
397
|
-
_default_model_cache, \
|
|
398
|
-
_default_vision_model_cache, \
|
|
399
|
-
_default_vqa_model_cache
|
|
419
|
+
global _model_validation_cache, _default_model_cache, _default_vision_model_cache
|
|
400
420
|
_model_validation_cache.clear()
|
|
401
421
|
_default_model_cache = None
|
|
402
422
|
_default_vision_model_cache = None
|
|
403
|
-
|
|
423
|
+
|
|
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
|
|
404
433
|
|
|
405
434
|
|
|
406
435
|
def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
@@ -414,6 +443,10 @@ def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
|
414
443
|
True if the model supports the setting, False otherwise.
|
|
415
444
|
Defaults to True for backwards compatibility if model config doesn't specify.
|
|
416
445
|
"""
|
|
446
|
+
# GLM-4.7 models always support clear_thinking setting
|
|
447
|
+
if setting == "clear_thinking" and "glm-4.7" in model_name.lower():
|
|
448
|
+
return True
|
|
449
|
+
|
|
417
450
|
try:
|
|
418
451
|
from code_puppy.model_factory import ModelFactory
|
|
419
452
|
|
|
@@ -439,26 +472,49 @@ def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
|
439
472
|
def get_global_model_name():
|
|
440
473
|
"""Return a valid model name for Code Puppy to use.
|
|
441
474
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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.
|
|
447
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
|
|
448
491
|
|
|
492
|
+
# First access - initialize from file
|
|
449
493
|
stored_model = get_value("model")
|
|
450
494
|
|
|
451
495
|
if stored_model:
|
|
452
496
|
# Use cached validation to avoid hitting ModelFactory every time
|
|
453
497
|
if _validate_model_exists(stored_model):
|
|
454
|
-
|
|
498
|
+
_SESSION_MODEL = stored_model
|
|
499
|
+
return _SESSION_MODEL
|
|
455
500
|
|
|
456
501
|
# Either no stored model or it's not valid – choose default from models.json
|
|
457
|
-
|
|
502
|
+
_SESSION_MODEL = _default_model_from_models_json()
|
|
503
|
+
return _SESSION_MODEL
|
|
458
504
|
|
|
459
505
|
|
|
460
506
|
def set_model_name(model: str):
|
|
461
|
-
"""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
|
|
462
518
|
config = configparser.ConfigParser()
|
|
463
519
|
config.read(CONFIG_FILE)
|
|
464
520
|
if DEFAULT_SECTION not in config:
|
|
@@ -471,20 +527,6 @@ def set_model_name(model: str):
|
|
|
471
527
|
clear_model_cache()
|
|
472
528
|
|
|
473
529
|
|
|
474
|
-
def get_vqa_model_name() -> str:
|
|
475
|
-
"""Return the configured VQA model, falling back to an inferred default."""
|
|
476
|
-
stored_model = get_value("vqa_model_name")
|
|
477
|
-
if stored_model and _validate_model_exists(stored_model):
|
|
478
|
-
return stored_model
|
|
479
|
-
return _default_vqa_model_from_models_json()
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
def set_vqa_model_name(model: str):
|
|
483
|
-
"""Persist the configured VQA model name and refresh caches."""
|
|
484
|
-
set_config_value("vqa_model_name", model or "")
|
|
485
|
-
clear_model_cache()
|
|
486
|
-
|
|
487
|
-
|
|
488
530
|
def get_puppy_token():
|
|
489
531
|
"""Returns the puppy_token from config, or None if not set."""
|
|
490
532
|
return get_value("puppy_token")
|
|
@@ -1291,6 +1333,8 @@ DEFAULT_BANNER_COLORS = {
|
|
|
1291
1333
|
"invoke_agent": "deep_pink4", # Ruby - agent invocation
|
|
1292
1334
|
"subagent_response": "sea_green3", # Emerald - sub-agent success
|
|
1293
1335
|
"list_agents": "dark_slate_gray3", # Slate - neutral listing
|
|
1336
|
+
# Browser/Terminal tools - same color as edit_file (gold)
|
|
1337
|
+
"terminal_tool": "dark_goldenrod", # Gold - browser terminal operations
|
|
1294
1338
|
}
|
|
1295
1339
|
|
|
1296
1340
|
|
|
@@ -1584,3 +1628,34 @@ def set_default_agent(agent_name: str) -> None:
|
|
|
1584
1628
|
agent_name: The name of the agent to set as default.
|
|
1585
1629
|
"""
|
|
1586
1630
|
set_config_value("default_agent", agent_name)
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
# --- FRONTEND EMITTER CONFIGURATION ---
|
|
1634
|
+
def get_frontend_emitter_enabled() -> bool:
|
|
1635
|
+
"""Check if frontend emitter is enabled."""
|
|
1636
|
+
val = get_value("frontend_emitter_enabled")
|
|
1637
|
+
if val is None:
|
|
1638
|
+
return True # Enabled by default
|
|
1639
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def get_frontend_emitter_max_recent_events() -> int:
|
|
1643
|
+
"""Get max number of recent events to buffer."""
|
|
1644
|
+
val = get_value("frontend_emitter_max_recent_events")
|
|
1645
|
+
if val is None:
|
|
1646
|
+
return 100
|
|
1647
|
+
try:
|
|
1648
|
+
return int(val)
|
|
1649
|
+
except ValueError:
|
|
1650
|
+
return 100
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
def get_frontend_emitter_queue_size() -> int:
|
|
1654
|
+
"""Get max subscriber queue size."""
|
|
1655
|
+
val = get_value("frontend_emitter_queue_size")
|
|
1656
|
+
if val is None:
|
|
1657
|
+
return 100
|
|
1658
|
+
try:
|
|
1659
|
+
return int(val)
|
|
1660
|
+
except ValueError:
|
|
1661
|
+
return 100
|