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
shotgun/agents/plan.py
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from functools import partial
|
|
4
4
|
|
|
5
|
-
from pydantic_ai import (
|
|
6
|
-
Agent,
|
|
7
|
-
)
|
|
8
5
|
from pydantic_ai.agent import AgentRunResult
|
|
9
6
|
from pydantic_ai.messages import ModelMessage
|
|
10
7
|
|
|
11
8
|
from shotgun.agents.config import ProviderType
|
|
9
|
+
from shotgun.agents.models import ShotgunAgent
|
|
12
10
|
from shotgun.logging_config import get_logger
|
|
13
11
|
|
|
14
12
|
from .common import (
|
|
13
|
+
EventStreamHandler,
|
|
15
14
|
add_system_status_message,
|
|
16
15
|
build_agent_system_prompt,
|
|
17
16
|
create_base_agent,
|
|
@@ -25,7 +24,7 @@ logger = get_logger(__name__)
|
|
|
25
24
|
|
|
26
25
|
async def create_plan_agent(
|
|
27
26
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
28
|
-
) -> tuple[
|
|
27
|
+
) -> tuple[ShotgunAgent, AgentDeps]:
|
|
29
28
|
"""Create a plan agent with artifact management capabilities.
|
|
30
29
|
|
|
31
30
|
Args:
|
|
@@ -51,26 +50,25 @@ async def create_plan_agent(
|
|
|
51
50
|
|
|
52
51
|
|
|
53
52
|
async def run_plan_agent(
|
|
54
|
-
agent:
|
|
55
|
-
|
|
53
|
+
agent: ShotgunAgent,
|
|
54
|
+
prompt: str,
|
|
56
55
|
deps: AgentDeps,
|
|
57
56
|
message_history: list[ModelMessage] | None = None,
|
|
57
|
+
event_stream_handler: EventStreamHandler | None = None,
|
|
58
58
|
) -> AgentRunResult[AgentResponse]:
|
|
59
|
-
"""Create or update a plan based on the given
|
|
59
|
+
"""Create or update a plan based on the given prompt using artifacts.
|
|
60
60
|
|
|
61
61
|
Args:
|
|
62
62
|
agent: The configured plan agent
|
|
63
|
-
|
|
63
|
+
prompt: The planning prompt or instruction
|
|
64
64
|
deps: Agent dependencies
|
|
65
65
|
message_history: Optional message history for conversation continuity
|
|
66
|
+
event_stream_handler: Optional callback for streaming events
|
|
66
67
|
|
|
67
68
|
Returns:
|
|
68
69
|
AgentRunResult containing the planning process output
|
|
69
70
|
"""
|
|
70
|
-
logger.debug("📋 Starting planning for
|
|
71
|
-
|
|
72
|
-
# Simple prompt - the agent system prompt has all the artifact instructions
|
|
73
|
-
full_prompt = f"Create a comprehensive plan for: {goal}"
|
|
71
|
+
logger.debug("📋 Starting planning for prompt: %s", prompt)
|
|
74
72
|
|
|
75
73
|
try:
|
|
76
74
|
# Create usage limits for responsible API usage
|
|
@@ -80,10 +78,11 @@ async def run_plan_agent(
|
|
|
80
78
|
|
|
81
79
|
result = await run_agent(
|
|
82
80
|
agent=agent,
|
|
83
|
-
prompt=
|
|
81
|
+
prompt=prompt,
|
|
84
82
|
deps=deps,
|
|
85
83
|
message_history=message_history,
|
|
86
84
|
usage_limits=usage_limits,
|
|
85
|
+
event_stream_handler=event_stream_handler,
|
|
87
86
|
)
|
|
88
87
|
|
|
89
88
|
logger.debug("✅ Planning completed successfully")
|
shotgun/agents/research.py
CHANGED
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from functools import partial
|
|
4
4
|
|
|
5
|
-
from pydantic_ai import (
|
|
6
|
-
Agent,
|
|
7
|
-
)
|
|
8
5
|
from pydantic_ai.agent import AgentRunResult
|
|
9
6
|
from pydantic_ai.messages import (
|
|
10
7
|
ModelMessage,
|
|
11
8
|
)
|
|
12
9
|
|
|
13
10
|
from shotgun.agents.config import ProviderType
|
|
11
|
+
from shotgun.agents.models import ShotgunAgent
|
|
14
12
|
from shotgun.logging_config import get_logger
|
|
15
13
|
|
|
16
14
|
from .common import (
|
|
15
|
+
EventStreamHandler,
|
|
17
16
|
add_system_status_message,
|
|
18
17
|
build_agent_system_prompt,
|
|
19
18
|
create_base_agent,
|
|
@@ -28,7 +27,7 @@ logger = get_logger(__name__)
|
|
|
28
27
|
|
|
29
28
|
async def create_research_agent(
|
|
30
29
|
agent_runtime_options: AgentRuntimeOptions, provider: ProviderType | None = None
|
|
31
|
-
) -> tuple[
|
|
30
|
+
) -> tuple[ShotgunAgent, AgentDeps]:
|
|
32
31
|
"""Create a research agent with web search and artifact management capabilities.
|
|
33
32
|
|
|
34
33
|
Args:
|
|
@@ -65,22 +64,25 @@ async def create_research_agent(
|
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
async def run_research_agent(
|
|
68
|
-
agent:
|
|
69
|
-
|
|
67
|
+
agent: ShotgunAgent,
|
|
68
|
+
prompt: str,
|
|
70
69
|
deps: AgentDeps,
|
|
71
70
|
message_history: list[ModelMessage] | None = None,
|
|
71
|
+
event_stream_handler: EventStreamHandler | None = None,
|
|
72
72
|
) -> AgentRunResult[AgentResponse]:
|
|
73
|
-
"""Perform research on the given
|
|
73
|
+
"""Perform research on the given prompt and update research artifacts.
|
|
74
74
|
|
|
75
75
|
Args:
|
|
76
76
|
agent: The configured research agent
|
|
77
|
-
|
|
77
|
+
prompt: The research prompt to investigate
|
|
78
78
|
deps: Agent dependencies
|
|
79
|
+
message_history: Optional message history for conversation continuity
|
|
80
|
+
event_stream_handler: Optional callback for streaming events
|
|
79
81
|
|
|
80
82
|
Returns:
|
|
81
83
|
Summary of research findings
|
|
82
84
|
"""
|
|
83
|
-
logger.debug("🔬 Starting research for
|
|
85
|
+
logger.debug("🔬 Starting research for prompt: %s", prompt)
|
|
84
86
|
|
|
85
87
|
message_history = await add_system_status_message(deps, message_history)
|
|
86
88
|
|
|
@@ -90,10 +92,11 @@ async def run_research_agent(
|
|
|
90
92
|
|
|
91
93
|
result = await run_agent(
|
|
92
94
|
agent=agent,
|
|
93
|
-
prompt=
|
|
95
|
+
prompt=prompt,
|
|
94
96
|
deps=deps,
|
|
95
97
|
message_history=message_history,
|
|
96
98
|
usage_limits=usage_limits,
|
|
99
|
+
event_stream_handler=event_stream_handler,
|
|
97
100
|
)
|
|
98
101
|
|
|
99
102
|
logger.debug("✅ Research completed successfully")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Router Agent - The intelligent orchestrator for shotgun agents."""
|
|
2
|
+
|
|
3
|
+
from shotgun.agents.router.models import (
|
|
4
|
+
CascadeScope,
|
|
5
|
+
CreatePlanInput,
|
|
6
|
+
DelegationInput,
|
|
7
|
+
DelegationResult,
|
|
8
|
+
ExecutionPlan,
|
|
9
|
+
ExecutionStep,
|
|
10
|
+
ExecutionStepInput,
|
|
11
|
+
MarkStepDoneInput,
|
|
12
|
+
PlanApprovalStatus,
|
|
13
|
+
RemoveStepInput,
|
|
14
|
+
RouterDeps,
|
|
15
|
+
RouterMode,
|
|
16
|
+
StepCheckpointAction,
|
|
17
|
+
SubAgentResult,
|
|
18
|
+
SubAgentResultStatus,
|
|
19
|
+
ToolResult,
|
|
20
|
+
)
|
|
21
|
+
from shotgun.agents.router.router import create_router_agent, run_router_agent
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Agent factory
|
|
25
|
+
"create_router_agent",
|
|
26
|
+
"run_router_agent",
|
|
27
|
+
# Enums
|
|
28
|
+
"RouterMode",
|
|
29
|
+
"PlanApprovalStatus",
|
|
30
|
+
"StepCheckpointAction",
|
|
31
|
+
"CascadeScope",
|
|
32
|
+
"SubAgentResultStatus",
|
|
33
|
+
# Plan models
|
|
34
|
+
"ExecutionStep",
|
|
35
|
+
"ExecutionPlan",
|
|
36
|
+
# Tool I/O models
|
|
37
|
+
"ExecutionStepInput",
|
|
38
|
+
"CreatePlanInput",
|
|
39
|
+
"MarkStepDoneInput",
|
|
40
|
+
"RemoveStepInput",
|
|
41
|
+
"DelegationInput",
|
|
42
|
+
"ToolResult",
|
|
43
|
+
"DelegationResult",
|
|
44
|
+
"SubAgentResult",
|
|
45
|
+
# Deps
|
|
46
|
+
"RouterDeps",
|
|
47
|
+
]
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Router Agent Data Models.
|
|
3
|
+
|
|
4
|
+
Type definitions for the Router Agent MVP.
|
|
5
|
+
These models define the contracts between router, sub-agents, and UI.
|
|
6
|
+
|
|
7
|
+
IMPORTANT: All tool inputs/outputs must use Pydantic models - no raw dict/list/tuple.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import AsyncIterable, Awaitable, Callable
|
|
11
|
+
from enum import StrEnum
|
|
12
|
+
from typing import TYPE_CHECKING, Final
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from shotgun.agents.models import AgentDeps
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
# Import SubAgentContext from the main models module
|
|
20
|
+
from shotgun.agents.models import SubAgentContext
|
|
21
|
+
|
|
22
|
+
# Re-export SubAgentContext for convenience
|
|
23
|
+
__all__ = ["SubAgentContext"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Mode & Status Enums
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RouterMode(StrEnum):
|
|
32
|
+
"""Router execution modes."""
|
|
33
|
+
|
|
34
|
+
PLANNING = "planning" # Incremental, confirmatory - asks before acting
|
|
35
|
+
DRAFTING = "drafting" # Auto-execute - runs full plan without stopping
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PlanApprovalStatus(StrEnum):
|
|
39
|
+
"""Status of plan approval in Planning mode."""
|
|
40
|
+
|
|
41
|
+
PENDING = "pending" # Plan shown, awaiting user decision
|
|
42
|
+
APPROVED = "approved" # User approved, ready to execute
|
|
43
|
+
REJECTED = "rejected" # User wants to clarify/modify
|
|
44
|
+
SKIPPED = "skipped" # Simple request, no approval needed
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StepCheckpointAction(StrEnum):
|
|
48
|
+
"""User action at step checkpoint (Planning mode only)."""
|
|
49
|
+
|
|
50
|
+
CONTINUE = "continue" # Proceed to next step
|
|
51
|
+
MODIFY = "modify" # User wants to adjust the plan
|
|
52
|
+
STOP = "stop" # Stop execution, keep remaining steps
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CascadeScope(StrEnum):
|
|
56
|
+
"""Scope for cascade updates to dependent files."""
|
|
57
|
+
|
|
58
|
+
ALL = "all" # Update all dependent files
|
|
59
|
+
PLAN_ONLY = "plan_only" # Update only plan.md
|
|
60
|
+
TASKS_ONLY = "tasks_only" # Update only tasks.md
|
|
61
|
+
NONE = "none" # Don't update any dependents
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SubAgentResultStatus(StrEnum):
|
|
65
|
+
"""Status of sub-agent execution."""
|
|
66
|
+
|
|
67
|
+
SUCCESS = "success"
|
|
68
|
+
PARTIAL = "partial" # Interrupted or incomplete
|
|
69
|
+
ERROR = "error"
|
|
70
|
+
NEEDS_CLARIFICATION = "needs_clarification"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Execution Plan Models (In-Memory, Not File-Based)
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ExecutionStep(BaseModel):
|
|
79
|
+
"""A single step in an execution plan."""
|
|
80
|
+
|
|
81
|
+
id: str = Field(
|
|
82
|
+
..., description="Human-readable identifier (e.g., 'research-oauth')"
|
|
83
|
+
)
|
|
84
|
+
title: str = Field(..., description="Short title SHOWN to user in plan display")
|
|
85
|
+
objective: str = Field(
|
|
86
|
+
..., description="Detailed goal HIDDEN from user (for sub-agent)"
|
|
87
|
+
)
|
|
88
|
+
done: bool = Field(
|
|
89
|
+
default=False, description="Whether this step has been completed"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ExecutionPlan(BaseModel):
|
|
94
|
+
"""
|
|
95
|
+
Router's execution plan.
|
|
96
|
+
|
|
97
|
+
Stored IN-MEMORY in RouterDeps, NOT in a file.
|
|
98
|
+
Shown to router in system status message every turn.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
goal: str = Field(..., description="High-level goal from user request")
|
|
102
|
+
steps: list[ExecutionStep] = Field(
|
|
103
|
+
default_factory=list, description="Ordered list of execution steps"
|
|
104
|
+
)
|
|
105
|
+
current_step_index: int = Field(
|
|
106
|
+
default=0, description="Index of currently executing step"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def needs_approval(self) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Determine if plan requires user approval in Planning mode.
|
|
112
|
+
|
|
113
|
+
All plans require approval - user should always see and approve
|
|
114
|
+
the plan before execution begins.
|
|
115
|
+
"""
|
|
116
|
+
return len(self.steps) >= 1
|
|
117
|
+
|
|
118
|
+
def current_step(self) -> ExecutionStep | None:
|
|
119
|
+
"""Get the current step being executed."""
|
|
120
|
+
if 0 <= self.current_step_index < len(self.steps):
|
|
121
|
+
return self.steps[self.current_step_index]
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def next_step(self) -> ExecutionStep | None:
|
|
125
|
+
"""Get the next step to execute."""
|
|
126
|
+
next_idx = self.current_step_index + 1
|
|
127
|
+
if next_idx < len(self.steps):
|
|
128
|
+
return self.steps[next_idx]
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def is_complete(self) -> bool:
|
|
132
|
+
"""Check if all steps are done."""
|
|
133
|
+
return all(step.done for step in self.steps)
|
|
134
|
+
|
|
135
|
+
def pending_steps(self) -> list[ExecutionStep]:
|
|
136
|
+
"""Get steps that haven't been completed."""
|
|
137
|
+
return [step for step in self.steps if not step.done]
|
|
138
|
+
|
|
139
|
+
def format_for_display(self) -> str:
|
|
140
|
+
"""Format plan for display in system status message."""
|
|
141
|
+
lines = [f"**Goal:** {self.goal}", "", "**Steps:**"]
|
|
142
|
+
for i, step in enumerate(self.steps):
|
|
143
|
+
marker = "✅" if step.done else "⬜"
|
|
144
|
+
current = " ◀" if i == self.current_step_index and not step.done else ""
|
|
145
|
+
lines.append(f"{i + 1}. {marker} {step.title}{current}")
|
|
146
|
+
return "\n".join(lines)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# =============================================================================
|
|
150
|
+
# Tool Input Models (Pydantic only - no dict/list/tuple)
|
|
151
|
+
# =============================================================================
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ExecutionStepInput(BaseModel):
|
|
155
|
+
"""Input model for creating a step."""
|
|
156
|
+
|
|
157
|
+
id: str = Field(..., description="Human-readable identifier")
|
|
158
|
+
title: str = Field(..., description="Short title shown to user")
|
|
159
|
+
objective: str = Field(..., description="Detailed goal for sub-agent")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CreatePlanInput(BaseModel):
|
|
163
|
+
"""Input model for create_plan tool."""
|
|
164
|
+
|
|
165
|
+
goal: str = Field(..., description="High-level goal from user request")
|
|
166
|
+
steps: list[ExecutionStepInput] = Field(..., description="Ordered list of steps")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class MarkStepDoneInput(BaseModel):
|
|
170
|
+
"""Input model for mark_step_done tool."""
|
|
171
|
+
|
|
172
|
+
step_id: str = Field(..., description="ID of the step to mark as done")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AddStepInput(BaseModel):
|
|
176
|
+
"""Input model for add_step tool."""
|
|
177
|
+
|
|
178
|
+
step: ExecutionStepInput = Field(..., description="The step to add")
|
|
179
|
+
after_step_id: str | None = Field(
|
|
180
|
+
default=None, description="Insert after this step ID (None = append to end)"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class RemoveStepInput(BaseModel):
|
|
185
|
+
"""Input model for remove_step tool."""
|
|
186
|
+
|
|
187
|
+
step_id: str = Field(..., description="ID of the step to remove")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class DelegationInput(BaseModel):
|
|
191
|
+
"""Input model for delegation tools."""
|
|
192
|
+
|
|
193
|
+
task: str = Field(..., description="The task to delegate to the sub-agent")
|
|
194
|
+
context_hint: str | None = Field(
|
|
195
|
+
default=None, description="Optional context to help the sub-agent"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# =============================================================================
|
|
200
|
+
# Tool Output Models (Pydantic only - no dict/list/tuple)
|
|
201
|
+
# =============================================================================
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ToolResult(BaseModel):
|
|
205
|
+
"""Generic result from a tool operation."""
|
|
206
|
+
|
|
207
|
+
success: bool = Field(..., description="Whether the operation succeeded")
|
|
208
|
+
message: str = Field(..., description="Human-readable result message")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class DelegationResult(BaseModel):
|
|
212
|
+
"""Result from a sub-agent delegation."""
|
|
213
|
+
|
|
214
|
+
success: bool = Field(..., description="Whether delegation succeeded")
|
|
215
|
+
response: str = Field(default="", description="Sub-agent's response text")
|
|
216
|
+
files_modified: list[str] = Field(
|
|
217
|
+
default_factory=list, description="Files modified by sub-agent"
|
|
218
|
+
)
|
|
219
|
+
has_questions: bool = Field(
|
|
220
|
+
default=False, description="Whether sub-agent has clarifying questions"
|
|
221
|
+
)
|
|
222
|
+
questions: list[str] = Field(
|
|
223
|
+
default_factory=list, description="Clarifying questions from sub-agent"
|
|
224
|
+
)
|
|
225
|
+
error: str | None = Field(default=None, description="Error message if failed")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class SubAgentResult(BaseModel):
|
|
229
|
+
"""Full result from a sub-agent execution."""
|
|
230
|
+
|
|
231
|
+
status: SubAgentResultStatus = Field(..., description="Execution status")
|
|
232
|
+
response: str = Field(default="", description="Sub-agent's response text")
|
|
233
|
+
questions: list[str] = Field(
|
|
234
|
+
default_factory=list, description="Clarifying questions from sub-agent (if any)"
|
|
235
|
+
)
|
|
236
|
+
partial_response: str = Field(default="", description="Partial work if interrupted")
|
|
237
|
+
error: str | None = Field(
|
|
238
|
+
default=None, description="Error message if status is ERROR"
|
|
239
|
+
)
|
|
240
|
+
is_retryable: bool = Field(
|
|
241
|
+
default=False, description="Whether the error is transient and retryable"
|
|
242
|
+
)
|
|
243
|
+
files_modified: list[str] = Field(
|
|
244
|
+
default_factory=list, description="Files modified by this sub-agent"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# =============================================================================
|
|
249
|
+
# Pending State Models (for UI coordination)
|
|
250
|
+
# =============================================================================
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class PendingCheckpoint(BaseModel):
|
|
254
|
+
"""Pending checkpoint state for Planning mode step-by-step execution.
|
|
255
|
+
|
|
256
|
+
Set by mark_step_done tool to trigger checkpoint UI.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
completed_step: ExecutionStep = Field(
|
|
260
|
+
..., description="The step that was just completed"
|
|
261
|
+
)
|
|
262
|
+
next_step: ExecutionStep | None = Field(
|
|
263
|
+
default=None, description="The next step to execute, or None if plan complete"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class PendingCascade(BaseModel):
|
|
268
|
+
"""Pending cascade confirmation state for Planning mode.
|
|
269
|
+
|
|
270
|
+
Set when a file with dependents is modified and cascade confirmation is needed.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
updated_file: str = Field(..., description="The file that was just updated")
|
|
274
|
+
dependent_files: list[str] = Field(
|
|
275
|
+
default_factory=list, description="Files that depend on the updated file"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class PendingApproval(BaseModel):
|
|
280
|
+
"""Pending approval state for Planning mode multi-step plans.
|
|
281
|
+
|
|
282
|
+
Set by create_plan tool when plan.needs_approval() returns True
|
|
283
|
+
(i.e., the plan has more than one step) in Planning mode.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
plan: "ExecutionPlan" = Field(..., description="The plan that needs user approval")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# =============================================================================
|
|
290
|
+
# File Dependency Map (for Cascade Confirmation)
|
|
291
|
+
# =============================================================================
|
|
292
|
+
|
|
293
|
+
FILE_DEPENDENCIES: Final[dict[str, tuple[str, ...]]] = {
|
|
294
|
+
"research.md": ("specification.md", "plan.md", "tasks.md"),
|
|
295
|
+
"specification.md": ("plan.md", "tasks.md"),
|
|
296
|
+
"plan.md": ("tasks.md",),
|
|
297
|
+
"tasks.md": (), # Leaf node, no dependents
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_dependent_files(file_path: str) -> list[str]:
|
|
302
|
+
"""Get files that depend on the given file."""
|
|
303
|
+
# Normalize path to just filename
|
|
304
|
+
file_name = file_path.split("/")[-1]
|
|
305
|
+
return list(FILE_DEPENDENCIES.get(file_name, ()))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# =============================================================================
|
|
309
|
+
# RouterDeps (extends AgentDeps)
|
|
310
|
+
# =============================================================================
|
|
311
|
+
|
|
312
|
+
# Import AgentDeps for inheritance - must be done here to avoid circular imports
|
|
313
|
+
from shotgun.agents.models import AgentDeps, AgentType # noqa: E402
|
|
314
|
+
|
|
315
|
+
# Type alias for sub-agent cache entries
|
|
316
|
+
# Each entry is a tuple of (Agent instance, AgentDeps instance)
|
|
317
|
+
# Using object for agent to avoid forward reference issues with pydantic
|
|
318
|
+
SubAgentCacheEntry = tuple[object, AgentDeps]
|
|
319
|
+
|
|
320
|
+
# Type alias for event stream handler callback
|
|
321
|
+
# Matches the signature expected by pydantic_ai's agent.run()
|
|
322
|
+
# Using object to avoid forward reference issues with pydantic
|
|
323
|
+
EventStreamHandler = Callable[[object, AsyncIterable[object]], Awaitable[None]]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class RouterDeps(AgentDeps):
|
|
327
|
+
"""
|
|
328
|
+
Router-specific dependencies that extend AgentDeps.
|
|
329
|
+
|
|
330
|
+
This class contains router-specific state on top of the base AgentDeps.
|
|
331
|
+
It is used by the router agent and its tools to manage execution plans
|
|
332
|
+
and sub-agent orchestration.
|
|
333
|
+
|
|
334
|
+
Fields:
|
|
335
|
+
router_mode: Current execution mode (Planning or Drafting)
|
|
336
|
+
current_plan: The execution plan stored in-memory (NOT file-based)
|
|
337
|
+
approval_status: Current approval state for the plan
|
|
338
|
+
active_sub_agent: Which sub-agent is currently executing (for UI)
|
|
339
|
+
is_executing: Whether a plan is currently being executed
|
|
340
|
+
sub_agent_cache: Cached sub-agent instances for lazy initialization
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
router_mode: RouterMode = Field(default=RouterMode.PLANNING)
|
|
344
|
+
current_plan: ExecutionPlan | None = Field(default=None)
|
|
345
|
+
approval_status: PlanApprovalStatus = Field(default=PlanApprovalStatus.SKIPPED)
|
|
346
|
+
active_sub_agent: AgentType | None = Field(default=None)
|
|
347
|
+
is_executing: bool = Field(default=False)
|
|
348
|
+
sub_agent_cache: dict[AgentType, SubAgentCacheEntry] = Field(default_factory=dict)
|
|
349
|
+
# Checkpoint state for Planning mode step-by-step execution
|
|
350
|
+
# Set by mark_step_done tool to trigger checkpoint UI
|
|
351
|
+
# Excluded from serialization as it's transient UI state
|
|
352
|
+
pending_checkpoint: PendingCheckpoint | None = Field(default=None, exclude=True)
|
|
353
|
+
# Cascade confirmation state for Planning mode
|
|
354
|
+
# Set when a file with dependents is modified
|
|
355
|
+
# Excluded from serialization as it's transient UI state
|
|
356
|
+
pending_cascade: PendingCascade | None = Field(default=None, exclude=True)
|
|
357
|
+
# Approval state for Planning mode multi-step plans
|
|
358
|
+
# Set by create_plan tool when plan.needs_approval() returns True
|
|
359
|
+
# Excluded from serialization as it's transient UI state
|
|
360
|
+
pending_approval: PendingApproval | None = Field(default=None, exclude=True)
|
|
361
|
+
# Event stream handler for forwarding sub-agent streaming events to UI
|
|
362
|
+
# This is set by the AgentManager when running the router with streaming
|
|
363
|
+
# Excluded from serialization as it's a callable
|
|
364
|
+
parent_stream_handler: EventStreamHandler | None = Field(
|
|
365
|
+
default=None,
|
|
366
|
+
exclude=True,
|
|
367
|
+
description="Event stream handler from parent context for forwarding sub-agent events",
|
|
368
|
+
)
|
|
369
|
+
# Callback for notifying TUI when plan changes (Stage 11)
|
|
370
|
+
# Set by ChatScreen to receive plan updates for the Plan Panel widget
|
|
371
|
+
# Excluded from serialization as it's a callable
|
|
372
|
+
on_plan_changed: Callable[["ExecutionPlan | None"], None] | None = Field(
|
|
373
|
+
default=None,
|
|
374
|
+
exclude=True,
|
|
375
|
+
description="Callback to notify TUI when plan changes",
|
|
376
|
+
)
|