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
code_puppy/agents/__init__.py
CHANGED
|
@@ -5,19 +5,27 @@ configurations, each with their own system prompts and tool sets.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .agent_manager import (
|
|
8
|
+
clone_agent,
|
|
9
|
+
delete_clone_agent,
|
|
8
10
|
get_agent_descriptions,
|
|
9
11
|
get_available_agents,
|
|
10
12
|
get_current_agent,
|
|
13
|
+
is_clone_agent_name,
|
|
11
14
|
load_agent,
|
|
12
15
|
refresh_agents,
|
|
13
16
|
set_current_agent,
|
|
14
17
|
)
|
|
18
|
+
from .subagent_stream_handler import subagent_stream_handler
|
|
15
19
|
|
|
16
20
|
__all__ = [
|
|
21
|
+
"clone_agent",
|
|
22
|
+
"delete_clone_agent",
|
|
17
23
|
"get_available_agents",
|
|
18
24
|
"get_current_agent",
|
|
25
|
+
"is_clone_agent_name",
|
|
19
26
|
"set_current_agent",
|
|
20
27
|
"load_agent",
|
|
21
28
|
"get_agent_descriptions",
|
|
22
29
|
"refresh_agents",
|
|
30
|
+
"subagent_stream_handler",
|
|
23
31
|
]
|
|
@@ -4,6 +4,7 @@ import importlib
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import pkgutil
|
|
7
|
+
import re
|
|
7
8
|
import uuid
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Dict, List, Optional, Type, Union
|
|
@@ -13,7 +14,7 @@ from pydantic_ai.messages import ModelMessage
|
|
|
13
14
|
from code_puppy.agents.base_agent import BaseAgent
|
|
14
15
|
from code_puppy.agents.json_agent import JSONAgent, discover_json_agents
|
|
15
16
|
from code_puppy.callbacks import on_agent_reload
|
|
16
|
-
from code_puppy.messaging import emit_warning
|
|
17
|
+
from code_puppy.messaging import emit_success, emit_warning
|
|
17
18
|
|
|
18
19
|
# Registry of available agents (Python classes and JSON file paths)
|
|
19
20
|
_AGENT_REGISTRY: Dict[str, Union[Type[BaseAgent], str]] = {}
|
|
@@ -225,6 +226,55 @@ def _discover_agents(message_group_id: Optional[str] = None):
|
|
|
225
226
|
)
|
|
226
227
|
continue
|
|
227
228
|
|
|
229
|
+
# 1b. Discover agents in sub-packages (like 'pack')
|
|
230
|
+
for _, subpkg_name, ispkg in pkgutil.iter_modules(agents_package.__path__):
|
|
231
|
+
if not ispkg or subpkg_name.startswith("_"):
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Import the sub-package
|
|
236
|
+
subpkg = importlib.import_module(f"code_puppy.agents.{subpkg_name}")
|
|
237
|
+
|
|
238
|
+
# Iterate through modules in the sub-package
|
|
239
|
+
if not hasattr(subpkg, "__path__"):
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
for _, modname, _ in pkgutil.iter_modules(subpkg.__path__):
|
|
243
|
+
if modname.startswith("_"):
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
# Import the submodule
|
|
248
|
+
module = importlib.import_module(
|
|
249
|
+
f"code_puppy.agents.{subpkg_name}.{modname}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Look for BaseAgent subclasses
|
|
253
|
+
for attr_name in dir(module):
|
|
254
|
+
attr = getattr(module, attr_name)
|
|
255
|
+
if (
|
|
256
|
+
isinstance(attr, type)
|
|
257
|
+
and issubclass(attr, BaseAgent)
|
|
258
|
+
and attr not in [BaseAgent, JSONAgent]
|
|
259
|
+
):
|
|
260
|
+
# Create an instance to get the name
|
|
261
|
+
agent_instance = attr()
|
|
262
|
+
_AGENT_REGISTRY[agent_instance.name] = attr
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
emit_warning(
|
|
266
|
+
f"Warning: Could not load agent {subpkg_name}.{modname}: {e}",
|
|
267
|
+
message_group=message_group_id,
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
emit_warning(
|
|
273
|
+
f"Warning: Could not load agent sub-package {subpkg_name}: {e}",
|
|
274
|
+
message_group=message_group_id,
|
|
275
|
+
)
|
|
276
|
+
continue
|
|
277
|
+
|
|
228
278
|
# 2. Discover JSON agents in user directory
|
|
229
279
|
try:
|
|
230
280
|
json_agents = discover_json_agents()
|
|
@@ -246,12 +296,21 @@ def get_available_agents() -> Dict[str, str]:
|
|
|
246
296
|
Returns:
|
|
247
297
|
Dict mapping agent names to display names.
|
|
248
298
|
"""
|
|
299
|
+
from ..config import PACK_AGENT_NAMES, get_pack_agents_enabled
|
|
300
|
+
|
|
249
301
|
# Generate a message group ID for this operation
|
|
250
302
|
message_group_id = str(uuid.uuid4())
|
|
251
303
|
_discover_agents(message_group_id=message_group_id)
|
|
252
304
|
|
|
305
|
+
# Check if pack agents are enabled
|
|
306
|
+
pack_agents_enabled = get_pack_agents_enabled()
|
|
307
|
+
|
|
253
308
|
agents = {}
|
|
254
309
|
for name, agent_ref in _AGENT_REGISTRY.items():
|
|
310
|
+
# Filter out pack agents if disabled
|
|
311
|
+
if not pack_agents_enabled and name in PACK_AGENT_NAMES:
|
|
312
|
+
continue
|
|
313
|
+
|
|
255
314
|
try:
|
|
256
315
|
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
257
316
|
agent_instance = JSONAgent(agent_ref)
|
|
@@ -374,12 +433,21 @@ def get_agent_descriptions() -> Dict[str, str]:
|
|
|
374
433
|
Returns:
|
|
375
434
|
Dict mapping agent names to their descriptions.
|
|
376
435
|
"""
|
|
436
|
+
from ..config import PACK_AGENT_NAMES, get_pack_agents_enabled
|
|
437
|
+
|
|
377
438
|
# Generate a message group ID for this operation
|
|
378
439
|
message_group_id = str(uuid.uuid4())
|
|
379
440
|
_discover_agents(message_group_id=message_group_id)
|
|
380
441
|
|
|
442
|
+
# Check if pack agents are enabled
|
|
443
|
+
pack_agents_enabled = get_pack_agents_enabled()
|
|
444
|
+
|
|
381
445
|
descriptions = {}
|
|
382
446
|
for name, agent_ref in _AGENT_REGISTRY.items():
|
|
447
|
+
# Filter out pack agents if disabled
|
|
448
|
+
if not pack_agents_enabled and name in PACK_AGENT_NAMES:
|
|
449
|
+
continue
|
|
450
|
+
|
|
383
451
|
try:
|
|
384
452
|
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
385
453
|
agent_instance = JSONAgent(agent_ref)
|
|
@@ -400,3 +468,206 @@ def refresh_agents():
|
|
|
400
468
|
# Generate a message group ID for agent refreshing
|
|
401
469
|
message_group_id = str(uuid.uuid4())
|
|
402
470
|
_discover_agents(message_group_id=message_group_id)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
_CLONE_NAME_PATTERN = re.compile(r"^(?P<base>.+)-clone-(?P<index>\d+)$")
|
|
474
|
+
_CLONE_DISPLAY_PATTERN = re.compile(r"\s*\(Clone\s+\d+\)$", re.IGNORECASE)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _strip_clone_suffix(agent_name: str) -> str:
|
|
478
|
+
"""Strip a trailing -clone-N suffix from a name if present."""
|
|
479
|
+
match = _CLONE_NAME_PATTERN.match(agent_name)
|
|
480
|
+
return match.group("base") if match else agent_name
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _strip_clone_display_suffix(display_name: str) -> str:
|
|
484
|
+
"""Remove a trailing "(Clone N)" suffix from display names."""
|
|
485
|
+
cleaned = _CLONE_DISPLAY_PATTERN.sub("", display_name).strip()
|
|
486
|
+
return cleaned or display_name
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def is_clone_agent_name(agent_name: str) -> bool:
|
|
490
|
+
"""Return True if the agent name looks like a clone."""
|
|
491
|
+
return bool(_CLONE_NAME_PATTERN.match(agent_name))
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _default_display_name(agent_name: str) -> str:
|
|
495
|
+
"""Build a default display name from an agent name."""
|
|
496
|
+
title = agent_name.title()
|
|
497
|
+
return f"{title} 🤖"
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _build_clone_display_name(display_name: str, clone_index: int) -> str:
|
|
501
|
+
"""Build a clone display name based on the source display name."""
|
|
502
|
+
base_name = _strip_clone_display_suffix(display_name)
|
|
503
|
+
return f"{base_name} (Clone {clone_index})"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _filter_available_tools(tool_names: List[str]) -> List[str]:
|
|
507
|
+
"""Filter a tool list to only available tool names."""
|
|
508
|
+
from code_puppy.tools import get_available_tool_names
|
|
509
|
+
|
|
510
|
+
available_tools = set(get_available_tool_names())
|
|
511
|
+
return [tool for tool in tool_names if tool in available_tools]
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _next_clone_index(
|
|
515
|
+
base_name: str, existing_names: set[str], agents_dir: Path
|
|
516
|
+
) -> int:
|
|
517
|
+
"""Compute the next clone index for a base name."""
|
|
518
|
+
clone_pattern = re.compile(rf"^{re.escape(base_name)}-clone-(\\d+)$")
|
|
519
|
+
indices = []
|
|
520
|
+
for name in existing_names:
|
|
521
|
+
match = clone_pattern.match(name)
|
|
522
|
+
if match:
|
|
523
|
+
indices.append(int(match.group(1)))
|
|
524
|
+
|
|
525
|
+
next_index = max(indices, default=0) + 1
|
|
526
|
+
while True:
|
|
527
|
+
clone_name = f"{base_name}-clone-{next_index}"
|
|
528
|
+
clone_path = agents_dir / f"{clone_name}.json"
|
|
529
|
+
if clone_name not in existing_names and not clone_path.exists():
|
|
530
|
+
return next_index
|
|
531
|
+
next_index += 1
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def clone_agent(agent_name: str) -> Optional[str]:
|
|
535
|
+
"""Clone an agent definition into the user agents directory.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
agent_name: Source agent name to clone.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
The cloned agent name, or None if cloning failed.
|
|
542
|
+
"""
|
|
543
|
+
# Generate a message group ID for agent cloning
|
|
544
|
+
message_group_id = str(uuid.uuid4())
|
|
545
|
+
_discover_agents(message_group_id=message_group_id)
|
|
546
|
+
|
|
547
|
+
agent_ref = _AGENT_REGISTRY.get(agent_name)
|
|
548
|
+
if agent_ref is None:
|
|
549
|
+
emit_warning(f"Agent '{agent_name}' not found for cloning.")
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
from ..config import get_agent_pinned_model, get_user_agents_directory
|
|
553
|
+
|
|
554
|
+
agents_dir = Path(get_user_agents_directory())
|
|
555
|
+
base_name = _strip_clone_suffix(agent_name)
|
|
556
|
+
existing_names = set(_AGENT_REGISTRY.keys())
|
|
557
|
+
clone_index = _next_clone_index(base_name, existing_names, agents_dir)
|
|
558
|
+
clone_name = f"{base_name}-clone-{clone_index}"
|
|
559
|
+
clone_path = agents_dir / f"{clone_name}.json"
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
if isinstance(agent_ref, str):
|
|
563
|
+
with open(agent_ref, "r", encoding="utf-8") as f:
|
|
564
|
+
source_config = json.load(f)
|
|
565
|
+
|
|
566
|
+
source_display_name = source_config.get("display_name")
|
|
567
|
+
if not source_display_name:
|
|
568
|
+
source_display_name = _default_display_name(base_name)
|
|
569
|
+
|
|
570
|
+
clone_config = dict(source_config)
|
|
571
|
+
clone_config["name"] = clone_name
|
|
572
|
+
clone_config["display_name"] = _build_clone_display_name(
|
|
573
|
+
source_display_name, clone_index
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
tools = source_config.get("tools", [])
|
|
577
|
+
clone_config["tools"] = (
|
|
578
|
+
_filter_available_tools(tools) if isinstance(tools, list) else []
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if not clone_config.get("model"):
|
|
582
|
+
clone_config.pop("model", None)
|
|
583
|
+
else:
|
|
584
|
+
agent_instance = agent_ref()
|
|
585
|
+
clone_config = {
|
|
586
|
+
"name": clone_name,
|
|
587
|
+
"display_name": _build_clone_display_name(
|
|
588
|
+
agent_instance.display_name, clone_index
|
|
589
|
+
),
|
|
590
|
+
"description": agent_instance.description,
|
|
591
|
+
"system_prompt": agent_instance.get_system_prompt(),
|
|
592
|
+
"tools": _filter_available_tools(agent_instance.get_available_tools()),
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
user_prompt = agent_instance.get_user_prompt()
|
|
596
|
+
if user_prompt is not None:
|
|
597
|
+
clone_config["user_prompt"] = user_prompt
|
|
598
|
+
|
|
599
|
+
tools_config = agent_instance.get_tools_config()
|
|
600
|
+
if tools_config is not None:
|
|
601
|
+
clone_config["tools_config"] = tools_config
|
|
602
|
+
|
|
603
|
+
pinned_model = get_agent_pinned_model(agent_instance.name)
|
|
604
|
+
if pinned_model:
|
|
605
|
+
clone_config["model"] = pinned_model
|
|
606
|
+
except Exception as exc:
|
|
607
|
+
emit_warning(f"Failed to build clone for '{agent_name}': {exc}")
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
if clone_path.exists():
|
|
611
|
+
emit_warning(f"Clone target '{clone_name}' already exists.")
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
with open(clone_path, "w", encoding="utf-8") as f:
|
|
616
|
+
json.dump(clone_config, f, indent=2, ensure_ascii=False)
|
|
617
|
+
emit_success(f"Cloned '{agent_name}' to '{clone_name}'.")
|
|
618
|
+
return clone_name
|
|
619
|
+
except Exception as exc:
|
|
620
|
+
emit_warning(f"Failed to write clone file '{clone_path}': {exc}")
|
|
621
|
+
return None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def delete_clone_agent(agent_name: str) -> bool:
|
|
625
|
+
"""Delete a cloned JSON agent definition.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
agent_name: Clone agent name to delete.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
True if the clone was deleted, False otherwise.
|
|
632
|
+
"""
|
|
633
|
+
message_group_id = str(uuid.uuid4())
|
|
634
|
+
_discover_agents(message_group_id=message_group_id)
|
|
635
|
+
|
|
636
|
+
if not is_clone_agent_name(agent_name):
|
|
637
|
+
emit_warning(f"Agent '{agent_name}' is not a clone.")
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
if get_current_agent_name() == agent_name:
|
|
641
|
+
emit_warning("Cannot delete the active agent. Switch agents first.")
|
|
642
|
+
return False
|
|
643
|
+
|
|
644
|
+
agent_ref = _AGENT_REGISTRY.get(agent_name)
|
|
645
|
+
if agent_ref is None:
|
|
646
|
+
emit_warning(f"Clone '{agent_name}' not found.")
|
|
647
|
+
return False
|
|
648
|
+
|
|
649
|
+
if not isinstance(agent_ref, str):
|
|
650
|
+
emit_warning(f"Clone '{agent_name}' is not a JSON agent.")
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
clone_path = Path(agent_ref)
|
|
654
|
+
if not clone_path.exists():
|
|
655
|
+
emit_warning(f"Clone file for '{agent_name}' does not exist.")
|
|
656
|
+
return False
|
|
657
|
+
|
|
658
|
+
from ..config import get_user_agents_directory
|
|
659
|
+
|
|
660
|
+
agents_dir = Path(get_user_agents_directory()).resolve()
|
|
661
|
+
if clone_path.resolve().parent != agents_dir:
|
|
662
|
+
emit_warning(f"Refusing to delete non-user clone '{agent_name}'.")
|
|
663
|
+
return False
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
clone_path.unlink()
|
|
667
|
+
emit_success(f"Deleted clone '{agent_name}'.")
|
|
668
|
+
_AGENT_REGISTRY.pop(agent_name, None)
|
|
669
|
+
_AGENT_HISTORIES.pop(agent_name, None)
|
|
670
|
+
return True
|
|
671
|
+
except Exception as exc:
|
|
672
|
+
emit_warning(f"Failed to delete clone '{agent_name}': {exc}")
|
|
673
|
+
return False
|