shotgun-sh 0.2.17__py3-none-any.whl → 0.4.0.dev1__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.
- shotgun/agents/agent_manager.py +219 -37
- shotgun/agents/common.py +79 -78
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +364 -53
- shotgun/agents/config/models.py +101 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +12 -13
- shotgun/agents/models.py +66 -1
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +376 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +503 -0
- shotgun/agents/router/tools/plan_tools.py +322 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/file_management.py +49 -1
- shotgun/agents/tools/registry.py +2 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/codebase/core/change_detector.py +1 -1
- shotgun/codebase/core/ingestor.py +154 -8
- shotgun/codebase/core/manager.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
- shotgun/prompts/agents/plan.j2 +29 -1
- shotgun/prompts/agents/research.j2 +75 -23
- shotgun/prompts/agents/router.j2 +440 -0
- shotgun/prompts/agents/specify.j2 +80 -4
- shotgun/prompts/agents/state/system_state.j2 +15 -8
- shotgun/prompts/agents/tasks.j2 +63 -23
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/tui/app.py +78 -15
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/status_bar.py +2 -2
- shotgun/tui/containers.py +1 -1
- shotgun/tui/dependencies.py +64 -9
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +9 -1
- shotgun/tui/screens/chat/chat_screen.py +1015 -106
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -89
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +179 -26
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
- shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
- shotgun_sh-0.2.17.dist-info/RECORD +0 -194
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Router agent factory for the intelligent orchestrator.
|
|
2
|
+
|
|
3
|
+
The Router agent is the single user-facing interface that orchestrates
|
|
4
|
+
sub-agents (Research, Specify, Plan, Tasks, Export) based on user intent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import traceback
|
|
8
|
+
from functools import partial
|
|
9
|
+
|
|
10
|
+
from pydantic_ai import Agent, Tool
|
|
11
|
+
from pydantic_ai.agent import AgentRunResult
|
|
12
|
+
from pydantic_ai.messages import ModelMessage
|
|
13
|
+
|
|
14
|
+
from shotgun.agents.common import (
|
|
15
|
+
add_system_status_message,
|
|
16
|
+
build_agent_system_prompt,
|
|
17
|
+
create_usage_limits,
|
|
18
|
+
run_agent,
|
|
19
|
+
)
|
|
20
|
+
from shotgun.agents.config import ProviderType, get_provider_model
|
|
21
|
+
from shotgun.agents.conversation.history import token_limit_compactor
|
|
22
|
+
from shotgun.agents.models import AgentResponse, AgentRuntimeOptions, AgentType
|
|
23
|
+
from shotgun.agents.router.models import RouterDeps
|
|
24
|
+
from shotgun.agents.router.tools import (
|
|
25
|
+
add_step,
|
|
26
|
+
create_plan,
|
|
27
|
+
mark_step_done,
|
|
28
|
+
remove_step,
|
|
29
|
+
)
|
|
30
|
+
from shotgun.agents.router.tools.delegation_tools import (
|
|
31
|
+
delegate_to_export,
|
|
32
|
+
delegate_to_plan,
|
|
33
|
+
delegate_to_research,
|
|
34
|
+
delegate_to_specification,
|
|
35
|
+
delegate_to_tasks,
|
|
36
|
+
prepare_delegation_tool,
|
|
37
|
+
)
|
|
38
|
+
from shotgun.agents.tools import read_file
|
|
39
|
+
from shotgun.logging_config import get_logger
|
|
40
|
+
from shotgun.sdk.services import get_codebase_service
|
|
41
|
+
from shotgun.utils import ensure_shotgun_directory_exists
|
|
42
|
+
|
|
43
|
+
logger = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def create_router_agent(
|
|
47
|
+
agent_runtime_options: AgentRuntimeOptions,
|
|
48
|
+
provider: ProviderType | None = None,
|
|
49
|
+
) -> tuple[Agent[RouterDeps, AgentResponse], RouterDeps]:
|
|
50
|
+
"""Create the Router agent with plan management and delegation capabilities.
|
|
51
|
+
|
|
52
|
+
The Router is the intelligent orchestrator that:
|
|
53
|
+
- Understands user intent
|
|
54
|
+
- Creates and manages execution plans
|
|
55
|
+
- Delegates work to specialized sub-agents
|
|
56
|
+
- Operates in Planning (incremental) or Drafting (auto-execute) mode
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
agent_runtime_options: Runtime options for the agent
|
|
60
|
+
provider: Optional provider override. If None, uses configured default
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (Configured Router agent, RouterDeps with plan management state)
|
|
64
|
+
"""
|
|
65
|
+
logger.debug("Initializing router agent")
|
|
66
|
+
ensure_shotgun_directory_exists()
|
|
67
|
+
|
|
68
|
+
# Get configured model
|
|
69
|
+
try:
|
|
70
|
+
model_config = await get_provider_model(provider)
|
|
71
|
+
logger.debug(
|
|
72
|
+
"Router agent using %s model: %s",
|
|
73
|
+
model_config.provider.value.upper(),
|
|
74
|
+
model_config.name,
|
|
75
|
+
)
|
|
76
|
+
model = model_config.model_instance
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error("Failed to load configured model for router: %s", e)
|
|
79
|
+
raise ValueError("Configured model is required for router agent") from e
|
|
80
|
+
|
|
81
|
+
# Create RouterDeps (extends AgentDeps with router-specific state)
|
|
82
|
+
codebase_service = get_codebase_service()
|
|
83
|
+
system_prompt_fn = partial(build_agent_system_prompt, "router")
|
|
84
|
+
|
|
85
|
+
deps = RouterDeps(
|
|
86
|
+
**agent_runtime_options.model_dump(),
|
|
87
|
+
llm_model=model_config,
|
|
88
|
+
codebase_service=codebase_service,
|
|
89
|
+
system_prompt_fn=system_prompt_fn,
|
|
90
|
+
agent_mode=AgentType.ROUTER,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Create history processor with access to deps via closure
|
|
94
|
+
async def history_processor(messages: list[ModelMessage]) -> list[ModelMessage]:
|
|
95
|
+
"""History processor with access to deps via closure."""
|
|
96
|
+
|
|
97
|
+
class ProcessorContext:
|
|
98
|
+
def __init__(self, deps: RouterDeps):
|
|
99
|
+
self.deps = deps
|
|
100
|
+
self.usage = None
|
|
101
|
+
|
|
102
|
+
ctx = ProcessorContext(deps)
|
|
103
|
+
return await token_limit_compactor(ctx, messages) # type: ignore[arg-type]
|
|
104
|
+
|
|
105
|
+
# Delegation tools with prepare function - only visible after plan is approved
|
|
106
|
+
# in Planning mode, always available in Drafting mode
|
|
107
|
+
delegation_tools = [
|
|
108
|
+
Tool(delegate_to_research, prepare=prepare_delegation_tool),
|
|
109
|
+
Tool(delegate_to_specification, prepare=prepare_delegation_tool),
|
|
110
|
+
Tool(delegate_to_plan, prepare=prepare_delegation_tool),
|
|
111
|
+
Tool(delegate_to_tasks, prepare=prepare_delegation_tool),
|
|
112
|
+
Tool(delegate_to_export, prepare=prepare_delegation_tool),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
# Create the agent with delegation tools that have prepare functions
|
|
116
|
+
agent: Agent[RouterDeps, AgentResponse] = Agent(
|
|
117
|
+
model,
|
|
118
|
+
output_type=AgentResponse,
|
|
119
|
+
deps_type=RouterDeps,
|
|
120
|
+
instrument=True,
|
|
121
|
+
history_processors=[history_processor],
|
|
122
|
+
retries=3,
|
|
123
|
+
tools=delegation_tools,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Register plan management tools (router-specific, always available)
|
|
127
|
+
agent.tool(create_plan)
|
|
128
|
+
agent.tool(mark_step_done)
|
|
129
|
+
agent.tool(add_step)
|
|
130
|
+
agent.tool(remove_step)
|
|
131
|
+
|
|
132
|
+
# Register read-only file access for .shotgun/ directory
|
|
133
|
+
agent.tool(read_file)
|
|
134
|
+
|
|
135
|
+
# Note: The Router does NOT have write_file, append_file, or codebase tools.
|
|
136
|
+
# All file modifications and codebase understanding must be delegated to
|
|
137
|
+
# the appropriate sub-agent (Research, Specify, Plan, Tasks, Export).
|
|
138
|
+
|
|
139
|
+
logger.debug("Router agent tools registered")
|
|
140
|
+
logger.info(
|
|
141
|
+
"Router agent created in %s mode",
|
|
142
|
+
deps.router_mode.value.upper(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return agent, deps
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def run_router_agent(
|
|
149
|
+
agent: Agent[RouterDeps, AgentResponse],
|
|
150
|
+
prompt: str,
|
|
151
|
+
deps: RouterDeps,
|
|
152
|
+
message_history: list[ModelMessage] | None = None,
|
|
153
|
+
) -> AgentRunResult[AgentResponse]:
|
|
154
|
+
"""Run the router agent with a user prompt.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
agent: The configured router agent
|
|
158
|
+
prompt: User's request
|
|
159
|
+
deps: RouterDeps with plan management state
|
|
160
|
+
message_history: Optional existing message history
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Agent run result with response and any clarifying questions
|
|
164
|
+
"""
|
|
165
|
+
logger.debug("Running router agent with prompt: %s", prompt[:100])
|
|
166
|
+
|
|
167
|
+
message_history = await add_system_status_message(deps, message_history)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
usage_limits = create_usage_limits()
|
|
171
|
+
|
|
172
|
+
result = await run_agent(
|
|
173
|
+
agent=agent, # type: ignore[arg-type]
|
|
174
|
+
prompt=prompt,
|
|
175
|
+
deps=deps,
|
|
176
|
+
message_history=message_history,
|
|
177
|
+
usage_limits=usage_limits,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
logger.debug("Router agent completed successfully")
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
except Exception:
|
|
184
|
+
logger.error("Router agent error:\n%s", traceback.format_exc())
|
|
185
|
+
raise
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Router tools package."""
|
|
2
|
+
|
|
3
|
+
from shotgun.agents.router.tools.plan_tools import (
|
|
4
|
+
add_step,
|
|
5
|
+
create_plan,
|
|
6
|
+
mark_step_done,
|
|
7
|
+
remove_step,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Note: Delegation tools are imported directly in router.py to use
|
|
11
|
+
# the prepare_delegation_tool function for conditional visibility.
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"add_step",
|
|
15
|
+
"create_plan",
|
|
16
|
+
"mark_step_done",
|
|
17
|
+
"remove_step",
|
|
18
|
+
]
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""Delegation tools for the Router agent.
|
|
2
|
+
|
|
3
|
+
These tools allow the Router to delegate work to specialized sub-agents
|
|
4
|
+
(Research, Specify, Plan, Tasks, Export) for specific tasks.
|
|
5
|
+
|
|
6
|
+
Sub-agents run with isolated message histories to prevent context window bloat.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic_ai import RunContext
|
|
13
|
+
from pydantic_ai.tools import ToolDefinition
|
|
14
|
+
|
|
15
|
+
from shotgun.agents.export import create_export_agent, run_export_agent
|
|
16
|
+
from shotgun.agents.models import (
|
|
17
|
+
AgentDeps,
|
|
18
|
+
AgentRuntimeOptions,
|
|
19
|
+
AgentType,
|
|
20
|
+
ShotgunAgent,
|
|
21
|
+
SubAgentContext,
|
|
22
|
+
)
|
|
23
|
+
from shotgun.agents.plan import create_plan_agent, run_plan_agent
|
|
24
|
+
from shotgun.agents.research import create_research_agent, run_research_agent
|
|
25
|
+
from shotgun.agents.router.models import (
|
|
26
|
+
DelegationInput,
|
|
27
|
+
DelegationResult,
|
|
28
|
+
RouterDeps,
|
|
29
|
+
RouterMode,
|
|
30
|
+
SubAgentCacheEntry,
|
|
31
|
+
)
|
|
32
|
+
from shotgun.agents.specify import create_specify_agent, run_specify_agent
|
|
33
|
+
from shotgun.agents.tasks import create_tasks_agent, run_tasks_agent
|
|
34
|
+
from shotgun.agents.tools.registry import ToolCategory, register_tool
|
|
35
|
+
from shotgun.logging_config import get_logger
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# Tool Preparation (Conditional Availability)
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def prepare_delegation_tool(
|
|
46
|
+
ctx: RunContext[RouterDeps], tool_def: ToolDefinition
|
|
47
|
+
) -> ToolDefinition | None:
|
|
48
|
+
"""Prepare function to conditionally show delegation tools.
|
|
49
|
+
|
|
50
|
+
In Planning mode, delegation tools are ONLY available when:
|
|
51
|
+
1. A plan exists (current_plan is not None)
|
|
52
|
+
2. The plan has been approved (pending_approval is None)
|
|
53
|
+
|
|
54
|
+
In Drafting mode, delegation tools are always available.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
ctx: RunContext with RouterDeps containing plan state.
|
|
58
|
+
tool_def: The tool definition to conditionally return.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The tool_def if delegation is allowed, None to hide the tool.
|
|
62
|
+
"""
|
|
63
|
+
deps = ctx.deps
|
|
64
|
+
|
|
65
|
+
# Drafting mode - tools always available
|
|
66
|
+
if deps.router_mode == RouterMode.DRAFTING:
|
|
67
|
+
return tool_def
|
|
68
|
+
|
|
69
|
+
# Planning mode - check plan state
|
|
70
|
+
if deps.current_plan is None:
|
|
71
|
+
logger.debug("Hiding %s: no plan exists in Planning mode", tool_def.name)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
if deps.pending_approval is not None:
|
|
75
|
+
logger.debug("Hiding %s: plan pending user approval", tool_def.name)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Plan exists and is approved - allow delegation
|
|
79
|
+
return tool_def
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Type aliases for factory functions
|
|
83
|
+
CreateAgentFn = Callable[
|
|
84
|
+
[AgentRuntimeOptions], Awaitable[tuple[ShotgunAgent, AgentDeps]]
|
|
85
|
+
]
|
|
86
|
+
RunAgentFn = Callable[..., Awaitable[Any]]
|
|
87
|
+
|
|
88
|
+
# Maximum retries for transient errors
|
|
89
|
+
MAX_RETRIES = 2
|
|
90
|
+
|
|
91
|
+
# Map agent types to their factory and run functions
|
|
92
|
+
AGENT_FACTORIES: dict[AgentType, tuple[CreateAgentFn, RunAgentFn]] = {
|
|
93
|
+
AgentType.RESEARCH: (create_research_agent, run_research_agent),
|
|
94
|
+
AgentType.SPECIFY: (create_specify_agent, run_specify_agent),
|
|
95
|
+
AgentType.PLAN: (create_plan_agent, run_plan_agent),
|
|
96
|
+
AgentType.TASKS: (create_tasks_agent, run_tasks_agent),
|
|
97
|
+
AgentType.EXPORT: (create_export_agent, run_export_agent),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_retryable_error(exception: BaseException) -> bool:
|
|
102
|
+
"""Check if exception should trigger a retry.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
exception: The exception to check.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if the exception is a transient error that should be retried.
|
|
109
|
+
"""
|
|
110
|
+
# ValueError for truncated/incomplete JSON
|
|
111
|
+
if isinstance(exception, ValueError):
|
|
112
|
+
error_str = str(exception)
|
|
113
|
+
return "EOF while parsing" in error_str or (
|
|
114
|
+
"JSON" in error_str and "parsing" in error_str
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# API errors (overload, rate limits)
|
|
118
|
+
exception_name = type(exception).__name__
|
|
119
|
+
if "APIStatusError" in exception_name:
|
|
120
|
+
error_str = str(exception)
|
|
121
|
+
return "overload" in error_str.lower() or "rate" in error_str.lower()
|
|
122
|
+
|
|
123
|
+
# Network errors
|
|
124
|
+
if "ConnectionError" in exception_name or "TimeoutError" in exception_name:
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _create_agent_runtime_options(deps: RouterDeps) -> AgentRuntimeOptions:
|
|
131
|
+
"""Create AgentRuntimeOptions from RouterDeps for sub-agent creation.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
deps: RouterDeps containing shared runtime configuration.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
AgentRuntimeOptions configured for sub-agent creation.
|
|
138
|
+
"""
|
|
139
|
+
return AgentRuntimeOptions(
|
|
140
|
+
interactive_mode=deps.interactive_mode,
|
|
141
|
+
working_directory=deps.working_directory,
|
|
142
|
+
is_tui_context=deps.is_tui_context,
|
|
143
|
+
max_iterations=deps.max_iterations,
|
|
144
|
+
queue=deps.queue,
|
|
145
|
+
tasks=deps.tasks,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _get_or_create_sub_agent(
|
|
150
|
+
deps: RouterDeps,
|
|
151
|
+
agent_type: AgentType,
|
|
152
|
+
) -> SubAgentCacheEntry:
|
|
153
|
+
"""Get a cached sub-agent or create a new one.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
deps: RouterDeps with sub_agent_cache.
|
|
157
|
+
agent_type: The type of agent to get or create.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Tuple of (agent, agent_deps) for the requested agent type.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError: If agent_type is not supported for delegation.
|
|
164
|
+
"""
|
|
165
|
+
# Check cache first
|
|
166
|
+
if agent_type in deps.sub_agent_cache:
|
|
167
|
+
logger.debug("Using cached %s agent", agent_type.value)
|
|
168
|
+
return deps.sub_agent_cache[agent_type]
|
|
169
|
+
|
|
170
|
+
# Get factory functions
|
|
171
|
+
if agent_type not in AGENT_FACTORIES:
|
|
172
|
+
raise ValueError(f"Agent type {agent_type} is not supported for delegation")
|
|
173
|
+
|
|
174
|
+
create_fn, _ = AGENT_FACTORIES[agent_type]
|
|
175
|
+
runtime_options = _create_agent_runtime_options(deps)
|
|
176
|
+
|
|
177
|
+
logger.debug("Creating new %s agent for delegation", agent_type.value)
|
|
178
|
+
agent, agent_deps = await create_fn(runtime_options)
|
|
179
|
+
|
|
180
|
+
# Cache for reuse
|
|
181
|
+
cache_entry: SubAgentCacheEntry = (agent, agent_deps)
|
|
182
|
+
deps.sub_agent_cache[agent_type] = cache_entry
|
|
183
|
+
|
|
184
|
+
return cache_entry
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _build_sub_agent_context(deps: RouterDeps) -> SubAgentContext:
|
|
188
|
+
"""Build SubAgentContext with plan information.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
deps: RouterDeps with current plan.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
SubAgentContext configured for delegation.
|
|
195
|
+
"""
|
|
196
|
+
current_step = deps.current_plan.current_step() if deps.current_plan else None
|
|
197
|
+
|
|
198
|
+
return SubAgentContext(
|
|
199
|
+
is_router_delegated=True,
|
|
200
|
+
plan_goal=deps.current_plan.goal if deps.current_plan else "",
|
|
201
|
+
current_step_id=current_step.id if current_step else "",
|
|
202
|
+
current_step_title=current_step.title if current_step else "",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def _run_sub_agent(
|
|
207
|
+
ctx: RunContext[RouterDeps],
|
|
208
|
+
agent_type: AgentType,
|
|
209
|
+
task: str,
|
|
210
|
+
context_hint: str | None = None,
|
|
211
|
+
) -> DelegationResult:
|
|
212
|
+
"""Run a sub-agent with the given task.
|
|
213
|
+
|
|
214
|
+
This helper function handles:
|
|
215
|
+
- Checking for pending approval (blocks delegation until user approves)
|
|
216
|
+
- Getting or creating the sub-agent from cache
|
|
217
|
+
- Setting up SubAgentContext
|
|
218
|
+
- Managing active_sub_agent state for UI updates
|
|
219
|
+
- Running the sub-agent with isolated message history
|
|
220
|
+
- Extracting files_modified and handling errors with retries
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
ctx: RunContext with RouterDeps.
|
|
224
|
+
agent_type: The type of sub-agent to run.
|
|
225
|
+
task: The task to delegate to the sub-agent.
|
|
226
|
+
context_hint: Optional context to help the sub-agent.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
DelegationResult with success/failure status, response, and files_modified.
|
|
230
|
+
"""
|
|
231
|
+
deps = ctx.deps
|
|
232
|
+
|
|
233
|
+
# Note: Delegation checks are now handled by prepare_delegation_tool which
|
|
234
|
+
# hides delegation tools entirely when delegation isn't allowed. This is a
|
|
235
|
+
# cleaner approach than returning errors - the LLM simply won't see the tools.
|
|
236
|
+
|
|
237
|
+
# Build the prompt with context hint if provided
|
|
238
|
+
prompt = task
|
|
239
|
+
if context_hint:
|
|
240
|
+
prompt = f"{task}\n\nContext: {context_hint}"
|
|
241
|
+
|
|
242
|
+
# Get or create the sub-agent
|
|
243
|
+
try:
|
|
244
|
+
agent, sub_agent_deps = await _get_or_create_sub_agent(deps, agent_type)
|
|
245
|
+
except ValueError as e:
|
|
246
|
+
return DelegationResult(
|
|
247
|
+
success=False,
|
|
248
|
+
error=str(e),
|
|
249
|
+
response="",
|
|
250
|
+
files_modified=[],
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Set up SubAgentContext so sub-agent knows it's being orchestrated
|
|
254
|
+
sub_agent_deps.sub_agent_context = _build_sub_agent_context(deps)
|
|
255
|
+
|
|
256
|
+
# Clear sub-agent's file tracker for fresh tracking
|
|
257
|
+
sub_agent_deps.file_tracker.clear()
|
|
258
|
+
|
|
259
|
+
# Set active_sub_agent for UI mode indicator
|
|
260
|
+
deps.active_sub_agent = agent_type
|
|
261
|
+
logger.info("Delegating to %s agent: %s", agent_type.value, task[:100])
|
|
262
|
+
|
|
263
|
+
# Get the run function for this agent type
|
|
264
|
+
_, run_fn = AGENT_FACTORIES[agent_type]
|
|
265
|
+
|
|
266
|
+
# Retry loop for transient errors
|
|
267
|
+
last_error: BaseException | None = None
|
|
268
|
+
for attempt in range(MAX_RETRIES + 1):
|
|
269
|
+
try:
|
|
270
|
+
# Run sub-agent with isolated message history and streaming support
|
|
271
|
+
result = await run_fn(
|
|
272
|
+
agent=agent,
|
|
273
|
+
prompt=prompt,
|
|
274
|
+
deps=sub_agent_deps,
|
|
275
|
+
message_history=[], # Isolated context
|
|
276
|
+
event_stream_handler=deps.parent_stream_handler, # Forward streaming
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Extract response text
|
|
280
|
+
response_text = ""
|
|
281
|
+
if result and result.output:
|
|
282
|
+
response_text = result.output.response
|
|
283
|
+
|
|
284
|
+
# Extract files modified
|
|
285
|
+
files_modified = [
|
|
286
|
+
op.file_path for op in sub_agent_deps.file_tracker.operations
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
# Check for clarifying questions
|
|
290
|
+
has_questions = False
|
|
291
|
+
questions: list[str] = []
|
|
292
|
+
if result and result.output and result.output.clarifying_questions:
|
|
293
|
+
has_questions = True
|
|
294
|
+
questions = result.output.clarifying_questions
|
|
295
|
+
|
|
296
|
+
logger.info(
|
|
297
|
+
"Sub-agent %s completed. Files modified: %s",
|
|
298
|
+
agent_type.value,
|
|
299
|
+
files_modified,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Clear active_sub_agent
|
|
303
|
+
deps.active_sub_agent = None
|
|
304
|
+
|
|
305
|
+
return DelegationResult(
|
|
306
|
+
success=True,
|
|
307
|
+
response=response_text,
|
|
308
|
+
files_modified=files_modified,
|
|
309
|
+
has_questions=has_questions,
|
|
310
|
+
questions=questions,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
last_error = e
|
|
315
|
+
if _is_retryable_error(e) and attempt < MAX_RETRIES:
|
|
316
|
+
logger.warning(
|
|
317
|
+
"Sub-agent %s failed (attempt %d/%d), retrying: %s",
|
|
318
|
+
agent_type.value,
|
|
319
|
+
attempt + 1,
|
|
320
|
+
MAX_RETRIES + 1,
|
|
321
|
+
str(e),
|
|
322
|
+
)
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
# Non-retryable error or max retries exceeded
|
|
326
|
+
logger.error(
|
|
327
|
+
"Sub-agent %s failed after %d attempts: %s",
|
|
328
|
+
agent_type.value,
|
|
329
|
+
attempt + 1,
|
|
330
|
+
str(e),
|
|
331
|
+
)
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
# Clear active_sub_agent on failure
|
|
335
|
+
deps.active_sub_agent = None
|
|
336
|
+
|
|
337
|
+
return DelegationResult(
|
|
338
|
+
success=False,
|
|
339
|
+
error=str(last_error) if last_error else "Unknown error",
|
|
340
|
+
response="",
|
|
341
|
+
files_modified=[],
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# =============================================================================
|
|
346
|
+
# Delegation Tools
|
|
347
|
+
# =============================================================================
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@register_tool(
|
|
351
|
+
category=ToolCategory.DELEGATION,
|
|
352
|
+
display_text="Delegating to Research agent",
|
|
353
|
+
key_arg="task",
|
|
354
|
+
)
|
|
355
|
+
async def delegate_to_research(
|
|
356
|
+
ctx: RunContext[RouterDeps],
|
|
357
|
+
input: DelegationInput,
|
|
358
|
+
) -> DelegationResult:
|
|
359
|
+
"""Delegate a task to the Research agent.
|
|
360
|
+
|
|
361
|
+
The Research agent specializes in:
|
|
362
|
+
- Finding information via web search
|
|
363
|
+
- Analyzing code and documentation
|
|
364
|
+
- Gathering background research
|
|
365
|
+
- Saving research findings to .shotgun/research.md
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
ctx: RunContext with RouterDeps.
|
|
369
|
+
input: DelegationInput with task and optional context_hint.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
DelegationResult with the research findings.
|
|
373
|
+
"""
|
|
374
|
+
return await _run_sub_agent(
|
|
375
|
+
ctx,
|
|
376
|
+
AgentType.RESEARCH,
|
|
377
|
+
input.task,
|
|
378
|
+
input.context_hint,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@register_tool(
|
|
383
|
+
category=ToolCategory.DELEGATION,
|
|
384
|
+
display_text="Delegating to Specification agent",
|
|
385
|
+
key_arg="task",
|
|
386
|
+
)
|
|
387
|
+
async def delegate_to_specification(
|
|
388
|
+
ctx: RunContext[RouterDeps],
|
|
389
|
+
input: DelegationInput,
|
|
390
|
+
) -> DelegationResult:
|
|
391
|
+
"""Delegate a task to the Specification agent.
|
|
392
|
+
|
|
393
|
+
The Specification agent specializes in:
|
|
394
|
+
- Writing and updating .shotgun/specification.md
|
|
395
|
+
- Creating Pydantic contracts in .shotgun/contracts/
|
|
396
|
+
- Defining technical requirements and interfaces
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
ctx: RunContext with RouterDeps.
|
|
400
|
+
input: DelegationInput with task and optional context_hint.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
DelegationResult with the specification updates.
|
|
404
|
+
"""
|
|
405
|
+
return await _run_sub_agent(
|
|
406
|
+
ctx,
|
|
407
|
+
AgentType.SPECIFY,
|
|
408
|
+
input.task,
|
|
409
|
+
input.context_hint,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@register_tool(
|
|
414
|
+
category=ToolCategory.DELEGATION,
|
|
415
|
+
display_text="Delegating to Plan agent",
|
|
416
|
+
key_arg="task",
|
|
417
|
+
)
|
|
418
|
+
async def delegate_to_plan(
|
|
419
|
+
ctx: RunContext[RouterDeps],
|
|
420
|
+
input: DelegationInput,
|
|
421
|
+
) -> DelegationResult:
|
|
422
|
+
"""Delegate a task to the Plan agent.
|
|
423
|
+
|
|
424
|
+
The Plan agent specializes in:
|
|
425
|
+
- Writing and updating .shotgun/plan.md
|
|
426
|
+
- Creating implementation plans with stages
|
|
427
|
+
- Defining technical approach and architecture
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
ctx: RunContext with RouterDeps.
|
|
431
|
+
input: DelegationInput with task and optional context_hint.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
DelegationResult with the plan updates.
|
|
435
|
+
"""
|
|
436
|
+
return await _run_sub_agent(
|
|
437
|
+
ctx,
|
|
438
|
+
AgentType.PLAN,
|
|
439
|
+
input.task,
|
|
440
|
+
input.context_hint,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@register_tool(
|
|
445
|
+
category=ToolCategory.DELEGATION,
|
|
446
|
+
display_text="Delegating to Tasks agent",
|
|
447
|
+
key_arg="task",
|
|
448
|
+
)
|
|
449
|
+
async def delegate_to_tasks(
|
|
450
|
+
ctx: RunContext[RouterDeps],
|
|
451
|
+
input: DelegationInput,
|
|
452
|
+
) -> DelegationResult:
|
|
453
|
+
"""Delegate a task to the Tasks agent.
|
|
454
|
+
|
|
455
|
+
The Tasks agent specializes in:
|
|
456
|
+
- Writing and updating .shotgun/tasks.md
|
|
457
|
+
- Creating actionable implementation tasks
|
|
458
|
+
- Breaking down work into manageable items
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
ctx: RunContext with RouterDeps.
|
|
462
|
+
input: DelegationInput with task and optional context_hint.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
DelegationResult with the task list updates.
|
|
466
|
+
"""
|
|
467
|
+
return await _run_sub_agent(
|
|
468
|
+
ctx,
|
|
469
|
+
AgentType.TASKS,
|
|
470
|
+
input.task,
|
|
471
|
+
input.context_hint,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@register_tool(
|
|
476
|
+
category=ToolCategory.DELEGATION,
|
|
477
|
+
display_text="Delegating to Export agent",
|
|
478
|
+
key_arg="task",
|
|
479
|
+
)
|
|
480
|
+
async def delegate_to_export(
|
|
481
|
+
ctx: RunContext[RouterDeps],
|
|
482
|
+
input: DelegationInput,
|
|
483
|
+
) -> DelegationResult:
|
|
484
|
+
"""Delegate a task to the Export agent.
|
|
485
|
+
|
|
486
|
+
The Export agent specializes in:
|
|
487
|
+
- Exporting artifacts and deliverables
|
|
488
|
+
- Generating outputs to .shotgun/export/
|
|
489
|
+
- Creating documentation exports
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
ctx: RunContext with RouterDeps.
|
|
493
|
+
input: DelegationInput with task and optional context_hint.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
DelegationResult with the export results.
|
|
497
|
+
"""
|
|
498
|
+
return await _run_sub_agent(
|
|
499
|
+
ctx,
|
|
500
|
+
AgentType.EXPORT,
|
|
501
|
+
input.task,
|
|
502
|
+
input.context_hint,
|
|
503
|
+
)
|