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,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Optional, cast
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from teddy_executor.adapters.inbound.textual_plan_reviewer_app import ReviewerApp
|
|
10
|
+
from teddy_executor.core.domain.models.plan import ActionData
|
|
11
|
+
|
|
12
|
+
from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
|
|
13
|
+
ConfirmScreen,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Low-level editor helpers
|
|
20
|
+
def handle_mock_editor(path: Any, output: str) -> str:
|
|
21
|
+
"""Helper for mock editor output in tests."""
|
|
22
|
+
if path and isinstance(path, (str, os.PathLike)):
|
|
23
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
24
|
+
f.write(output)
|
|
25
|
+
return output
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def spawn_editor(cmd: list[str], path: Any) -> None:
|
|
29
|
+
"""Spawns an external editor process."""
|
|
30
|
+
import subprocess # nosec B404
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
subprocess.Popen( # nosec B603
|
|
34
|
+
cmd + [str(path)],
|
|
35
|
+
stdin=subprocess.DEVNULL,
|
|
36
|
+
stdout=subprocess.DEVNULL,
|
|
37
|
+
stderr=subprocess.DEVNULL,
|
|
38
|
+
)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.debug("Failed to spawn editor: %s", e)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_mock_diff(p_file: Any, before: str, delete_fn: Any) -> bool:
|
|
44
|
+
"""Helper for mock diff output in tests."""
|
|
45
|
+
mock_out = os.environ.get("TEDDY_TEST_MOCK_EDITOR_OUTPUT")
|
|
46
|
+
if mock_out:
|
|
47
|
+
with open(p_file, "w", encoding="utf-8") as f:
|
|
48
|
+
f.write(mock_out)
|
|
49
|
+
delete_fn(before)
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def prepare_after_file(path: Any, proposed: str) -> None:
|
|
55
|
+
"""Ensures the 'after' file is ready for diffing/editing."""
|
|
56
|
+
if os.path.exists(path):
|
|
57
|
+
os.chmod(path, 0o644)
|
|
58
|
+
if not os.path.exists(path) or os.path.getsize(path) == 0:
|
|
59
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
60
|
+
f.write(str(proposed))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def harvest_edit_diff(action: Any, p_file: Any, original: str, proposed: str) -> None:
|
|
64
|
+
"""Helper to harvest diff results and update action params."""
|
|
65
|
+
try:
|
|
66
|
+
with open(p_file, "r", encoding="utf-8") as f:
|
|
67
|
+
final: Optional[str] = f.read()
|
|
68
|
+
except Exception:
|
|
69
|
+
final = None
|
|
70
|
+
if final is not None and str(final) != str(proposed):
|
|
71
|
+
action.params["edits"] = [{"find": original, "replace": str(final)}]
|
|
72
|
+
action.params.pop("content", None)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def launch_editor(
|
|
76
|
+
app: "ReviewerApp",
|
|
77
|
+
initial_content: str,
|
|
78
|
+
suffix: str = ".txt",
|
|
79
|
+
persistent_path: Optional[str] = None,
|
|
80
|
+
skip_confirm: bool = False,
|
|
81
|
+
) -> Optional[str]:
|
|
82
|
+
"""Launches an external editor non-blockingly and waits for TUI confirmation."""
|
|
83
|
+
mock_out = os.environ.get("TEDDY_TEST_MOCK_EDITOR_OUTPUT")
|
|
84
|
+
temp_file = persistent_path or app._system_env.create_temp_file(suffix=suffix)
|
|
85
|
+
is_temp = persistent_path is None
|
|
86
|
+
|
|
87
|
+
if mock_out:
|
|
88
|
+
handle_mock_editor(temp_file, mock_out)
|
|
89
|
+
confirmed = (
|
|
90
|
+
True
|
|
91
|
+
if app.is_headless or skip_confirm
|
|
92
|
+
else await app.push_screen_wait(ConfirmScreen())
|
|
93
|
+
)
|
|
94
|
+
return mock_out if confirmed else None
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
if is_temp or (
|
|
98
|
+
not os.path.exists(temp_file) or os.path.getsize(temp_file) == 0
|
|
99
|
+
):
|
|
100
|
+
with open(temp_file, "w", encoding="utf-8") as f:
|
|
101
|
+
f.write(str(initial_content))
|
|
102
|
+
|
|
103
|
+
editor_cmd = app._console_tooling.find_editor()
|
|
104
|
+
if not editor_cmd:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
if os.path.exists(temp_file):
|
|
108
|
+
os.chmod(temp_file, 0o644)
|
|
109
|
+
|
|
110
|
+
spawn_editor(editor_cmd, temp_file)
|
|
111
|
+
return await _confirm_and_harvest(
|
|
112
|
+
app, temp_file, initial_content, is_temp, skip_confirm=skip_confirm
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.debug("Failed to launch editor flow: %s", e)
|
|
116
|
+
return None
|
|
117
|
+
finally:
|
|
118
|
+
if is_temp:
|
|
119
|
+
app._system_env.delete_file(temp_file)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def _confirm_and_harvest(
|
|
123
|
+
app: ReviewerApp, path: Any, initial: str, is_temp: bool, skip_confirm: bool = False
|
|
124
|
+
) -> Optional[str]:
|
|
125
|
+
confirmed = (
|
|
126
|
+
True
|
|
127
|
+
if app.is_headless or skip_confirm
|
|
128
|
+
else await app.push_screen_wait(ConfirmScreen())
|
|
129
|
+
)
|
|
130
|
+
if confirmed:
|
|
131
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
132
|
+
return f.read()
|
|
133
|
+
if not is_temp:
|
|
134
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
135
|
+
f.write(str(initial))
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def preview_edit_diff_viewer(
|
|
140
|
+
app: ReviewerApp,
|
|
141
|
+
action: ActionData,
|
|
142
|
+
diff_viewer: list[str],
|
|
143
|
+
original: str,
|
|
144
|
+
proposed: str,
|
|
145
|
+
) -> bool:
|
|
146
|
+
import subprocess # nosec B404
|
|
147
|
+
|
|
148
|
+
path_str = cast(str, action.params.get("path", ""))
|
|
149
|
+
before = _setup_before_file(app, path_str, original)
|
|
150
|
+
p_file = action.pending_temp_file
|
|
151
|
+
|
|
152
|
+
if p_file and isinstance(p_file, (str, os.PathLike)):
|
|
153
|
+
if handle_mock_diff(p_file, before, app._system_env.delete_file):
|
|
154
|
+
return True
|
|
155
|
+
prepare_after_file(p_file, proposed)
|
|
156
|
+
try:
|
|
157
|
+
subprocess.Popen( # nosec B603
|
|
158
|
+
diff_viewer + [str(before), str(p_file)],
|
|
159
|
+
stdin=subprocess.DEVNULL,
|
|
160
|
+
stdout=subprocess.DEVNULL,
|
|
161
|
+
stderr=subprocess.DEVNULL,
|
|
162
|
+
)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.debug("Failed to launch diff viewer: %s", e)
|
|
165
|
+
|
|
166
|
+
confirmed = True if app.is_headless else await app.push_screen_wait(ConfirmScreen())
|
|
167
|
+
app._system_env.delete_file(before)
|
|
168
|
+
return _process_diff_result(confirmed, action, p_file, original, proposed)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _setup_before_file(app: ReviewerApp, path: str, content: str) -> str:
|
|
172
|
+
suffix = pathlib.Path(path).suffix or ".txt"
|
|
173
|
+
before = app._system_env.create_temp_file(suffix=f".before{suffix}")
|
|
174
|
+
with open(before, "w", encoding="utf-8") as f:
|
|
175
|
+
f.write(content)
|
|
176
|
+
os.chmod(before, 0o444)
|
|
177
|
+
return before
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _process_diff_result(
|
|
181
|
+
confirmed: bool, action: ActionData, p_file: Any, original: str, proposed: str
|
|
182
|
+
) -> bool:
|
|
183
|
+
if confirmed and p_file and isinstance(p_file, (str, os.PathLike)):
|
|
184
|
+
action.modified = True
|
|
185
|
+
if "edits" not in action.modified_fields:
|
|
186
|
+
action.modified_fields.append("edits")
|
|
187
|
+
harvest_edit_diff(action, p_file, original, proposed)
|
|
188
|
+
return True
|
|
189
|
+
if not confirmed and p_file and isinstance(p_file, (str, os.PathLike)):
|
|
190
|
+
with open(p_file, "w", encoding="utf-8") as f:
|
|
191
|
+
f.write(str(proposed))
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def handle_list_view_selected(
|
|
196
|
+
app: "ReviewerApp", item: Any, update_fn: Any
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Handle parameter editing when a DetailItem is selected in the right pane."""
|
|
199
|
+
from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
|
|
200
|
+
ActionTree,
|
|
201
|
+
PathInputScreen,
|
|
202
|
+
ParameterEditModal,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
node = app.query_one(ActionTree).cursor_node
|
|
206
|
+
if not node or not node.data or not hasattr(item, "data"):
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
action, key, val = node.data, item.data.get("key"), item.data.get("val")
|
|
210
|
+
from teddy_executor.core.domain.models.plan import ActionData
|
|
211
|
+
|
|
212
|
+
if not isinstance(action, ActionData) or action.executed:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if key == "path":
|
|
216
|
+
new_val = await cast(Any, app.push_screen_wait(PathInputScreen(str(val))))
|
|
217
|
+
else:
|
|
218
|
+
if not isinstance(val, (str, int, float, bool, list)) and val is not None:
|
|
219
|
+
return
|
|
220
|
+
v_str = ", ".join(map(str, val)) if isinstance(val, list) else str(val)
|
|
221
|
+
new_val = await cast(
|
|
222
|
+
Any, app.push_screen_wait(ParameterEditModal(f"{key}:", v_str))
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if new_val is not None and str(new_val) != str(val):
|
|
226
|
+
from teddy_executor.adapters.inbound.textual_plan_reviewer_helpers import (
|
|
227
|
+
_apply_param_edit,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
_apply_param_edit(action, key, new_val)
|
|
231
|
+
action.modified = True
|
|
232
|
+
if key and key not in action.modified_fields:
|
|
233
|
+
action.modified_fields.append(key)
|
|
234
|
+
app._refresh_node(node)
|
|
235
|
+
update_fn(app, action)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def handle_edit_action(
|
|
239
|
+
app: "ReviewerApp", node: Any, action: Any, update_fn: Any
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Handles the (e)dit key logic by branching to modals or external editor."""
|
|
242
|
+
from teddy_executor.adapters.inbound.textual_plan_reviewer_widgets import (
|
|
243
|
+
ParameterEditModal,
|
|
244
|
+
)
|
|
245
|
+
from teddy_executor.adapters.inbound.textual_plan_reviewer_previews import (
|
|
246
|
+
do_preview_logic,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if action.type == "EXECUTE":
|
|
250
|
+
val = action.params.get("command", "")
|
|
251
|
+
new_val = await cast(
|
|
252
|
+
Any, app.push_screen_wait(ParameterEditModal("Command:", val))
|
|
253
|
+
)
|
|
254
|
+
if new_val is not None and new_val != val:
|
|
255
|
+
action.params["command"] = new_val
|
|
256
|
+
action.modified = True
|
|
257
|
+
if "command" not in action.modified_fields:
|
|
258
|
+
action.modified_fields.append("command")
|
|
259
|
+
app._refresh_node(node)
|
|
260
|
+
update_fn(app, action)
|
|
261
|
+
elif action.type == "RESEARCH":
|
|
262
|
+
val = action.params.get("queries", [])
|
|
263
|
+
val_str = ", ".join(val) if isinstance(val, list) else str(val)
|
|
264
|
+
new_val = await cast(
|
|
265
|
+
Any,
|
|
266
|
+
app.push_screen_wait(
|
|
267
|
+
ParameterEditModal("Queries (comma separated):", val_str)
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
if new_val is not None and new_val != val_str:
|
|
271
|
+
action.params["queries"] = [
|
|
272
|
+
q.strip() for q in new_val.split(",") if q.strip()
|
|
273
|
+
]
|
|
274
|
+
action.modified = True
|
|
275
|
+
if "queries" not in action.modified_fields:
|
|
276
|
+
action.modified_fields.append("queries")
|
|
277
|
+
app._refresh_node(node)
|
|
278
|
+
update_fn(app, action)
|
|
279
|
+
else:
|
|
280
|
+
await do_preview_logic(app, node, action)
|
|
281
|
+
update_fn(app, action)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from teddy_executor.adapters.inbound.textual_plan_reviewer_app import ReviewerApp
|
|
10
|
+
from teddy_executor.core.domain.models.plan import ActionData
|
|
11
|
+
from teddy_executor.core.domain.models.execution_report import ActionLog
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def harvest_action_content(action: Any, instruction_marker: str) -> None:
|
|
15
|
+
"""Harvest modified content from a pending temporary file back to the action."""
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
is_valid_path = isinstance(action.pending_temp_file, (str, os.PathLike))
|
|
19
|
+
if not (
|
|
20
|
+
action.pending_temp_file
|
|
21
|
+
and is_valid_path
|
|
22
|
+
and os.path.exists(action.pending_temp_file)
|
|
23
|
+
):
|
|
24
|
+
return
|
|
25
|
+
try:
|
|
26
|
+
with open(action.pending_temp_file, "r", encoding="utf-8") as f:
|
|
27
|
+
new_content = f.read()
|
|
28
|
+
mapping = {"CREATE": "content", "EXECUTE": "command", "RESEARCH": "queries"}
|
|
29
|
+
if action.type in mapping:
|
|
30
|
+
action.params[mapping[action.type]] = new_content
|
|
31
|
+
os.remove(action.pending_temp_file)
|
|
32
|
+
action.pending_temp_file = None
|
|
33
|
+
except Exception as e:
|
|
34
|
+
import logging
|
|
35
|
+
|
|
36
|
+
logging.getLogger(__name__).debug(
|
|
37
|
+
"Failed to harvest action content from %s: %s", action.pending_temp_file, e
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_action_log(log: ActionLog) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Formats an ActionLog entry using the global MarkdownReportFormatter to ensure
|
|
44
|
+
exact formatting consistency with the final execution report.
|
|
45
|
+
"""
|
|
46
|
+
from datetime import datetime, timezone
|
|
47
|
+
|
|
48
|
+
from teddy_executor.core.domain.models.execution_report import (
|
|
49
|
+
ExecutionReport,
|
|
50
|
+
RunStatus,
|
|
51
|
+
RunSummary,
|
|
52
|
+
)
|
|
53
|
+
from teddy_executor.core.services.markdown_report_formatter import (
|
|
54
|
+
MarkdownReportFormatter,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
now = datetime.now(timezone.utc)
|
|
58
|
+
# Map the action status to a run status for the synthetic report summary
|
|
59
|
+
run_status = RunStatus.SUCCESS
|
|
60
|
+
if log.status.value in ["FAILURE", "SKIPPED"]:
|
|
61
|
+
# Fallback mapping if ActionStatus enum string matches RunStatus
|
|
62
|
+
run_status = getattr(RunStatus, log.status.value, RunStatus.FAILURE)
|
|
63
|
+
|
|
64
|
+
synthetic_report = ExecutionReport(
|
|
65
|
+
plan_title=f"{log.action_type} Details",
|
|
66
|
+
run_summary=RunSummary(
|
|
67
|
+
status=run_status,
|
|
68
|
+
start_time=now,
|
|
69
|
+
end_time=now,
|
|
70
|
+
),
|
|
71
|
+
action_logs=[log],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
formatter = MarkdownReportFormatter()
|
|
75
|
+
return formatter.format(synthetic_report)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resolve_action_parameters(action: ActionData) -> dict[str, Any]:
|
|
79
|
+
"""Resolves the full set of parameters for an action, including defaults."""
|
|
80
|
+
from teddy_executor.core.domain.models.plan import (
|
|
81
|
+
DEFAULT_SIMILARITY_THRESHOLD,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Base defaults for all actions
|
|
85
|
+
defaults: dict[str, Any] = {
|
|
86
|
+
"overwrite": False,
|
|
87
|
+
"match_all": False,
|
|
88
|
+
"allow_failure": False,
|
|
89
|
+
"background": False,
|
|
90
|
+
"timeout": 60.0,
|
|
91
|
+
"similarity_threshold": DEFAULT_SIMILARITY_THRESHOLD,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Type-specific relevant parameters
|
|
95
|
+
param_map = {
|
|
96
|
+
"CREATE": ["path", "overwrite", "description"],
|
|
97
|
+
"EDIT": ["path", "match_all", "similarity_threshold", "description"],
|
|
98
|
+
"EXECUTE": [
|
|
99
|
+
"command",
|
|
100
|
+
"allow_failure",
|
|
101
|
+
"background",
|
|
102
|
+
"timeout",
|
|
103
|
+
"tail",
|
|
104
|
+
"description",
|
|
105
|
+
],
|
|
106
|
+
"READ": ["resource", "lines", "description"],
|
|
107
|
+
"RESEARCH": ["queries", "description"],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
keys = param_map.get(action.type, [])
|
|
111
|
+
# Hide description from the detail view to reduce clutter
|
|
112
|
+
keys = [k for k in keys if k != "description"]
|
|
113
|
+
resolved: dict[str, Any] = {}
|
|
114
|
+
for key in keys:
|
|
115
|
+
# Use provided value if exists, else fallback to default (if one exists for that key)
|
|
116
|
+
val = action.params.get(key)
|
|
117
|
+
if val is None and key in defaults:
|
|
118
|
+
val = defaults[key]
|
|
119
|
+
|
|
120
|
+
# Format lists as comma-separated strings for clean UI display
|
|
121
|
+
if isinstance(val, list):
|
|
122
|
+
val = ", ".join(map(str, val))
|
|
123
|
+
|
|
124
|
+
resolved[key] = val
|
|
125
|
+
|
|
126
|
+
# After execution, some parameters are hidden from the preview to reduce clutter
|
|
127
|
+
if action.executed:
|
|
128
|
+
# Hide large content/queries/commands once executed; view via 'd'
|
|
129
|
+
for hidden_key in ["content", "queries", "command", "edits"]:
|
|
130
|
+
resolved.pop(hidden_key, None)
|
|
131
|
+
|
|
132
|
+
log = action.action_log
|
|
133
|
+
if log:
|
|
134
|
+
resolved["status"] = log.status.value
|
|
135
|
+
if log.failed_command:
|
|
136
|
+
resolved["failed_command"] = log.failed_command
|
|
137
|
+
# Details are intentionally omitted here; they are viewed via 'd' binding
|
|
138
|
+
|
|
139
|
+
return resolved
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def orchestrate_execution(app: ReviewerApp, node: Any, update_fn: Any) -> None:
|
|
143
|
+
"""Orchestrates the execution of a single action node."""
|
|
144
|
+
from teddy_executor.core.domain.models.plan import ActionData, ExecutionStatus
|
|
145
|
+
|
|
146
|
+
action: Any = node.data
|
|
147
|
+
if (
|
|
148
|
+
not isinstance(action, ActionData)
|
|
149
|
+
or action.executed
|
|
150
|
+
or action.state == ExecutionStatus.RUNNING
|
|
151
|
+
):
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
action.state = ExecutionStatus.RUNNING
|
|
155
|
+
app._refresh_node(node)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
import anyio
|
|
159
|
+
|
|
160
|
+
from teddy_executor.core.domain.models.execution_report import ActionStatus
|
|
161
|
+
|
|
162
|
+
log = await anyio.to_thread.run_sync(
|
|
163
|
+
_execute_silently, app._action_dispatcher, action
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
action.executed, action.action_log = True, log
|
|
167
|
+
action.state = (
|
|
168
|
+
ExecutionStatus.SUCCESS
|
|
169
|
+
if log.status == ActionStatus.SUCCESS
|
|
170
|
+
else ExecutionStatus.FAILURE
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# For MESSAGE actions, print the user's typed reply to the console
|
|
174
|
+
# since the TUI execution path bypasses SessionOrchestrator.execute()
|
|
175
|
+
# where _print_user_message is called.
|
|
176
|
+
if (
|
|
177
|
+
log.status == ActionStatus.SUCCESS
|
|
178
|
+
and action.type.upper() == "MESSAGE"
|
|
179
|
+
and log.details
|
|
180
|
+
and isinstance(log.details, str)
|
|
181
|
+
and log.details.strip()
|
|
182
|
+
):
|
|
183
|
+
import typer
|
|
184
|
+
|
|
185
|
+
typer.secho("")
|
|
186
|
+
typer.secho("User Message:")
|
|
187
|
+
typer.secho(log.details.strip())
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logging.getLogger(__name__).debug("Background execution failed: %s", e)
|
|
190
|
+
action.executed, action.state = True, ExecutionStatus.FAILURE
|
|
191
|
+
finally:
|
|
192
|
+
app._refresh_node(node)
|
|
193
|
+
update_fn(app, action)
|
|
194
|
+
app.refresh_bindings()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _execute_silently(dispatcher: Any, act: Any) -> Any:
|
|
198
|
+
"""Helper to run dispatcher silently."""
|
|
199
|
+
|
|
200
|
+
logger = logging.getLogger("teddy_executor.core.services.action_dispatcher")
|
|
201
|
+
old_level = logger.level
|
|
202
|
+
logger.setLevel(logging.WARNING)
|
|
203
|
+
f = io.StringIO()
|
|
204
|
+
try:
|
|
205
|
+
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
|
|
206
|
+
return dispatcher.dispatch_and_execute(act)
|
|
207
|
+
finally:
|
|
208
|
+
logger.setLevel(old_level)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def execute_step_logic(app: ReviewerApp, node: Any, update_fn: Any) -> None:
|
|
212
|
+
"""Executes the action with real-time state transitions and feedback."""
|
|
213
|
+
await orchestrate_execution(app, node, update_fn)
|