teddy-cli 0.1.0__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.
- teddy_cli-0.1.0.dist-info/LICENSE +677 -0
- teddy_cli-0.1.0.dist-info/METADATA +33 -0
- teddy_cli-0.1.0.dist-info/RECORD +143 -0
- teddy_cli-0.1.0.dist-info/WHEEL +4 -0
- teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
- teddy_executor/__init__.py +1 -0
- teddy_executor/__main__.py +335 -0
- teddy_executor/adapters/__init__.py +0 -0
- teddy_executor/adapters/inbound/__init__.py +0 -0
- teddy_executor/adapters/inbound/cli_formatter.py +107 -0
- teddy_executor/adapters/inbound/cli_helpers.py +249 -0
- teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
- teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
- teddy_executor/adapters/outbound/__init__.py +7 -0
- teddy_executor/adapters/outbound/console_interactor.py +212 -0
- teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
- teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
- teddy_executor/adapters/outbound/console_tooling.py +62 -0
- teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
- teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
- teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
- teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
- teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
- teddy_executor/adapters/outbound/shell_adapter.py +344 -0
- teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
- teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
- teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
- teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
- teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
- teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
- teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
- teddy_executor/container.py +333 -0
- teddy_executor/core/__init__.py +0 -0
- teddy_executor/core/domain/__init__.py +0 -0
- teddy_executor/core/domain/models/__init__.py +44 -0
- teddy_executor/core/domain/models/action_ports.py +28 -0
- teddy_executor/core/domain/models/change_set.py +10 -0
- teddy_executor/core/domain/models/exceptions.py +40 -0
- teddy_executor/core/domain/models/execution_report.py +65 -0
- teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
- teddy_executor/core/domain/models/plan.py +85 -0
- teddy_executor/core/domain/models/planning_ports.py +43 -0
- teddy_executor/core/domain/models/project_context.py +56 -0
- teddy_executor/core/domain/models/report_assembly_data.py +18 -0
- teddy_executor/core/domain/models/session.py +17 -0
- teddy_executor/core/domain/models/shell_output.py +12 -0
- teddy_executor/core/domain/models/web_search_results.py +26 -0
- teddy_executor/core/ports/__init__.py +0 -0
- teddy_executor/core/ports/inbound/__init__.py +0 -0
- teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
- teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
- teddy_executor/core/ports/inbound/init.py +15 -0
- teddy_executor/core/ports/inbound/plan_parser.py +52 -0
- teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
- teddy_executor/core/ports/inbound/plan_validator.py +26 -0
- teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
- teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
- teddy_executor/core/ports/outbound/__init__.py +34 -0
- teddy_executor/core/ports/outbound/config_service.py +29 -0
- teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
- teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
- teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
- teddy_executor/core/ports/outbound/llm_client.py +90 -0
- teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
- teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
- teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
- teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
- teddy_executor/core/ports/outbound/session_manager.py +97 -0
- teddy_executor/core/ports/outbound/session_repository.py +65 -0
- teddy_executor/core/ports/outbound/shell_executor.py +24 -0
- teddy_executor/core/ports/outbound/system_environment.py +25 -0
- teddy_executor/core/ports/outbound/time_service.py +28 -0
- teddy_executor/core/ports/outbound/user_interactor.py +126 -0
- teddy_executor/core/ports/outbound/web_scraper.py +24 -0
- teddy_executor/core/ports/outbound/web_searcher.py +25 -0
- teddy_executor/core/services/__init__.py +0 -0
- teddy_executor/core/services/action_changeset_builder.py +90 -0
- teddy_executor/core/services/action_diff_manager.py +110 -0
- teddy_executor/core/services/action_dispatcher.py +142 -0
- teddy_executor/core/services/action_executor.py +209 -0
- teddy_executor/core/services/action_factory.py +197 -0
- teddy_executor/core/services/action_parser_complex.py +216 -0
- teddy_executor/core/services/action_parser_strategies.py +84 -0
- teddy_executor/core/services/context_service.py +437 -0
- teddy_executor/core/services/edit_simulator.py +128 -0
- teddy_executor/core/services/execution_orchestrator.py +295 -0
- teddy_executor/core/services/execution_report_assembler.py +62 -0
- teddy_executor/core/services/init_service.py +80 -0
- teddy_executor/core/services/markdown_plan_parser.py +309 -0
- teddy_executor/core/services/markdown_report_formatter.py +143 -0
- teddy_executor/core/services/parser_infrastructure.py +222 -0
- teddy_executor/core/services/parser_metadata.py +153 -0
- teddy_executor/core/services/parser_reporting.py +267 -0
- teddy_executor/core/services/plan_validator.py +82 -0
- teddy_executor/core/services/planning_service.py +242 -0
- teddy_executor/core/services/prompt_manager.py +146 -0
- teddy_executor/core/services/session_lifecycle_manager.py +228 -0
- teddy_executor/core/services/session_loop_guard.py +46 -0
- teddy_executor/core/services/session_orchestrator.py +538 -0
- teddy_executor/core/services/session_planner.py +43 -0
- teddy_executor/core/services/session_pruning_service.py +438 -0
- teddy_executor/core/services/session_replanner.py +105 -0
- teddy_executor/core/services/session_repository.py +194 -0
- teddy_executor/core/services/session_service.py +529 -0
- teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
- teddy_executor/core/services/validation_rules/__init__.py +4 -0
- teddy_executor/core/services/validation_rules/edit.py +207 -0
- teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
- teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
- teddy_executor/core/services/validation_rules/execute.py +37 -0
- teddy_executor/core/services/validation_rules/filesystem.py +73 -0
- teddy_executor/core/services/validation_rules/helpers.py +178 -0
- teddy_executor/core/services/validation_rules/message.py +29 -0
- teddy_executor/core/utils/__init__.py +1 -0
- teddy_executor/core/utils/diff.py +57 -0
- teddy_executor/core/utils/io.py +75 -0
- teddy_executor/core/utils/markdown.py +131 -0
- teddy_executor/core/utils/serialization.py +39 -0
- teddy_executor/core/utils/string.py +351 -0
- teddy_executor/prompts.py +45 -0
- teddy_executor/registries/__init__.py +1 -0
- teddy_executor/registries/infrastructure.py +147 -0
- teddy_executor/registries/reviewer.py +57 -0
- teddy_executor/registries/validators.py +47 -0
- teddy_executor/resources/__init__.py +1 -0
- teddy_executor/resources/config/.gitignore +2 -0
- teddy_executor/resources/config/__init__.py +1 -0
- teddy_executor/resources/config/config.yaml +49 -0
- teddy_executor/resources/config/init.context +5 -0
- teddy_executor/resources/config/prompts/architect.xml +462 -0
- teddy_executor/resources/config/prompts/assistant.xml +336 -0
- teddy_executor/resources/config/prompts/debugger.xml +456 -0
- teddy_executor/resources/config/prompts/developer.xml +481 -0
- teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
- teddy_executor/resources/config/prompts/prototyper.xml +425 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from teddy_executor.core.domain.models import (
|
|
7
|
+
ActionData,
|
|
8
|
+
ActionLog,
|
|
9
|
+
ExecutionReport,
|
|
10
|
+
Plan,
|
|
11
|
+
ReportAssemblyData,
|
|
12
|
+
ActionStatus,
|
|
13
|
+
)
|
|
14
|
+
from teddy_executor.core.ports.inbound.plan_parser import InvalidPlanError
|
|
15
|
+
from teddy_executor.core.ports.inbound.run_plan_use_case import IRunPlanUseCase
|
|
16
|
+
from teddy_executor.core.domain.models.orchestrator_ports import OrchestratorPorts
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExecutionOrchestrator(IRunPlanUseCase):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
ports: OrchestratorPorts,
|
|
25
|
+
):
|
|
26
|
+
self._plan_parser = ports.plan_parser
|
|
27
|
+
self._plan_validator = ports.plan_validator
|
|
28
|
+
self._action_executor = ports.action_executor
|
|
29
|
+
self._file_system_manager = ports.file_system_manager
|
|
30
|
+
self._report_assembler = ports.report_assembler
|
|
31
|
+
self._user_interactor = ports.user_interactor
|
|
32
|
+
self._plan_reviewer = ports.plan_reviewer
|
|
33
|
+
|
|
34
|
+
def _perform_interactive_review(
|
|
35
|
+
self,
|
|
36
|
+
plan: Plan,
|
|
37
|
+
interactive: bool,
|
|
38
|
+
project_context: Optional[Any] = None,
|
|
39
|
+
) -> Plan | None:
|
|
40
|
+
"""Allows the user to review and modify the plan before execution."""
|
|
41
|
+
# Scenario: Communication Turns (MESSAGE) bypass TUI
|
|
42
|
+
# Single-action communication turns bypass approval for fluid conversation.
|
|
43
|
+
if plan.is_communication_turn():
|
|
44
|
+
return plan
|
|
45
|
+
|
|
46
|
+
# We only call the bulk review (TUI) if interactive is True AND a reviewer is present.
|
|
47
|
+
if interactive and self._plan_reviewer:
|
|
48
|
+
reviewed_plan = self._plan_reviewer.review(
|
|
49
|
+
plan, project_context=project_context
|
|
50
|
+
)
|
|
51
|
+
# Harden against Mocks in tests: if it's not a Plan or None, use the original plan
|
|
52
|
+
if reviewed_plan is not None and not isinstance(reviewed_plan, Plan):
|
|
53
|
+
return plan
|
|
54
|
+
return reviewed_plan
|
|
55
|
+
return plan
|
|
56
|
+
|
|
57
|
+
def _process_plan_actions(self, plan: Plan, interactive: bool) -> list[ActionLog]:
|
|
58
|
+
"""Iterates through actions and dispatches them."""
|
|
59
|
+
# Reset file hashes at the start of each plan execution to prevent
|
|
60
|
+
# stale hashes from previous turns from causing false pre-check failures.
|
|
61
|
+
self._action_executor.reset_file_hashes()
|
|
62
|
+
|
|
63
|
+
action_logs = []
|
|
64
|
+
halt_execution = False
|
|
65
|
+
for action in plan.actions:
|
|
66
|
+
action_log, should_halt = self._handle_action_in_loop(
|
|
67
|
+
action, plan, interactive, halt_execution
|
|
68
|
+
)
|
|
69
|
+
action_logs.append(action_log)
|
|
70
|
+
if should_halt:
|
|
71
|
+
halt_execution = True
|
|
72
|
+
return action_logs
|
|
73
|
+
|
|
74
|
+
def _handle_action_in_loop(
|
|
75
|
+
self, action: ActionData, plan: Plan, interactive: bool, halt_execution: bool
|
|
76
|
+
) -> tuple[ActionLog, bool]:
|
|
77
|
+
"""Logic for processing a single action within the execution loop."""
|
|
78
|
+
if halt_execution:
|
|
79
|
+
return (
|
|
80
|
+
self._action_executor.handle_skipped_action(
|
|
81
|
+
action,
|
|
82
|
+
"Skipped because a previous action failed. (Hint: use 'Allow Failure: true' in EXECUTE actions to proceed even with non-zero exit codes.)",
|
|
83
|
+
),
|
|
84
|
+
True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if action.executed and action.action_log:
|
|
88
|
+
should_halt = (
|
|
89
|
+
action.action_log.status == ActionStatus.FAILURE
|
|
90
|
+
and not action.params.get("allow_failure")
|
|
91
|
+
)
|
|
92
|
+
return action.action_log, should_halt
|
|
93
|
+
|
|
94
|
+
if not action.selected:
|
|
95
|
+
reason = "User deselected this action in the plan reviewer."
|
|
96
|
+
return (
|
|
97
|
+
self._action_executor.handle_skipped_action(action, reason),
|
|
98
|
+
False,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
action_log, captured_message = self._dispatch_single_action(
|
|
103
|
+
action, plan, interactive
|
|
104
|
+
)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
action_log = self._action_executor.handle_failed_action(action, str(e))
|
|
107
|
+
captured_message = ""
|
|
108
|
+
|
|
109
|
+
if captured_message:
|
|
110
|
+
plan.metadata["user_request"] = captured_message
|
|
111
|
+
|
|
112
|
+
should_halt = (
|
|
113
|
+
action_log.status == ActionStatus.FAILURE
|
|
114
|
+
and not action.params.get("allow_failure")
|
|
115
|
+
)
|
|
116
|
+
return action_log, should_halt
|
|
117
|
+
|
|
118
|
+
def _dispatch_single_action(
|
|
119
|
+
self, action: Any, plan: Plan, interactive: bool
|
|
120
|
+
) -> tuple[ActionLog, str]:
|
|
121
|
+
"""Handles the review and dispatch of a single action."""
|
|
122
|
+
agent_name = plan.metadata.get("Agent") or plan.metadata.get("agent")
|
|
123
|
+
reviewer_handled = False
|
|
124
|
+
should_dispatch = True
|
|
125
|
+
captured_message = ""
|
|
126
|
+
|
|
127
|
+
# Scenario: Communication Turns (MESSAGE) bypass confirmation
|
|
128
|
+
# Single communication actions bypass approval for fluid conversation.
|
|
129
|
+
is_communication_action = plan.is_communication_turn()
|
|
130
|
+
|
|
131
|
+
if interactive and self._plan_reviewer and not is_communication_action:
|
|
132
|
+
should_dispatch, captured_message = self._plan_reviewer.review_action(
|
|
133
|
+
action, len(plan.actions), agent_name=agent_name
|
|
134
|
+
)
|
|
135
|
+
reviewer_handled = True
|
|
136
|
+
|
|
137
|
+
if not should_dispatch:
|
|
138
|
+
reason = "User skipped this action in the plan reviewer."
|
|
139
|
+
return (
|
|
140
|
+
self._action_executor.handle_skipped_action(action, reason),
|
|
141
|
+
"",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if reviewer_handled:
|
|
145
|
+
# Reviewer (TUI) handled approval, execute immediately skipping isolation.
|
|
146
|
+
action_log, dispatch_message = self._action_executor.confirm_and_dispatch(
|
|
147
|
+
action,
|
|
148
|
+
interactive=False,
|
|
149
|
+
total_actions=len(plan.actions),
|
|
150
|
+
agent_name=agent_name,
|
|
151
|
+
is_session=plan.is_session,
|
|
152
|
+
skip_isolation=True,
|
|
153
|
+
)
|
|
154
|
+
return action_log, dispatch_message or captured_message
|
|
155
|
+
|
|
156
|
+
# Fallback to ActionExecutor interaction if no reviewer is present.
|
|
157
|
+
return self._action_executor.confirm_and_dispatch(
|
|
158
|
+
action,
|
|
159
|
+
interactive=interactive,
|
|
160
|
+
total_actions=len(plan.actions),
|
|
161
|
+
agent_name=agent_name,
|
|
162
|
+
is_session=plan.is_session,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _resolve_plan(
|
|
166
|
+
self,
|
|
167
|
+
plan: Optional[Plan],
|
|
168
|
+
plan_content: Optional[str],
|
|
169
|
+
plan_path: Optional[str],
|
|
170
|
+
) -> tuple[Plan, Optional[str]]:
|
|
171
|
+
"""Resolves the plan from content, path, or object, creating a temp file if needed."""
|
|
172
|
+
import tempfile
|
|
173
|
+
|
|
174
|
+
if plan:
|
|
175
|
+
return plan, None
|
|
176
|
+
|
|
177
|
+
temp_path = None
|
|
178
|
+
if plan_content is not None:
|
|
179
|
+
if not plan_path:
|
|
180
|
+
import os
|
|
181
|
+
|
|
182
|
+
fd, temp_path = tempfile.mkstemp(
|
|
183
|
+
prefix="teddy_manual_plan_", suffix=".md"
|
|
184
|
+
)
|
|
185
|
+
os.close(fd)
|
|
186
|
+
self._file_system_manager.write_file(temp_path, plan_content)
|
|
187
|
+
plan_path = temp_path
|
|
188
|
+
return (
|
|
189
|
+
self._plan_parser.parse(plan_content, plan_path=plan_path),
|
|
190
|
+
temp_path,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if plan_path is not None:
|
|
194
|
+
content = self._file_system_manager.read_file(plan_path)
|
|
195
|
+
return self._plan_parser.parse(content, plan_path=plan_path), None
|
|
196
|
+
|
|
197
|
+
raise ValueError("Must provide either plan, plan_content, or plan_path")
|
|
198
|
+
|
|
199
|
+
def _handle_aborted_execution(
|
|
200
|
+
self, plan: Plan, start_time: datetime, message: Optional[str]
|
|
201
|
+
) -> ExecutionReport:
|
|
202
|
+
"""Generates a report for an execution aborted by the user."""
|
|
203
|
+
from dataclasses import replace
|
|
204
|
+
from teddy_executor.core.domain.models import RunStatus
|
|
205
|
+
|
|
206
|
+
# If a message was captured in the TUI (via 'm' key), propagate it.
|
|
207
|
+
resolved_message = message or plan.metadata.get("user_request")
|
|
208
|
+
|
|
209
|
+
action_logs = []
|
|
210
|
+
for a in plan.actions:
|
|
211
|
+
if a.executed and a.action_log:
|
|
212
|
+
action_logs.append(a.action_log)
|
|
213
|
+
else:
|
|
214
|
+
action_logs.append(
|
|
215
|
+
self._action_executor.handle_skipped_action(
|
|
216
|
+
a, "Execution aborted by user."
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
report = self._report_assembler.assemble(
|
|
221
|
+
ReportAssemblyData(
|
|
222
|
+
plan=plan,
|
|
223
|
+
action_logs=action_logs,
|
|
224
|
+
start_time=start_time,
|
|
225
|
+
message=resolved_message,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
return replace(
|
|
229
|
+
report, run_summary=replace(report.run_summary, status=RunStatus.ABORTED)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def execute( # noqa: PLR0913
|
|
233
|
+
self,
|
|
234
|
+
plan: Optional[Plan] = None,
|
|
235
|
+
plan_content: Optional[str] = None,
|
|
236
|
+
plan_path: Optional[str] = None,
|
|
237
|
+
interactive: bool = True,
|
|
238
|
+
message: Optional[str] = None,
|
|
239
|
+
project_context: Optional[Any] = None,
|
|
240
|
+
) -> ExecutionReport:
|
|
241
|
+
import os
|
|
242
|
+
|
|
243
|
+
temp_plan_path = None
|
|
244
|
+
try:
|
|
245
|
+
plan, temp_plan_path = self._resolve_plan(plan, plan_content, plan_path)
|
|
246
|
+
start_time = datetime.now()
|
|
247
|
+
|
|
248
|
+
validation_errors = self._plan_validator.validate(plan)
|
|
249
|
+
if validation_errors:
|
|
250
|
+
error_msgs = "\n---\n".join(e.message for e in validation_errors)
|
|
251
|
+
raise InvalidPlanError(
|
|
252
|
+
f"Plan failed logical validation:\n{error_msgs}",
|
|
253
|
+
offending_nodes=[
|
|
254
|
+
e.offending_node for e in validation_errors if e.offending_node
|
|
255
|
+
],
|
|
256
|
+
validation_errors=validation_errors,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
reviewed_plan = self._perform_interactive_review(
|
|
260
|
+
plan, interactive, project_context=project_context
|
|
261
|
+
)
|
|
262
|
+
if reviewed_plan is None:
|
|
263
|
+
return self._handle_aborted_execution(plan, start_time, message)
|
|
264
|
+
|
|
265
|
+
action_logs = self._process_plan_actions(reviewed_plan, interactive)
|
|
266
|
+
return self._report_assembler.assemble(
|
|
267
|
+
ReportAssemblyData(
|
|
268
|
+
plan=reviewed_plan,
|
|
269
|
+
action_logs=action_logs,
|
|
270
|
+
start_time=start_time,
|
|
271
|
+
message=message,
|
|
272
|
+
is_session=reviewed_plan.is_session,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
finally:
|
|
276
|
+
if temp_plan_path and os.path.exists(temp_plan_path):
|
|
277
|
+
try:
|
|
278
|
+
os.remove(temp_plan_path)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.debug(
|
|
281
|
+
"Failed to clean up temporary plan file %s: %s",
|
|
282
|
+
temp_plan_path,
|
|
283
|
+
e,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def resume(
|
|
287
|
+
self,
|
|
288
|
+
session_name: str,
|
|
289
|
+
interactive: bool = True,
|
|
290
|
+
project_context: Optional[Any] = None,
|
|
291
|
+
) -> tuple[str, Optional[ExecutionReport]]:
|
|
292
|
+
"""Stateless orchestrator does not support session resumption."""
|
|
293
|
+
raise NotImplementedError(
|
|
294
|
+
"Session operations are not supported in stateless ExecutionOrchestrator."
|
|
295
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
|
|
4
|
+
from teddy_executor.core.domain.models import (
|
|
5
|
+
ActionLog,
|
|
6
|
+
ActionStatus,
|
|
7
|
+
ExecutionReport,
|
|
8
|
+
ReportAssemblyData,
|
|
9
|
+
RunStatus,
|
|
10
|
+
RunSummary,
|
|
11
|
+
)
|
|
12
|
+
from teddy_executor.core.ports.outbound.execution_report_assembler import (
|
|
13
|
+
IExecutionReportAssembler,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExecutionReportAssembler(IExecutionReportAssembler):
|
|
18
|
+
"""
|
|
19
|
+
Concrete implementation of the report assembly logic.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def assemble(
|
|
23
|
+
self,
|
|
24
|
+
data: ReportAssemblyData,
|
|
25
|
+
) -> ExecutionReport:
|
|
26
|
+
"""
|
|
27
|
+
Calculates the final run status and constructs a complete ExecutionReport.
|
|
28
|
+
"""
|
|
29
|
+
summary = RunSummary(
|
|
30
|
+
status=self._determine_overall_status(data.action_logs),
|
|
31
|
+
start_time=data.start_time,
|
|
32
|
+
end_time=datetime.now(),
|
|
33
|
+
)
|
|
34
|
+
return ExecutionReport(
|
|
35
|
+
run_summary=summary,
|
|
36
|
+
plan_title=data.plan.title,
|
|
37
|
+
rationale=data.plan.rationale,
|
|
38
|
+
user_request=data.message or data.plan.metadata.get("user_request"),
|
|
39
|
+
is_session=data.is_session or data.plan.is_session,
|
|
40
|
+
metadata=data.plan.metadata,
|
|
41
|
+
original_actions=data.plan.actions,
|
|
42
|
+
action_logs=data.action_logs,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _determine_overall_status(self, action_logs: Sequence[ActionLog]) -> RunStatus:
|
|
46
|
+
"""Determines the final run status based on the hierarchy of action outcomes."""
|
|
47
|
+
if not action_logs:
|
|
48
|
+
return RunStatus.SUCCESS
|
|
49
|
+
|
|
50
|
+
statuses = [log.status for log in action_logs]
|
|
51
|
+
if ActionStatus.FAILURE in statuses:
|
|
52
|
+
return RunStatus.FAILURE
|
|
53
|
+
|
|
54
|
+
# Success takes precedence: if any action succeeded, the run is a success.
|
|
55
|
+
if ActionStatus.SUCCESS in statuses:
|
|
56
|
+
return RunStatus.SUCCESS
|
|
57
|
+
|
|
58
|
+
# If every single action was skipped, the run is skipped.
|
|
59
|
+
if statuses and all(s == ActionStatus.SKIPPED for s in statuses):
|
|
60
|
+
return RunStatus.SKIPPED
|
|
61
|
+
|
|
62
|
+
return RunStatus.SUCCESS
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from importlib import resources
|
|
3
|
+
from teddy_executor.core.ports.inbound.init import IInitUseCase
|
|
4
|
+
from teddy_executor.core.ports.outbound.file_system_manager import IFileSystemManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InitService(IInitUseCase):
|
|
8
|
+
"""
|
|
9
|
+
Service for initializing projects.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, file_system: IFileSystemManager, config_dir: str | None = None):
|
|
13
|
+
self._file_system = file_system
|
|
14
|
+
# Find the config directory relative to the package root if not provided
|
|
15
|
+
if config_dir:
|
|
16
|
+
self._config_dir = config_dir
|
|
17
|
+
else:
|
|
18
|
+
# Use importlib.resources to find the bundled config templates
|
|
19
|
+
resource_path = resources.files("teddy_executor.resources.config")
|
|
20
|
+
# Ensure we resolve to an absolute string for compatibility with the FileSystem port
|
|
21
|
+
self._config_dir = os.path.abspath(str(resource_path))
|
|
22
|
+
|
|
23
|
+
def _get_default_content(self, filename: str) -> str | None:
|
|
24
|
+
"""Loads default content from the config directory using the file system port."""
|
|
25
|
+
try:
|
|
26
|
+
target_path = os.path.join(self._config_dir, filename)
|
|
27
|
+
if self._file_system.path_exists(target_path):
|
|
28
|
+
return self._file_system.read_file(target_path)
|
|
29
|
+
except Exception: # nosec B110
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def _init_prompts(self) -> None:
|
|
35
|
+
"""Copies bundled prompt XMLs to .teddy/prompts/ if missing."""
|
|
36
|
+
prompts_dir = ".teddy/prompts"
|
|
37
|
+
if not self._file_system.path_exists(prompts_dir):
|
|
38
|
+
self._file_system.create_directory(prompts_dir)
|
|
39
|
+
|
|
40
|
+
prompt_files = [
|
|
41
|
+
"architect.xml",
|
|
42
|
+
"assistant.xml",
|
|
43
|
+
"debugger.xml",
|
|
44
|
+
"developer.xml",
|
|
45
|
+
"pathfinder.xml",
|
|
46
|
+
"prototyper.xml",
|
|
47
|
+
]
|
|
48
|
+
for fname in prompt_files:
|
|
49
|
+
target_path = f"{prompts_dir}/{fname}"
|
|
50
|
+
if not self._file_system.path_exists(target_path):
|
|
51
|
+
content = self._get_default_content(f"prompts/{fname}")
|
|
52
|
+
if content is not None:
|
|
53
|
+
self._file_system.write_file(target_path, content)
|
|
54
|
+
|
|
55
|
+
def ensure_initialized(self) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Checks for and creates the .teddy directory and default files.
|
|
58
|
+
"""
|
|
59
|
+
if not self._file_system.path_exists(".teddy"):
|
|
60
|
+
self._file_system.create_directory(".teddy")
|
|
61
|
+
|
|
62
|
+
gitignore_path = ".teddy/.gitignore"
|
|
63
|
+
if not self._file_system.path_exists(gitignore_path):
|
|
64
|
+
content = self._get_default_content(".gitignore")
|
|
65
|
+
if content is not None:
|
|
66
|
+
self._file_system.write_file(gitignore_path, content)
|
|
67
|
+
|
|
68
|
+
config_path = ".teddy/config.yaml"
|
|
69
|
+
if not self._file_system.path_exists(config_path):
|
|
70
|
+
content = self._get_default_content("config.yaml")
|
|
71
|
+
if content is not None:
|
|
72
|
+
self._file_system.write_file(config_path, content)
|
|
73
|
+
|
|
74
|
+
init_context_path = ".teddy/init.context"
|
|
75
|
+
if not self._file_system.path_exists(init_context_path):
|
|
76
|
+
content = self._get_default_content("init.context")
|
|
77
|
+
if content is not None:
|
|
78
|
+
self._file_system.write_file(init_context_path, content)
|
|
79
|
+
|
|
80
|
+
self._init_prompts()
|