code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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/__init__.py +7 -1
- code_puppy/agents/__init__.py +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- 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/prompt_reviewer.py +145 -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 +174 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +233 -627
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +1 -4
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- 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/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- 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 +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -182
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -15
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/tools/agent_tools.py
CHANGED
|
@@ -1,22 +1,209 @@
|
|
|
1
1
|
# agent_tools.py
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import asyncio
|
|
3
|
+
import hashlib
|
|
4
|
+
import itertools
|
|
5
|
+
import json
|
|
6
|
+
import pickle
|
|
7
|
+
import re
|
|
8
|
+
import traceback
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from functools import partial
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Set
|
|
13
|
+
|
|
14
|
+
from dbos import DBOS, SetWorkflowID
|
|
4
15
|
from pydantic import BaseModel
|
|
5
|
-
from pydantic_ai import RunContext
|
|
6
16
|
|
|
17
|
+
# Import Agent from pydantic_ai to create temporary agents for invocation
|
|
18
|
+
from pydantic_ai import Agent, RunContext, UsageLimits
|
|
19
|
+
from pydantic_ai.messages import ModelMessage
|
|
20
|
+
|
|
21
|
+
from code_puppy.config import (
|
|
22
|
+
DATA_DIR,
|
|
23
|
+
get_message_limit,
|
|
24
|
+
get_use_dbos,
|
|
25
|
+
get_value,
|
|
26
|
+
)
|
|
7
27
|
from code_puppy.messaging import (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
emit_system_message,
|
|
28
|
+
SubAgentInvocationMessage,
|
|
29
|
+
SubAgentResponseMessage,
|
|
11
30
|
emit_error,
|
|
31
|
+
emit_info,
|
|
32
|
+
emit_success,
|
|
33
|
+
get_message_bus,
|
|
34
|
+
get_session_context,
|
|
35
|
+
set_session_context,
|
|
12
36
|
)
|
|
13
37
|
from code_puppy.tools.common import generate_group_id
|
|
14
|
-
from code_puppy.
|
|
38
|
+
from code_puppy.tools.subagent_context import subagent_context
|
|
15
39
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
40
|
+
# Set to track active subagent invocation tasks
|
|
41
|
+
_active_subagent_tasks: Set[asyncio.Task] = set()
|
|
42
|
+
|
|
43
|
+
# Atomic counter for DBOS workflow IDs - ensures uniqueness even in rapid back-to-back calls
|
|
44
|
+
# itertools.count() is thread-safe for next() calls
|
|
45
|
+
_dbos_workflow_counter = itertools.count()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _generate_dbos_workflow_id(base_id: str) -> str:
|
|
49
|
+
"""Generate a unique DBOS workflow ID by appending an atomic counter.
|
|
50
|
+
|
|
51
|
+
DBOS requires workflow IDs to be unique across all executions.
|
|
52
|
+
This function ensures uniqueness by combining the base_id with
|
|
53
|
+
an atomically incrementing counter.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
base_id: The base identifier (e.g., group_id from generate_group_id)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
A unique workflow ID in format: {base_id}-wf-{counter}
|
|
60
|
+
"""
|
|
61
|
+
counter = next(_dbos_workflow_counter)
|
|
62
|
+
return f"{base_id}-wf-{counter}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _generate_session_hash_suffix() -> str:
|
|
66
|
+
"""Generate a short SHA1 hash suffix based on current timestamp for uniqueness.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A 6-character hex string, e.g., "a3f2b1"
|
|
70
|
+
"""
|
|
71
|
+
timestamp = str(datetime.now().timestamp())
|
|
72
|
+
return hashlib.sha1(timestamp.encode()).hexdigest()[:6]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Regex pattern for kebab-case session IDs
|
|
76
|
+
SESSION_ID_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
|
77
|
+
SESSION_ID_MAX_LENGTH = 128
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _validate_session_id(session_id: str) -> None:
|
|
81
|
+
"""Validate that a session ID follows kebab-case naming conventions.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
session_id: The session identifier to validate
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If the session_id is invalid
|
|
88
|
+
|
|
89
|
+
Valid format:
|
|
90
|
+
- Lowercase letters (a-z)
|
|
91
|
+
- Numbers (0-9)
|
|
92
|
+
- Hyphens (-) to separate words
|
|
93
|
+
- No uppercase, no underscores, no special characters
|
|
94
|
+
- Length between 1 and 128 characters
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
Valid: "my-session", "agent-session-1", "discussion-about-code"
|
|
98
|
+
Invalid: "MySession", "my_session", "my session", "my--session"
|
|
99
|
+
"""
|
|
100
|
+
if not session_id:
|
|
101
|
+
raise ValueError("session_id cannot be empty")
|
|
102
|
+
|
|
103
|
+
if len(session_id) > SESSION_ID_MAX_LENGTH:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Invalid session_id '{session_id}': must be {SESSION_ID_MAX_LENGTH} characters or less"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if not SESSION_ID_PATTERN.match(session_id):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Invalid session_id '{session_id}': must be kebab-case "
|
|
111
|
+
"(lowercase letters, numbers, and hyphens only). "
|
|
112
|
+
"Examples: 'my-session', 'agent-session-1', 'discussion-about-code'"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _get_subagent_sessions_dir() -> Path:
|
|
117
|
+
"""Get the directory for storing subagent session data.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Path to XDG data directory/subagent_sessions/
|
|
121
|
+
"""
|
|
122
|
+
sessions_dir = Path(DATA_DIR) / "subagent_sessions"
|
|
123
|
+
sessions_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
124
|
+
return sessions_dir
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _save_session_history(
|
|
128
|
+
session_id: str,
|
|
129
|
+
message_history: List[ModelMessage],
|
|
130
|
+
agent_name: str,
|
|
131
|
+
initial_prompt: str | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Save session history to filesystem.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
session_id: The session identifier (must be kebab-case)
|
|
137
|
+
message_history: List of messages to save
|
|
138
|
+
agent_name: Name of the agent being invoked
|
|
139
|
+
initial_prompt: The first prompt that started this session (for .txt metadata)
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If session_id is not valid kebab-case format
|
|
143
|
+
"""
|
|
144
|
+
# Validate session_id format before saving
|
|
145
|
+
_validate_session_id(session_id)
|
|
146
|
+
|
|
147
|
+
sessions_dir = _get_subagent_sessions_dir()
|
|
148
|
+
|
|
149
|
+
# Save pickle file with message history
|
|
150
|
+
pkl_path = sessions_dir / f"{session_id}.pkl"
|
|
151
|
+
with open(pkl_path, "wb") as f:
|
|
152
|
+
pickle.dump(message_history, f)
|
|
153
|
+
|
|
154
|
+
# Save or update txt file with metadata
|
|
155
|
+
txt_path = sessions_dir / f"{session_id}.txt"
|
|
156
|
+
if not txt_path.exists() and initial_prompt:
|
|
157
|
+
# Only write initial metadata on first save
|
|
158
|
+
metadata = {
|
|
159
|
+
"session_id": session_id,
|
|
160
|
+
"agent_name": agent_name,
|
|
161
|
+
"initial_prompt": initial_prompt,
|
|
162
|
+
"created_at": datetime.now().isoformat(),
|
|
163
|
+
"message_count": len(message_history),
|
|
164
|
+
}
|
|
165
|
+
with open(txt_path, "w") as f:
|
|
166
|
+
json.dump(metadata, f, indent=2)
|
|
167
|
+
elif txt_path.exists():
|
|
168
|
+
# Update message count on subsequent saves
|
|
169
|
+
try:
|
|
170
|
+
with open(txt_path, "r") as f:
|
|
171
|
+
metadata = json.load(f)
|
|
172
|
+
metadata["message_count"] = len(message_history)
|
|
173
|
+
metadata["last_updated"] = datetime.now().isoformat()
|
|
174
|
+
with open(txt_path, "w") as f:
|
|
175
|
+
json.dump(metadata, f, indent=2)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass # If we can't update metadata, no big deal
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _load_session_history(session_id: str) -> List[ModelMessage]:
|
|
181
|
+
"""Load session history from filesystem.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
session_id: The session identifier (must be kebab-case)
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of ModelMessage objects, or empty list if session doesn't exist
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: If session_id is not valid kebab-case format
|
|
191
|
+
"""
|
|
192
|
+
# Validate session_id format before loading
|
|
193
|
+
_validate_session_id(session_id)
|
|
194
|
+
|
|
195
|
+
sessions_dir = _get_subagent_sessions_dir()
|
|
196
|
+
pkl_path = sessions_dir / f"{session_id}.pkl"
|
|
197
|
+
|
|
198
|
+
if not pkl_path.exists():
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
with open(pkl_path, "rb") as f:
|
|
203
|
+
return pickle.load(f)
|
|
204
|
+
except Exception:
|
|
205
|
+
# If pickle is corrupted or incompatible, return empty history
|
|
206
|
+
return []
|
|
20
207
|
|
|
21
208
|
|
|
22
209
|
class AgentInfo(BaseModel):
|
|
@@ -24,6 +211,7 @@ class AgentInfo(BaseModel):
|
|
|
24
211
|
|
|
25
212
|
name: str
|
|
26
213
|
display_name: str
|
|
214
|
+
description: str
|
|
27
215
|
|
|
28
216
|
|
|
29
217
|
class ListAgentsOutput(BaseModel):
|
|
@@ -38,6 +226,7 @@ class AgentInvokeOutput(BaseModel):
|
|
|
38
226
|
|
|
39
227
|
response: str | None
|
|
40
228
|
agent_name: str
|
|
229
|
+
session_id: str | None = None
|
|
41
230
|
error: str | None = None
|
|
42
231
|
|
|
43
232
|
|
|
@@ -58,36 +247,50 @@ def register_list_agents(agent):
|
|
|
58
247
|
# Generate a group ID for this tool execution
|
|
59
248
|
group_id = generate_group_id("list_agents")
|
|
60
249
|
|
|
250
|
+
from rich.text import Text
|
|
251
|
+
|
|
252
|
+
from code_puppy.config import get_banner_color
|
|
253
|
+
|
|
254
|
+
list_agents_color = get_banner_color("list_agents")
|
|
61
255
|
emit_info(
|
|
62
|
-
|
|
256
|
+
Text.from_markup(
|
|
257
|
+
f"\n[bold white on {list_agents_color}] LIST AGENTS [/bold white on {list_agents_color}]"
|
|
258
|
+
),
|
|
63
259
|
message_group=group_id,
|
|
64
260
|
)
|
|
65
|
-
emit_divider(message_group=group_id)
|
|
66
261
|
|
|
67
262
|
try:
|
|
68
|
-
|
|
263
|
+
from code_puppy.agents import get_agent_descriptions, get_available_agents
|
|
264
|
+
|
|
265
|
+
# Get available agents and their descriptions from the agent manager
|
|
69
266
|
agents_dict = get_available_agents()
|
|
267
|
+
descriptions_dict = get_agent_descriptions()
|
|
70
268
|
|
|
71
269
|
# Convert to list of AgentInfo objects
|
|
72
270
|
agents = [
|
|
73
|
-
AgentInfo(
|
|
271
|
+
AgentInfo(
|
|
272
|
+
name=name,
|
|
273
|
+
display_name=display_name,
|
|
274
|
+
description=descriptions_dict.get(name, "No description available"),
|
|
275
|
+
)
|
|
74
276
|
for name, display_name in agents_dict.items()
|
|
75
277
|
]
|
|
76
278
|
|
|
77
|
-
#
|
|
279
|
+
# Accumulate output into a single string and emit once
|
|
280
|
+
# Use Text.from_markup() to pass a Rich object that won't be escaped
|
|
281
|
+
lines = []
|
|
78
282
|
for agent_item in agents:
|
|
79
|
-
|
|
80
|
-
f"- [bold]{agent_item.name}[/bold]: {agent_item.display_name}"
|
|
81
|
-
|
|
283
|
+
lines.append(
|
|
284
|
+
f"- [bold]{agent_item.name}[/bold]: {agent_item.display_name}\n"
|
|
285
|
+
f" [dim]{agent_item.description}[/dim]"
|
|
82
286
|
)
|
|
287
|
+
emit_info(Text.from_markup("\n".join(lines)), message_group=group_id)
|
|
83
288
|
|
|
84
|
-
emit_divider(message_group=group_id)
|
|
85
289
|
return ListAgentsOutput(agents=agents)
|
|
86
290
|
|
|
87
291
|
except Exception as e:
|
|
88
292
|
error_msg = f"Error listing agents: {str(e)}"
|
|
89
293
|
emit_error(error_msg, message_group=group_id)
|
|
90
|
-
emit_divider(message_group=group_id)
|
|
91
294
|
return ListAgentsOutput(agents=[], error=error_msg)
|
|
92
295
|
|
|
93
296
|
return list_agents
|
|
@@ -101,35 +304,162 @@ def register_invoke_agent(agent):
|
|
|
101
304
|
"""
|
|
102
305
|
|
|
103
306
|
@agent.tool
|
|
104
|
-
def invoke_agent(
|
|
105
|
-
context: RunContext, agent_name: str, prompt: str
|
|
307
|
+
async def invoke_agent(
|
|
308
|
+
context: RunContext, agent_name: str, prompt: str, session_id: str | None = None
|
|
106
309
|
) -> AgentInvokeOutput:
|
|
107
310
|
"""Invoke a specific sub-agent with a given prompt.
|
|
108
311
|
|
|
109
312
|
Args:
|
|
110
313
|
agent_name: The name of the agent to invoke
|
|
111
314
|
prompt: The prompt to send to the agent
|
|
315
|
+
session_id: Optional session ID for maintaining conversation memory across invocations.
|
|
316
|
+
|
|
317
|
+
**Session ID Format:**
|
|
318
|
+
- Must be kebab-case (lowercase letters, numbers, hyphens only)
|
|
319
|
+
- Should be human-readable: e.g., "implement-oauth", "review-auth"
|
|
320
|
+
- For NEW sessions, a SHA1 hash suffix is automatically appended for uniqueness
|
|
321
|
+
- To CONTINUE a session, use the full session_id (with hash) from the previous invocation
|
|
322
|
+
- If None (default), auto-generates like "agent-name-session-1"
|
|
323
|
+
|
|
324
|
+
**When to use session_id:**
|
|
325
|
+
- **NEW SESSION**: Provide a base name like "review-auth" - we'll append a unique hash
|
|
326
|
+
- **CONTINUE SESSION**: Use the full session_id from output (e.g., "review-auth-a3f2b1")
|
|
327
|
+
- **ONE-OFF TASKS**: Leave as None (auto-generate)
|
|
328
|
+
|
|
329
|
+
**Most common pattern:** Leave session_id as None (auto-generate) unless you
|
|
330
|
+
specifically need conversational memory.
|
|
112
331
|
|
|
113
332
|
Returns:
|
|
114
|
-
AgentInvokeOutput:
|
|
333
|
+
AgentInvokeOutput: Contains:
|
|
334
|
+
- response (str | None): The agent's response to the prompt
|
|
335
|
+
- agent_name (str): Name of the invoked agent
|
|
336
|
+
- session_id (str | None): The full session ID (with hash suffix) - USE THIS to continue the conversation!
|
|
337
|
+
- error (str | None): Error message if invocation failed
|
|
338
|
+
|
|
339
|
+
Examples:
|
|
340
|
+
# COMMON CASE: One-off invocation, no memory needed (auto-generate session)
|
|
341
|
+
result = invoke_agent(
|
|
342
|
+
"qa-expert",
|
|
343
|
+
"Review this function: def add(a, b): return a + b"
|
|
344
|
+
)
|
|
345
|
+
# result.session_id will be something like "qa-expert-session-a3f2b1"
|
|
346
|
+
|
|
347
|
+
# MULTI-TURN: Start a NEW conversation with a base session ID
|
|
348
|
+
# A hash suffix is auto-appended: "review-add-function" -> "review-add-function-a3f2b1"
|
|
349
|
+
result1 = invoke_agent(
|
|
350
|
+
"qa-expert",
|
|
351
|
+
"Review this function: def add(a, b): return a + b",
|
|
352
|
+
session_id="review-add-function"
|
|
353
|
+
)
|
|
354
|
+
# result1.session_id contains the full ID like "review-add-function-a3f2b1"
|
|
355
|
+
|
|
356
|
+
# Continue the SAME conversation using session_id from the previous result
|
|
357
|
+
result2 = invoke_agent(
|
|
358
|
+
"qa-expert",
|
|
359
|
+
"Can you suggest edge cases for that function?",
|
|
360
|
+
session_id=result1.session_id # Use the session_id from previous output!
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Multiple INDEPENDENT reviews (each gets unique hash suffix)
|
|
364
|
+
auth_review = invoke_agent(
|
|
365
|
+
"code-reviewer",
|
|
366
|
+
"Review my authentication code",
|
|
367
|
+
session_id="auth-review" # -> "auth-review-<hash1>"
|
|
368
|
+
)
|
|
369
|
+
# auth_review.session_id contains the full ID to continue this review
|
|
370
|
+
|
|
371
|
+
payment_review = invoke_agent(
|
|
372
|
+
"code-reviewer",
|
|
373
|
+
"Review my payment processing code",
|
|
374
|
+
session_id="payment-review" # -> "payment-review-<hash2>"
|
|
375
|
+
)
|
|
376
|
+
# payment_review.session_id contains a different full ID
|
|
115
377
|
"""
|
|
378
|
+
from code_puppy.agents.agent_manager import load_agent
|
|
379
|
+
|
|
380
|
+
# Validate user-provided session_id if given
|
|
381
|
+
if session_id is not None:
|
|
382
|
+
try:
|
|
383
|
+
_validate_session_id(session_id)
|
|
384
|
+
except ValueError as e:
|
|
385
|
+
# Return error immediately if session_id is invalid
|
|
386
|
+
group_id = generate_group_id("invoke_agent", agent_name)
|
|
387
|
+
emit_error(str(e), message_group=group_id)
|
|
388
|
+
return AgentInvokeOutput(
|
|
389
|
+
response=None, agent_name=agent_name, error=str(e)
|
|
390
|
+
)
|
|
391
|
+
|
|
116
392
|
# Generate a group ID for this tool execution
|
|
117
393
|
group_id = generate_group_id("invoke_agent", agent_name)
|
|
118
394
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
395
|
+
# Check if this is an existing session or a new one
|
|
396
|
+
# For user-provided session_id, check if it exists
|
|
397
|
+
# For None, we'll generate a new one below
|
|
398
|
+
if session_id is not None:
|
|
399
|
+
message_history = _load_session_history(session_id)
|
|
400
|
+
is_new_session = len(message_history) == 0
|
|
401
|
+
else:
|
|
402
|
+
message_history = []
|
|
403
|
+
is_new_session = True
|
|
404
|
+
|
|
405
|
+
# Generate or finalize session_id
|
|
406
|
+
if session_id is None:
|
|
407
|
+
# Auto-generate a session ID with hash suffix for uniqueness
|
|
408
|
+
# Example: "qa-expert-session-a3f2b1"
|
|
409
|
+
hash_suffix = _generate_session_hash_suffix()
|
|
410
|
+
session_id = f"{agent_name}-session-{hash_suffix}"
|
|
411
|
+
elif is_new_session:
|
|
412
|
+
# User provided a base name for a NEW session - append hash suffix
|
|
413
|
+
# Example: "review-auth" -> "review-auth-a3f2b1"
|
|
414
|
+
hash_suffix = _generate_session_hash_suffix()
|
|
415
|
+
session_id = f"{session_id}-{hash_suffix}"
|
|
416
|
+
# else: continuing existing session, use session_id as-is
|
|
417
|
+
|
|
418
|
+
# Lazy imports to avoid circular dependency
|
|
419
|
+
from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
|
|
420
|
+
|
|
421
|
+
# Emit structured invocation message via MessageBus
|
|
422
|
+
bus = get_message_bus()
|
|
423
|
+
bus.emit(
|
|
424
|
+
SubAgentInvocationMessage(
|
|
425
|
+
agent_name=agent_name,
|
|
426
|
+
session_id=session_id,
|
|
427
|
+
prompt=prompt,
|
|
428
|
+
is_new_session=is_new_session,
|
|
429
|
+
message_count=len(message_history),
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Save current session context and set the new one for this sub-agent
|
|
434
|
+
previous_session_id = get_session_context()
|
|
435
|
+
set_session_context(session_id)
|
|
436
|
+
|
|
437
|
+
# Set terminal session for browser-based terminal tools
|
|
438
|
+
# This uses contextvars which properly propagate through async tasks
|
|
439
|
+
from code_puppy.tools.browser.terminal_tools import (
|
|
440
|
+
_terminal_session_var,
|
|
441
|
+
set_terminal_session,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
terminal_session_token = set_terminal_session(f"terminal-{session_id}")
|
|
445
|
+
|
|
446
|
+
# Set browser session for browser tools (qa-kitten, etc.)
|
|
447
|
+
# This allows parallel agent invocations to each have their own browser
|
|
448
|
+
from code_puppy.tools.browser.browser_manager import (
|
|
449
|
+
set_browser_session,
|
|
122
450
|
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
emit_divider(message_group=group_id)
|
|
451
|
+
|
|
452
|
+
browser_session_token = set_browser_session(f"browser-{session_id}")
|
|
126
453
|
|
|
127
454
|
try:
|
|
455
|
+
# Lazy import to break circular dependency with messaging module
|
|
456
|
+
from code_puppy.model_factory import ModelFactory, make_model_settings
|
|
457
|
+
|
|
128
458
|
# Load the specified agent config
|
|
129
|
-
agent_config =
|
|
459
|
+
agent_config = load_agent(agent_name)
|
|
130
460
|
|
|
131
461
|
# Get the current model for creating a temporary agent
|
|
132
|
-
model_name = get_model_name()
|
|
462
|
+
model_name = agent_config.get_model_name()
|
|
133
463
|
models_config = ModelFactory.load_config()
|
|
134
464
|
|
|
135
465
|
# Only proceed if we have a valid model configuration
|
|
@@ -140,36 +470,205 @@ def register_invoke_agent(agent):
|
|
|
140
470
|
|
|
141
471
|
# Create a temporary agent instance to avoid interfering with current agent state
|
|
142
472
|
instructions = agent_config.get_system_prompt()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
473
|
+
|
|
474
|
+
# Add AGENTS.md content to subagents
|
|
475
|
+
puppy_rules = agent_config.load_puppy_rules()
|
|
476
|
+
if puppy_rules:
|
|
477
|
+
instructions += f"\n\n{puppy_rules}"
|
|
478
|
+
|
|
479
|
+
# Apply prompt additions (like file permission handling) to temporary agents
|
|
480
|
+
from code_puppy import callbacks
|
|
481
|
+
from code_puppy.model_utils import prepare_prompt_for_model
|
|
482
|
+
|
|
483
|
+
prompt_additions = callbacks.on_load_prompt()
|
|
484
|
+
if len(prompt_additions):
|
|
485
|
+
instructions += "\n" + "\n".join(prompt_additions)
|
|
486
|
+
|
|
487
|
+
# Handle claude-code models: swap instructions, and prepend system prompt only on first message
|
|
488
|
+
prepared = prepare_prompt_for_model(
|
|
489
|
+
model_name,
|
|
490
|
+
instructions,
|
|
491
|
+
prompt,
|
|
492
|
+
prepend_system_to_user=is_new_session, # Only prepend on first message
|
|
148
493
|
)
|
|
494
|
+
instructions = prepared.instructions
|
|
495
|
+
prompt = prepared.user_prompt
|
|
496
|
+
|
|
497
|
+
subagent_name = f"temp-invoke-agent-{session_id}"
|
|
498
|
+
model_settings = make_model_settings(model_name)
|
|
499
|
+
|
|
500
|
+
# Get MCP servers for sub-agents (same as main agent)
|
|
501
|
+
from code_puppy.mcp_ import get_mcp_manager
|
|
502
|
+
|
|
503
|
+
mcp_servers = []
|
|
504
|
+
mcp_disabled = get_value("disable_mcp_servers")
|
|
505
|
+
if not (
|
|
506
|
+
mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on")
|
|
507
|
+
):
|
|
508
|
+
manager = get_mcp_manager()
|
|
509
|
+
mcp_servers = manager.get_servers_for_agent()
|
|
510
|
+
|
|
511
|
+
if get_use_dbos():
|
|
512
|
+
from pydantic_ai.durable_exec.dbos import DBOSAgent
|
|
513
|
+
|
|
514
|
+
# For DBOS, create agent without MCP servers (to avoid serialization issues)
|
|
515
|
+
# and add them at runtime
|
|
516
|
+
temp_agent = Agent(
|
|
517
|
+
model=model,
|
|
518
|
+
instructions=instructions,
|
|
519
|
+
output_type=str,
|
|
520
|
+
retries=3,
|
|
521
|
+
toolsets=[], # MCP servers added separately for DBOS
|
|
522
|
+
history_processors=[agent_config.message_history_accumulator],
|
|
523
|
+
model_settings=model_settings,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Register the tools that the agent needs
|
|
527
|
+
from code_puppy.tools import register_tools_for_agent
|
|
149
528
|
|
|
150
|
-
|
|
151
|
-
|
|
529
|
+
agent_tools = agent_config.get_available_tools()
|
|
530
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
152
531
|
|
|
153
|
-
|
|
154
|
-
|
|
532
|
+
# Wrap with DBOS - no streaming for sub-agents
|
|
533
|
+
dbos_agent = DBOSAgent(
|
|
534
|
+
temp_agent,
|
|
535
|
+
name=subagent_name,
|
|
536
|
+
)
|
|
537
|
+
temp_agent = dbos_agent
|
|
538
|
+
|
|
539
|
+
# Store MCP servers to add at runtime
|
|
540
|
+
subagent_mcp_servers = mcp_servers
|
|
541
|
+
else:
|
|
542
|
+
# Non-DBOS path - include MCP servers directly in the agent
|
|
543
|
+
temp_agent = Agent(
|
|
544
|
+
model=model,
|
|
545
|
+
instructions=instructions,
|
|
546
|
+
output_type=str,
|
|
547
|
+
retries=3,
|
|
548
|
+
toolsets=mcp_servers,
|
|
549
|
+
history_processors=[agent_config.message_history_accumulator],
|
|
550
|
+
model_settings=model_settings,
|
|
551
|
+
)
|
|
155
552
|
|
|
156
|
-
|
|
157
|
-
|
|
553
|
+
# Register the tools that the agent needs
|
|
554
|
+
from code_puppy.tools import register_tools_for_agent
|
|
555
|
+
|
|
556
|
+
agent_tools = agent_config.get_available_tools()
|
|
557
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
558
|
+
|
|
559
|
+
subagent_mcp_servers = None
|
|
560
|
+
|
|
561
|
+
# Run the temporary agent with the provided prompt as an asyncio task
|
|
562
|
+
# Pass the message_history from the session to continue the conversation
|
|
563
|
+
workflow_id = None # Track for potential cancellation
|
|
564
|
+
|
|
565
|
+
# Always use subagent_stream_handler to silence output and update console manager
|
|
566
|
+
# This ensures all sub-agent output goes through the aggregated dashboard
|
|
567
|
+
stream_handler = partial(subagent_stream_handler, session_id=session_id)
|
|
568
|
+
|
|
569
|
+
# Wrap the agent run in subagent context for tracking
|
|
570
|
+
with subagent_context(agent_name):
|
|
571
|
+
if get_use_dbos():
|
|
572
|
+
# Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
|
|
573
|
+
workflow_id = _generate_dbos_workflow_id(group_id)
|
|
574
|
+
|
|
575
|
+
# Add MCP servers to the DBOS agent's toolsets
|
|
576
|
+
# (temp_agent is discarded after this invocation, so no need to restore)
|
|
577
|
+
if subagent_mcp_servers:
|
|
578
|
+
temp_agent._toolsets = (
|
|
579
|
+
temp_agent._toolsets + subagent_mcp_servers
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
with SetWorkflowID(workflow_id):
|
|
583
|
+
task = asyncio.create_task(
|
|
584
|
+
temp_agent.run(
|
|
585
|
+
prompt,
|
|
586
|
+
message_history=message_history,
|
|
587
|
+
usage_limits=UsageLimits(
|
|
588
|
+
request_limit=get_message_limit()
|
|
589
|
+
),
|
|
590
|
+
event_stream_handler=stream_handler,
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
_active_subagent_tasks.add(task)
|
|
594
|
+
else:
|
|
595
|
+
task = asyncio.create_task(
|
|
596
|
+
temp_agent.run(
|
|
597
|
+
prompt,
|
|
598
|
+
message_history=message_history,
|
|
599
|
+
usage_limits=UsageLimits(request_limit=get_message_limit()),
|
|
600
|
+
event_stream_handler=stream_handler,
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
_active_subagent_tasks.add(task)
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
result = await task
|
|
607
|
+
finally:
|
|
608
|
+
_active_subagent_tasks.discard(task)
|
|
609
|
+
if task.cancelled():
|
|
610
|
+
if get_use_dbos() and workflow_id:
|
|
611
|
+
DBOS.cancel_workflow(workflow_id)
|
|
158
612
|
|
|
159
613
|
# Extract the response from the result
|
|
160
614
|
response = result.output
|
|
161
615
|
|
|
162
|
-
|
|
163
|
-
|
|
616
|
+
# Update the session history with the new messages from this interaction
|
|
617
|
+
# The result contains all_messages which includes the full conversation
|
|
618
|
+
updated_history = result.all_messages()
|
|
164
619
|
|
|
165
|
-
|
|
620
|
+
# Save to filesystem (include initial prompt only for new sessions)
|
|
621
|
+
_save_session_history(
|
|
622
|
+
session_id=session_id,
|
|
623
|
+
message_history=updated_history,
|
|
624
|
+
agent_name=agent_name,
|
|
625
|
+
initial_prompt=prompt if is_new_session else None,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Emit structured response message via MessageBus
|
|
629
|
+
bus.emit(
|
|
630
|
+
SubAgentResponseMessage(
|
|
631
|
+
agent_name=agent_name,
|
|
632
|
+
session_id=session_id,
|
|
633
|
+
response=response,
|
|
634
|
+
message_count=len(updated_history),
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
# Emit clean completion summary
|
|
639
|
+
emit_success(
|
|
640
|
+
f"✓ {agent_name} completed successfully", message_group=group_id
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
return AgentInvokeOutput(
|
|
644
|
+
response=response, agent_name=agent_name, session_id=session_id
|
|
645
|
+
)
|
|
166
646
|
|
|
167
647
|
except Exception as e:
|
|
168
|
-
|
|
648
|
+
# Emit clean failure summary
|
|
649
|
+
emit_error(f"✗ {agent_name} failed: {str(e)}", message_group=group_id)
|
|
650
|
+
|
|
651
|
+
# Full traceback for debugging
|
|
652
|
+
error_msg = f"Error invoking agent '{agent_name}': {traceback.format_exc()}"
|
|
169
653
|
emit_error(error_msg, message_group=group_id)
|
|
170
|
-
|
|
654
|
+
|
|
171
655
|
return AgentInvokeOutput(
|
|
172
|
-
response=None,
|
|
656
|
+
response=None,
|
|
657
|
+
agent_name=agent_name,
|
|
658
|
+
session_id=session_id,
|
|
659
|
+
error=error_msg,
|
|
173
660
|
)
|
|
174
661
|
|
|
662
|
+
finally:
|
|
663
|
+
# Restore the previous session context
|
|
664
|
+
set_session_context(previous_session_id)
|
|
665
|
+
# Reset terminal session context
|
|
666
|
+
_terminal_session_var.reset(terminal_session_token)
|
|
667
|
+
# Reset browser session context
|
|
668
|
+
from code_puppy.tools.browser.browser_manager import (
|
|
669
|
+
_browser_session_var,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
_browser_session_var.reset(browser_session_token)
|
|
673
|
+
|
|
175
674
|
return invoke_agent
|